Intermediate Guide: Topic Overview, Best Practices & Key Takeaways
This tutorial is an intermediate, hands-on guide to Git workflows for collaborative software development. It focuses on practical commands, repeatable habits, and the reasoning behind them—so you can work confidently in teams, reduce merge pain, and keep history readable.
1) Topic Overview: What “Intermediate Git” Really Means
Most people learn Git in phases:
- Beginner:
git init,git add,git commit,git push,git pull. - Intermediate: branching strategies, rebasing vs merging, conflict resolution, history editing, safe force-push, bisecting, hooks, submodules, and release tagging.
- Advanced: custom plumbing commands, complex refspecs, partial clones, monorepo scaling, advanced hooks/CI policy enforcement.
This guide targets the intermediate layer: you already know how to commit and push, but you want to:
- Collaborate without stepping on others’ work
- Keep your branch history clean and reviewable
- Handle conflicts calmly and systematically
- Recover from mistakes (lost commits, wrong merges, bad rebases)
- Use Git as a debugging tool (not just a storage tool)
2) Mental Model: Refs, Commits, and the Working Tree
Before best practices, align on how Git thinks:
- Working tree: your files on disk
- Index (staging area): what will go into the next commit
- Repository (commits): immutable snapshots linked by parent pointers
- Refs: movable pointers like
HEAD, branch names, and tags
Quick introspection commands:
git status
git log --oneline --decorate --graph --all --max-count=20
git show HEAD
git diff
git diff --staged
Key idea: a commit is a snapshot; a branch is just a pointer to a commit. Most “Git problems” become simpler when you remember that.
3) Repository Setup: Configure Git Like a Professional
3.1 Identity, editor, and defaults
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global core.editor "code --wait" # or "vim", "nano"
Helpful defaults:
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global rebase.autoStash true
git config --global fetch.prune true
What these do:
pull.rebase=true:git pullrebases your local commits on top of remote changes (often cleaner than merge commits).rebase.autoStash=true: stashes local uncommitted changes during rebase/pull and restores them.fetch.prune=true: removes stale remote-tracking branches you no longer need.
3.2 Useful aliases
git config --global alias.lg "log --oneline --decorate --graph --all"
git config --global alias.st "status -sb"
git config --global alias.co "checkout"
git config --global alias.br "branch -vv"
git config --global alias.unstage "restore --staged"
git config --global alias.last "log -1 --stat"
Now you can run:
git lg
git st
git br
4) Branching Strategy: Choosing a Workflow That Fits
There isn’t one “best” workflow, but there are patterns with predictable trade-offs.
4.1 Trunk-based development (common in fast-moving teams)
mainstays releasable- Feature branches are short-lived
- Integrate frequently (daily or more)
Typical flow:
git checkout main
git pull
git checkout -b feature/add-search
# work...
git add -A
git commit -m "Add basic search UI"
git push -u origin feature/add-search
Then open a Pull Request (PR), get review, merge.
Why it works: small diffs, fewer conflicts, faster feedback.
4.2 GitFlow (common in release-heavy environments)
mainfor releasesdevelopfor integrationrelease/*,hotfix/*,feature/*branches
It’s heavier but can be useful when you must support multiple release trains.
5) Keeping Your Branch Up to Date: Merge vs Rebase
5.1 Merging
If you merge main into your feature branch:
git checkout feature/add-search
git fetch origin
git merge origin/main
Pros:
- Preserves exact history of integration events
- Avoids rewriting commits (safer for shared branches)
Cons:
- Can create noisy merge commits if done frequently
5.2 Rebasing (common for feature branches before PR)
Rebase replays your commits on top of another base:
git checkout feature/add-search
git fetch origin
git rebase origin/main
Pros:
- Linear history (often easier to review)
- Keeps PR focused
Cons:
- Rewrites commit hashes (dangerous if others depend on your branch)
Rule of thumb:
- Rebase your local/private feature branches freely.
- Avoid rebasing branches that others have pulled—unless your team explicitly agrees and you coordinate.
5.3 Safe force-push after rebase
If you rebased a branch you already pushed, you must update the remote:
git push --force-with-lease
--force-with-lease is safer than --force: it refuses to overwrite remote changes you don’t have locally.
6) Commit Craft: Messages, Granularity, and Fixups
6.1 Write meaningful commit messages
A solid format:
- Subject line: imperative, concise, < 72 chars
- Optional body: why, context, trade-offs
Example:
git commit -m "Validate email format on signup" -m "Prevents invalid addresses from reaching downstream systems."
6.2 Make commits reviewable
Best practice: each commit should ideally:
- build and pass tests (when feasible)
- represent one logical change
- be easy to revert
Use interactive staging to craft commits:
git add -p
This lets you stage hunks selectively.
6.3 Fixup commits and autosquash
When addressing review feedback, don’t always create messy “WIP” commits. Use fixups:
git commit --fixup <commit-hash>
git rebase -i --autosquash origin/main
This automatically arranges fixup commits to be squashed into their target commits during interactive rebase.
7) Interactive Rebase: Editing History (Carefully)
Interactive rebase is the intermediate Git superpower.
7.1 Squash and reorder commits
git fetch origin
git rebase -i origin/main
You’ll see a todo list like:
pick a1b2c3d Add basic search UI
pick d4e5f6g Wire search to API
pick 123abcd Fix lint
You can change to:
pick a1b2c3d Add basic search UI
squash d4e5f6g Wire search to API
fixup 123abcd Fix lint
squash: merges commits and lets you edit the messagefixup: merges commits and discards the fixup message
7.2 Edit a past commit
Change pick to edit, then:
# after rebase stops:
git status
git commit --amend
git rebase --continue
7.3 Abort if you get into trouble
git rebase --abort
This restores the branch to the state before the rebase started.
8) Conflict Resolution: A Repeatable Process
Conflicts are normal. The goal is to resolve them systematically.
8.1 When a conflict occurs
During merge or rebase:
git status
Git will mark conflicted files. Open them and look for markers:
<<<<<<< HEAD
your changes
=======
their changes
>>>>>>> origin/main
Resolve by editing to the desired final content, then:
git add path/to/file
Continue:
-
If rebasing:
git rebase --continue -
If merging:
git commit
8.2 Use a merge tool (optional but helpful)
git mergetool
You can configure tools like VS Code, Beyond Compare, or kdiff3.
8.3 Rerere: reuse recorded conflict resolutions
If you often resolve similar conflicts, enable rerere:
git config --global rerere.enabled true
Git will remember how you resolved a conflict and try to apply it automatically next time.
9) Undoing Mistakes: Reset, Restore, Revert, and Reflog
Intermediate Git users distinguish between “undo locally” and “undo in shared history.”
9.1 Discard uncommitted changes
Discard changes in a file:
git restore path/to/file
Unstage something:
git restore --staged path/to/file
Discard everything (dangerous):
git restore .
9.2 Undo commits safely on shared branches: git revert
If a bad commit is already on main, prefer revert:
git checkout main
git pull
git revert <bad-commit-hash>
git push
This creates a new commit that undoes the changes without rewriting history.
9.3 Reset (rewriting local history)
Move branch pointer back:
git reset --soft HEAD~1 # keep changes staged
git reset --mixed HEAD~1 # keep changes unstaged (default)
git reset --hard HEAD~1 # discard changes completely
Use --hard only when you’re sure you don’t need the work.
9.4 Reflog: recover “lost” commits
If you reset or rebase and think you lost commits:
git reflog
Find the previous HEAD state and restore:
git checkout -b recovery <reflog-hash>
# or:
git reset --hard <reflog-hash>
Reflog is your safety net for local history.
10) Stashing: Context Switching Without Mess
Stash saves working tree changes temporarily.
git stash push -m "WIP: search UI"
git stash list
git stash show -p stash@{0}
git stash pop
If you want to keep the stash (apply but don’t drop):
git stash apply stash@{0}
Stash only tracks files Git already knows unless you include untracked:
git stash push -u -m "Include untracked files"
11) Code Review Hygiene: Make PRs Easy to Review
11.1 Keep PRs small and focused
A PR that changes 30 files and mixes refactors with features is hard to review and easy to break. Prefer:
- one feature per PR
- separate PR for refactor or dependency bump
11.2 Ensure your branch is based on current main
Before opening or updating a PR:
git fetch origin
git rebase origin/main
# resolve conflicts if needed
git push --force-with-lease
11.3 Use draft PRs and incremental commits
If your platform supports it, open a draft PR early for visibility. Use fixup commits during iteration, then squash/rebase before final review.
12) Tags and Releases: Marking Important Points
Tags are named pointers, often used for releases.
Create an annotated tag:
git tag -a v1.4.0 -m "Release v1.4.0"
git push origin v1.4.0
List tags:
git tag --list
Show tag details:
git show v1.4.0
Best practice: use annotated tags (-a) for releases; lightweight tags are fine for local bookmarks.
13) Debugging with Git: Blame, Bisect, and Search
Git is a powerful investigation tool.
13.1 Find when a line changed: git blame
git blame path/to/file
For a specific range:
git blame -L 20,60 path/to/file
13.2 Search history: git log -S and -G
Search for commits that add/remove a string:
git log -S "deprecatedFunction" --oneline
Search by regex in diffs:
git log -G "TODO\(" --oneline
13.3 Find the commit that introduced a bug: git bisect
Start bisect:
git bisect start
git bisect bad
git bisect good v1.3.0
Git checks out a midpoint commit. You test and mark:
# run tests or reproduce bug
pytest -q
git bisect good # if bug not present
# or
git bisect bad # if bug present
When finished, Git identifies the first bad commit. Then reset:
git bisect reset
Best practice: automate the test step:
git bisect start
git bisect bad
git bisect good v1.3.0
git bisect run bash -c 'pytest -q'
git bisect reset
14) Remote Management: Multiple Remotes and Forks
Check remotes:
git remote -v
Add an upstream remote (common with forks):
git remote add upstream git@github.com:ORIGINAL_OWNER/REPO.git
git fetch upstream
Update your main from upstream:
git checkout main
git fetch upstream
git rebase upstream/main
git push origin main
15) Submodules vs Subtree (When You Must Vendor Repos)
15.1 Submodules (pointer to an external repo commit)
Add:
git submodule add https://github.com/example/lib.git external/lib
git commit -m "Add lib as submodule"
Clone with submodules:
git clone --recurse-submodules <repo-url>
Update:
git submodule update --init --recursive
Trade-off: submodules are explicit and reproducible but add workflow complexity.
15.2 Subtree (vendor code into your repo)
Subtree is often simpler for consumers but duplicates history/code.
Example:
git subtree add --prefix=external/lib https://github.com/example/lib.git main --squash
16) Hooks and Automation: Enforce Standards Locally
Git hooks run scripts at key moments (pre-commit, pre-push, etc.). Hooks live in .git/hooks/ by default and are not versioned unless you manage them.
Example pre-commit hook (quick lint):
cat > .git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
echo "Running formatting check..."
npm run format:check
EOF
chmod +x .git/hooks/pre-commit
Best practice: use a tool like pre-commit, Husky, or a shared scripts directory plus installation instructions so the team shares the same checks.
17) Best Practices Summary (What to Do Consistently)
17.1 Branching and syncing
- Keep feature branches short-lived.
- Rebase your feature branch onto
origin/mainbefore requesting review. - Use
git push --force-with-lease(not--force) after rewriting history.
17.2 Commit quality
- Prefer small, logical commits.
- Use
git add -pto avoid mixing unrelated changes. - Use fixup/autosquash during review iteration.
17.3 Conflict resilience
- Resolve conflicts deliberately and run tests after.
- Enable
rerereif your team repeatedly hits similar conflicts. - Learn to abort (
git rebase --abort) and recover (git reflog).
17.4 Shared history safety
- Use
git reverton shared branches for rollbacks. - Avoid rebasing public branches unless explicitly coordinated.
17.5 Debugging and traceability
- Use
git bisectto pinpoint regressions quickly. - Use tags for releases and important milestones.
18) Key Takeaways (The Intermediate “Toolkit”)
If you internalize only a few things, make them these:
- Rebase is for cleaning up feature branches; revert is for undoing on shared branches.
--force-with-leaseis the safe way to update a rebased remote branch.- Interactive rebase (
git rebase -i) is how you craft readable history. - Conflicts are manageable with a process: inspect → edit → stage → continue.
- Reflog is your recovery mechanism when you think you lost work.
- Git is a debugging tool: blame, log search, and bisect can save hours.
19) Practical “Cheat Sheet” Commands
# Inspect
git status
git log --oneline --decorate --graph --all
git show <ref>
git diff
git diff --staged
# Branching
git checkout -b feature/x
git fetch origin
git rebase origin/main
git merge origin/main
# History editing
git rebase -i origin/main
git commit --amend
git push --force-with-lease
# Conflicts
git mergetool
git add <file>
git rebase --continue
git rebase --abort
# Undo
git restore <file>
git restore --staged <file>
git revert <commit>
git reset --soft HEAD~1
git reflog
# Debug
git blame <file>
git log -S "text"
git bisect start
20) Suggested Practice Exercises (To Build Confidence)
- Create a feature branch, make 3 commits, then squash them with interactive rebase.
- Simulate a conflict by editing the same line in two branches; resolve it and continue rebase.
- Make a “bad commit,” push it to a test branch, then revert it.
- Use
git reflogto recover from agit reset --hard. - Run a mini
git bisectby intentionally introducing a failing test between two tags.
If you want, tell me your team’s current workflow (GitHub Flow, GitLab Flow, GitFlow, trunk-based) and whether you squash-merge or rebase-merge PRs, and I can tailor a recommended set of defaults and a step-by-step “daily routine” command sequence.