← Back to Tutorials

Intermediate Guide: Topic Overview, Best Practices & Key Takeaways

intermediateguidebest-practicesoverviewhow-to

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:

This guide targets the intermediate layer: you already know how to commit and push, but you want to:


2) Mental Model: Refs, Commits, and the Working Tree

Before best practices, align on how Git thinks:

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:

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)

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)

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:

Cons:

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:

Cons:

Rule of thumb:

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:

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:

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

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:

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:

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.


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

17.2 Commit quality

17.3 Conflict resilience

17.4 Shared history safety

17.5 Debugging and traceability


18) Key Takeaways (The Intermediate “Toolkit”)

If you internalize only a few things, make them these:

  1. Rebase is for cleaning up feature branches; revert is for undoing on shared branches.
  2. --force-with-lease is the safe way to update a rebased remote branch.
  3. Interactive rebase (git rebase -i) is how you craft readable history.
  4. Conflicts are manageable with a process: inspect → edit → stage → continue.
  5. Reflog is your recovery mechanism when you think you lost work.
  6. 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)

  1. Create a feature branch, make 3 commits, then squash them with interactive rebase.
  2. Simulate a conflict by editing the same line in two branches; resolve it and continue rebase.
  3. Make a “bad commit,” push it to a test branch, then revert it.
  4. Use git reflog to recover from a git reset --hard.
  5. Run a mini git bisect by 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.