Skip to main content

๐Ÿงฐ Advanced Git

These are the tools you'll reach for after a few weeks of everyday Git: parking work, tidying commits before a pull request, finding the commit that broke something, and rescuing work you thought was gone. Each topic is short and practical. Read the one you need, when you need it.

โš ๏ธ Read this first: history-rewriting commands

Some commands on this page (commit --amend, reset, rebase, push --force-with-lease) rewrite history: they replace commits with new ones. That's safe on commits only you have. It causes real trouble on commits other people have already pulled.

  • The golden rule: only rewrite commits that haven't been pushed, or that live on a branch nobody else uses.
  • Make a bookmark first: git branch backup-before-rebase costs nothing and gives you a one-command way back.
  • Commit or stash before you start, so there's no uncommitted work in the way.
  • If it goes wrong: git rebase --abort / git merge --abort / git cherry-pick --abort cancel an operation in progress, and git reflog finds almost anything afterwards.
On This Page

๐Ÿ“ฆ Stash: Park Unfinished Work

You're halfway through a change when something urgent comes up on another branch. Git won't always let you switch with uncommitted changes, and a half-finished commit isn't ideal either. git stash puts your uncommitted changes on a shelf and gives you a clean working folder; git stash pop puts them back.

git stash push -m "half-done footer"   # shelve tracked changes
git stash push -u -m "with new files"  # -u also shelves untracked (new) files
git stash list                         # stash@{0}: On main: half-done footer
git stash pop                          # re-apply the newest stash and remove it
git stash apply stash@{1}              # re-apply a specific one, keep it on the shelf
git stash drop stash@{1}               # delete one you no longer need

Stashes aren't tied to a branch, so you can stash on one branch and pop on another. That's a handy way to move uncommitted work you started on the wrong branch. If popping causes a conflict, resolve it like a merge conflict; the stash is kept until you drop it.

๐Ÿ’ก Keep the shelf short

Stashes are easy to forget. If work will sit for more than a day, a commit on its own branch (git switch -c wip-footer) is safer and easier to find.

โช Reset: Soft, Mixed, Hard

git reset <commit> moves your current branch back to an earlier commit. The mode decides what happens to the changes from the commits you "un-did":

CommandBranch moves back?Staging areaYour filesTypical use
git reset --soft HEAD~1YesChanges stay stagedUnchangedRedo the last commit (different message, split it)
git reset HEAD~1 (mixed, the default)YesEmptiedUnchanged; changes show as modifiedUn-commit and re-stage more carefully
git reset --hard HEAD~1YesEmptiedChanges deletedThrow the last commit away completely

HEAD~1 means "one commit before the current one"; HEAD~3 means three back. You can also give a hash from git log --oneline. VS Code's Undo Last Commit is git reset --soft HEAD~1.

โš ๏ธ Safety

Reset rewrites history. Only reset commits you haven't pushed. For pushed commits, use git revert, which adds an undo commit instead. --hard also deletes uncommitted changes in your files, and those are not in the reflog, so run git status first and stash anything you care about. Committed work that --hard moved past can still be recovered with reflog.

Moving a commit you made on main by mistake (not pushed yet): create a branch where you are, then move main back. The commit lives on in the new branch.

git branch feature-idea      # bookmark the commit on a new branch
git reset --hard HEAD~1      # move main back one commit (working folder must be clean)
git switch feature-idea      # carry on with the work here

โœ‚๏ธ Interactive Rebase: Tidy Your Commits

Before opening a pull request you might have commits like "Add gallery", "fix typo", "oops", "really fix it". Interactive rebase lets you rewrite that series into one or two clean commits: combine, reorder, reword, or drop them.

git rebase -i HEAD~4    # edit the last 4 commits
git rebase -i main      # edit every commit on this branch since it left main

Git opens your editor (VS Code, if you set core.editor) with a to-do list, oldest commit first. Change the word at the start of each line, then save and close the tab:

pick 5bcf918 Add gallery page
fixup cf4e8cd fix typo
fixup f120041 oops
reword 3e7a0b2 Add captions

