Git: from ten thousand feet
Here’s a bird’s eye view of some git topics.
These are things a seasoned git user should be aware of.
I’ll be your Captain for this flight.
The porcelain and the plumbing
Git is split into two kinds of commands: porcelain and plumbing.
Porcelain commands are the everyday ones you use like git commit. Porcelain commands use the plumbing commands under the hood.
If you are scripting things around git you might want to use the plumbing commands directly to avoid your scripts breaking in the future.
UI versus command line
You can use git from command line, or you can use a UI, or you can use both.
UIs are a close reflection of the things you’d do on the command line.
Note that not all git UIs are graphical: some are also command-line tools like LazyGit and GitUI.
I believe it’s good to have some experience of the git command line, even if you use a UI usually. (Why? Because you’ll be git-able in any environment, and also articles/docs about git will usually be talking about the git command line, not your particular UI.)
There are many different UIs/GUIs so this article focuses on the git command line.
Time things
Sometimes you want to rewrite history: tidy up messy commits before you share them, fix a typo in an old commit message, or fold several small commits into one.
Interactive rebase is the main tool for this. It opens an editor listing the commits you’re about to replay, and you choose what to do with each one:
git rebase -i HEAD~5 # edit the last 5 commits
In that editor you can:
reword— keep the change but edit the commit messagesquash— merge a commit into the previous one, combining their messagesfixup— like squash, but throw away the commit’s messagedrop— remove the commit entirelyedit— pause at that commit so you can amend the actual changes
There’s also some quick most-recent-commit fixes:
git commit --amend "A fixed commit message" # change the most recent commit (message and/or contents)
git commit --fixup <hash> # mark a commit to be folded into <hash> later
A word of warning: rewriting history that you’ve already pushed and shared means everyone else has to deal with the divergence (typically using git rebase or cherry-picking). Rewrite freely on personal branches; be very careful on shared ones.
Moving things
Sometimes you want to move commits from one place to another.
Cherry pick copies individual commits onto your current branch. Handy when a single fix lives on the wrong branch, or you want one commit out of a feature branch without merging the whole thing:
git cherry-pick <hash> # apply that commit here
git cherry-pick <hashA> <hashB> # apply several
Rebase moves a whole series of commits onto a new base. The classic use is bringing your feature branch up to date by replaying your work on top of the latest main, giving a tidy linear history instead of a merge commit:
git switch my-feature
git rebase main
Undeleting things
You accidentally moved a branch and lost sight of some commits. Where have they gone?
They’re still there, but without a branch or tag pointing at them, they’re invisible.
git reflog to the rescue. This allows us to see commits that are otherwise invisible.
Reflog is usually much nicer to use in GUIs; on the command line it looks like this:
> git reflog
8f23abc HEAD@{0}: commit: Fix authentication timeout
12ad901 HEAD@{1}: checkout: moving from feature/login to main
73bc112 HEAD@{2}: commit: Add login validation
12ad901 HEAD@{3}: checkout: moving from main to feature/login
...
> git checkout 8f23abc
Big things
Git isn’t happy storing large binary files — images, videos, datasets, models. Every version of them gets baked into history and bloats the repo forever.
Git LFS (Large File Storage) stores those files elsewhere and leaves lightweight pointers in the repo instead:
git lfs install
git lfs track "*.psd" # writes a rule into .gitattributes
git add .gitattributes
From then on, matching files are handled by LFS automatically when you commit and push.
Modularising things
Sometimes you want to pull another git repository into yours — a shared library, a theme, a vendored dependency — while keeping it as its own project with its own history.
Submodules do exactly that: a submodule is a link to another repo, pinned to a specific commit, living in a subdirectory of your own:
git submodule add https://example.com/lib.git vendor/lib
git commit -m "Add lib submodule"
Your repo records only the URL and the pinned commit (in a .gitmodules file), not the submodule’s files. So when someone clones your project they need to fetch the submodule contents too:
git clone --recurse-submodules https://example.com/app.git
# or, if you already cloned:
git submodule update --init --recursive
To move a submodule to a newer commit, go into it, check out what you want, then commit the new pointer in the parent:
cd vendor/lib
git pull origin main
cd ../..
git add vendor/lib
git commit -m "Bump lib submodule"
Submodules have a reputation for tripping people up — the pinned-commit model means it’s easy to forget to update or push the submodule, leaving collaborators with a dangling reference. They’re powerful, but worth understanding before you commit (pun intended) to them. Lighter-weight alternatives like git subtree, or a package manager, are often a better fit.
Parallel things
Normally a clone lets you have one branch checked out at a time, and switching branches shuffles your working directory around.
Worktrees let you have several branches checked out at once, each in its own directory, all backed by the same repository. Great for reviewing a PR while keeping your own work untouched, or running a long build on one branch while you carry on in another:
git worktree add ../hotfix main # check out main in a sibling directory
git worktree list
git worktree remove ../hotfix
Fixing things
A bug crept in somewhere over the last hundred commits and you don’t know which one introduced it.
Git bisect finds it with a binary search. You mark a known-good and known-bad commit, and git repeatedly checks out a commit halfway between them for you to test, narrowing down to the culprit in a handful of steps:
git bisect start
git bisect bad # current commit is broken
git bisect good <old-hash> # this old one was fine
# git checks out a midpoint; test it, then:
git bisect good # or: git bisect bad
# ...repeat until git names the first bad commit
git bisect reset # back to where you started
If you can script the test, you can even automate the whole hunt with git bisect run <cmd>.
Finding things
Git can search through history:
git grep searches the contents of your currently checkout:
git grep "TODO" # search the working tree
git grep "TODO" <hash> # search files as they were at <hash>
The pickaxe searches history for when a certain change happened:
git log -S "functionName" # commits that changed how many times the string appears
git log -G "regex" # commits whose diff matches the regex
And git log -- <path> shows the history of a single file, while git blame <file> shows which commit last touched each line.
Mass editing things
Occasionally you need to rewrite history wholesale: purge a file that should never have been committed (a secret, or a huge binary), change an email address across every commit, or split a subdirectory out into its own repo.
git filter-repo is the modern tool for this (it replaces the old, slow, and error-prone git filter-branch):
git filter-repo --path secrets.env --invert-paths # remove a file from all history
git filter-repo --mail-callback ... # rewrite author details
This rewrites every commit, so all the hashes change — it’s for repos you’re prepared to force-push and have collaborators re-clone.
Backing up things
A checkout of a git repo is usually not a full copy of everything in the host repo; there might be branches on the remote you don’t have a local copy of. So git clone itself isn’t a full backup of the repo.
For belt-and-braces backup have a look at git clone --mirror.
Research well before getting into any kind of git repo backup task/project. There are quite a lot of sites out there with incorrect information!