# Commands:
# p, pick   = use commit
# r, reword = use commit, but edit the commit message
# s, squash = use commit, but meld into previous commit
# f, fixup  = like "squash" but keep only the previous commit's message
# d, drop   = remove commit
# ...

That example turns four commits into two: "Add gallery page" (with the typo fixes folded in) and a reworded "Add captions" commit. If a step conflicts, Git pauses: fix the file, git add it, and run git rebase --continue. git rebase --abort puts everything back exactly as it was.

Shortcut: when you know a new commit is a fix for an earlier one, commit it with git commit --fixup <hash>. Later, git rebase -i --autosquash main lines the fixups up for you.

โš ๏ธ Safety

Every rebased commit gets a new hash, so this rewrites history. Do it on your own branch before others build on it. If the branch is already pushed (for example, an open PR that only you work on), you'll need --force-with-lease to update it. Never rebase main or any branch teammates have pulled.

๐Ÿ“ Rebasing onto main (instead of merging)

When main moves ahead while you're on a branch, you can bring it in with git merge main (what the class used, always safe) or replay your branch's commits on top of the new main with git rebase main. Rebasing gives a straight-line history with no merge commit, as if you'd started your branch today.

flowchart LR subgraph Before["Before: branch started from B"] direction LR A1((A)) --> B1((B)) --> C1((C main)) B1 --> D1((D)) --> E1((E feature)) end subgraph After["After: git rebase main"] direction LR A2((A)) --> B2((B)) --> C2((C main)) --> D2(("Dโ€ฒ")) --> E2(("Eโ€ฒ feature")) end Before ~~~ After
git switch main
git pull
git switch feature
git rebase main
# on conflict: fix the file, then
git add <file>
git rebase --continue
# or give up and go back:
git rebase --abort

Dโ€ฒ and Eโ€ฒ contain the same changes as D and E but are new commits with new hashes. That's why the golden rule applies. Many teams simply use merge and let GitHub's Squash and merge or Rebase and merge button tidy things at the end, which is a perfectly good choice.

โš ๏ธ Safety

Only rebase a branch that nobody else has pulled. If you've already pushed it, the next push will be rejected and needs --force-with-lease, which is fine on your own PR branch and harmful on a shared one.

๐Ÿ’ช Force-Pushing Safely

After you amend or rebase commits you've already pushed, your branch and GitHub's copy have different histories, and a normal push is rejected with (non-fast-forward). A force push tells GitHub "replace your version of this branch with mine".

git push --force-with-lease

Always prefer --force-with-lease over --force. It only overwrites the remote branch if it's still where you last saw it. If someone pushed a commit you haven't fetched, it refuses instead of silently deleting their work. (Running git fetch or using an editor that auto-fetches updates "where you last saw it", which weakens that protection. Glance at git log origin/<branch> before forcing.)

๐Ÿšซ Never force-push main (or any shared branch)

Everyone who already pulled would have history that no longer matches, and commits can be lost. If a mistake is already on main, fix it forward with git revert. Turning on "Block force pushes" in a branch ruleset (see Team Workflows) makes this impossible to do by accident.

๐Ÿ’ Cherry-Pick: Copy One Commit

git cherry-pick copies a single commit from anywhere in the repository onto your current branch. It's useful when one fix on an experimental branch is needed on main right now, without merging the whole experiment.

git switch main
git cherry-pick 3e7a0b2        # copy that commit here as a new commit
git cherry-pick -x 3e7a0b2     # same, and note the original hash in the message
# on conflict: fix, git add, then
git cherry-pick --continue     # or: git cherry-pick --abort

The copy is a new commit with a new hash; the original stays where it was. Cherry-picking doesn't rewrite existing history, but overusing it creates duplicate commits that can conflict later when the branches are eventually merged. For "I need all of that branch", merge instead.

๐Ÿ›Ÿ Reflog & Recovery

Git keeps a private log, the reflog, of every position HEAD has been in: every commit, switch, merge, reset, and rebase, for about 90 days by default. A commit that no branch points to any more isn't gone; it's just unlabelled, and the reflog lists it.

git reflog
f996ad6 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
c0ea865 HEAD@{1}: commit: Add testimonials section
f996ad6 (HEAD -> main) HEAD@{2}: commit: Add contact page

Once you've spotted the commit you want, put a branch on it so it's safe, then inspect it:

git branch rescued c0ea865       # recover a deleted branch / lost commits
git log --oneline rescued
git reset --hard HEAD@{1}        # OR: undo a reset/rebase on the current branch (see โš ๏ธ)

Dropped a stash by mistake? Stashes aren't in the normal reflog once dropped. git fsck --lost-found lists "dangling commit" hashes; git show <hash> each one until you find your work, then git stash apply <hash>. The message printed when you dropped it also includes the hash.

โš ๏ธ Safety

The reflog is local. It's only on your computer and isn't pushed or cloned. It only records commits, so changes you never committed can't be recovered this way. And git reset --hard HEAD@{1} discards uncommitted changes like any hard reset; creating a branch (git branch rescued โ€ฆ) is the risk-free first step.

๐Ÿ”Ž Bisect: Find the Commit That Broke It

Something worked last week and doesn't now, with 30 commits in between. git bisect does a binary search: it checks out the commit halfway between "good" and "bad", you test it, and it halves the range again. Thirty commits take about five tests. It only checks out old commits to look at; it doesn't rewrite anything.

git bisect start
git bisect bad                  # the current commit is broken
git bisect good a1b2c3d         # this older commit was fine
# Git checks out a middle commit: test it (e.g. refresh Live Server), then
git bisect good                 # or: git bisect bad
# ...repeat until Git prints "<hash> is the first bad commit"
git bisect reset                # ALWAYS finish here to return to your branch

If a commit can't be tested (the page doesn't load for an unrelated reason), use git bisect skip. If you have a script that exits with 0 for good and non-zero for bad, git bisect run <command> does the whole search automatically. Once you've found the culprit, fix it with git revert <hash> or a new commit. Practice Project 7 is a guided bug hunt.

๐Ÿท๏ธ Tags & Releases

A tag is a permanent, human-friendly name for one commit, usually a version number. Unlike a branch, it never moves. Annotated tags (with -a) store who tagged it, when, and a message, and are what you want for releases.

git tag -a v1.0 -m "Site after launch"        # tag the current commit
git tag -a v0.9 -m "Beta" 5bcf918             # tag an older commit
git tag                                       # list tags
git show v1.0                                 # see the tag and its commit
git push origin v1.0                          # tags are NOT pushed by git push
git push origin --tags                        # push all tags at once

On GitHub, open Releases โ†’ Draft a new release, pick a tag, and write release notes. Visitors can download a zip of the site at exactly that version. Many projects use semantic versioning (MAJOR.MINOR.PATCH).

๐Ÿ’ก Deleting a tag

git tag -d v1.0 deletes it locally and git push origin --delete v1.0 on GitHub. Avoid moving or reusing a tag others may already have fetched; create v1.0.1 instead.

๐Ÿ•ต๏ธ Searching History

"When did this line change, and why?" Git can answer that quickly:

git blame about.html                   # last commit + author for every line
git blame -L 10,20 about.html          # only lines 10โ€“20
git log -S "newsletter" --oneline      # commits that added or removed that text
git log -G "color: *red" --oneline     # commits whose changes match a pattern
git log --grep="gallery" --oneline     # commits whose MESSAGE mentions gallery
git log --author="Ray" --since="2 weeks ago" --oneline
git log -p -- css/style.css            # full diff history of one file

blame points at the last change to a line. If that was just a reformat, run git blame <hash>~1 -- <file> to look further back, or use VS Code's Timeline view. Despite the name, blame is for finding context, not culprits. The commit message usually explains the "why".

๐Ÿงน Clean: Remove Untracked Files

git clean deletes files Git isn't tracking: build output, stray downloads, test files. It's the only everyday Git command that deletes files Git has no copy of, so it has a mandatory dry run habit:

git clean -n      # DRY RUN: list what would be deleted
git clean -nd     # dry run including untracked folders
git clean -f      # actually delete untracked files
git clean -fd     # ...and untracked folders
git clean -i      # interactive: choose what to delete

โš ๏ธ Safety

Files removed by git clean don't go to the Recycle Bin/Trash, and Git can't bring them back because it never had them. Always run -n first and read the list. Ignored files (from .gitignore) are left alone unless you add -x, which would also delete things like your .env file.

๐Ÿช Hooks: Scripts That Run Automatically

Hooks are small scripts Git runs at certain moments: before a commit is created (pre-commit), after a message is written (commit-msg), before a push (pre-push), and more. If a hook exits with an error, Git stops the action. They're great for catching mistakes, such as a leftover conflict marker, before they're committed.

Hooks live in .git/hooks/ (which has .sample examples). To try one, create a file named exactly pre-commit (no extension) in that folder:

#!/bin/sh
# Refuse to commit leftover conflict markers or trailing whitespace
exec git diff --cached --check

On macOS and Linux, make it executable with chmod +x .git/hooks/pre-commit. Git for Windows runs hooks with its own bundled shell, so the same script works there. Now a staged file containing <<<<<<< produces leftover conflict marker and the commit is refused.

The .git folder isn't pushed, so hooks aren't shared automatically. Teams commit them to a normal folder and each person runs git config core.hooksPath .githooks; JavaScript projects often use tools like Husky or lefthook to set this up. Anyone can bypass client-side hooks with git commit --no-verify, so for rules that must hold, use GitHub Actions and branch rulesets as well (see Team Workflows).

๐ŸŒณ Worktrees: Two Branches Open at Once

Normally one folder shows one branch at a time. A worktree is an extra folder linked to the same repository, with a different branch checked out, so you can fix something on main in one VS Code window while a half-finished feature stays open in another, with no stashing.

git worktree add ../my-website-hotfix -b hotfix-footer   # new folder + new branch
git worktree add ../my-website-review feature/gallery     # existing branch
git worktree list
git worktree remove ../my-website-hotfix                  # when you're done

All worktrees share one history, so a commit in one is instantly visible (as a branch) in the others. The same branch can't be checked out in two worktrees at once. Remove worktrees with git worktree remove rather than just deleting the folder; if you already deleted it, git worktree prune tidies up.

๐Ÿงฉ Submodules: A Repository Inside a Repository

A submodule embeds another Git repository in a folder of yours, pinned to one specific commit. Example: a shared theme or component library used by several sites. Your repository records only which commit of the other one to use.

git submodule add https://github.com/example/shared-theme.git theme
git commit -m "Add shared theme as a submodule"

# cloning a repo that has submodules:
git clone --recurse-submodules <url>
# ...or, if you already cloned without it:
git submodule update --init --recursive

# moving the submodule to the theme's latest commit:
git submodule update --remote theme
git commit -am "Update shared theme"

Submodules are powerful but easy to trip over. Teammates must remember to update them, and the folder is often left on a detached commit. For a small website, copying the files in, or a package manager like npm, is usually simpler. If you use them, check that your host builds them; Netlify does clone public submodules.

๐Ÿš€ Large Repositories

Git is fast for text files, even very old projects. Slowness usually comes from huge histories or large binary files (videos, design files, big images). Some tools that help:

git maintenance start                     # schedule background optimization for this repo
git gc                                    # tidy and compress the repository now
git clone --filter=blob:none <url>        # "partial clone": fetch old file contents only when needed
git clone --depth 1 <url>                 # "shallow clone": only the latest commit (no history)
git sparse-checkout set css images/icons  # only check out some folders of a huge repo

For large binaries, Git LFS (Large File Storage) keeps just a small pointer in Git and stores the real file separately: git lfs install, then git lfs track "*.psd", then commit the generated .gitattributes. GitHub rejects files over 100 MB in normal Git and warns above 50 MB. For a website, the best fix is usually simpler: compress images and host video on a video platform.

๐ŸŒฑ You don't need all of this at once

Most working developers use stash, amend, and reflog weekly, interactive rebase and cherry-pick now and then, and bisect a few times a year when it saves an afternoon. Everything from your one-day class (status, add, commit, branch, merge, push, pull, pull requests) is still the foundation of every one of these. Come back to this page when a problem calls for one, try it on a practice repo first, and you'll learn each tool at exactly the moment it's useful.