Yet Another Git Guide
Yet Another Git Guide
I use Git (and its companions like GitHub) a lot, and intend to use it a lot more. As I continue to learn, I’ll add to this document, written in the form of a tutorial to myself. It’s something I’d like to keep public so that I can point friends and collaborators to it.
I don’t aim or claim to be comprehensive, so I’ll only cover what’s relevant to how I personally use Git. I find the following resources to be useful:
- Git manual pages. These are nice if you know what you’re looking for, but hard
to browse because the manual is split up into pages for each subcommand. To
see these, run
man git-<subcommand>, or find the page online.
- Atlassian’s Advanced Tips for Git. These are lengthier compared to what I have here, with better illustrations, and distributed over several articles.
- Scott Chacon and Ben Straub’s Pro Git, especially the Git Internals chapter. Goes into a lot detail about the inner workings of Git.
- Katie Sylor-Miller’s Oh Shit, Git!?!. Quick and dirty (language) guide for common Git maladies.
- Roger Dudler’s git - the simple guide. Nice place to get started, with nice illustrations.
If you come across this and have any suggestions or corrections, please reach out.
Here is a summary of useful commands that you may use while using Git.
Check state of local repo:
To add stage a file (include it in the next commit):
git add <file>
To restore from index (also known as staging area):
git restore <file>
To unstage a file (restore from
git restore --staged <file>
To move a file (rename it in the next commit):
git mv <src> <dst> mv <src> <dst> && git add <src> <dst> # equivalent
To delete a file (remove it in the next commit):
git rm <file> rm <file> && git add <file> # equivalent
To make a commit:
git commit # opens up $EDITOR to compose commit message git commit -v # short for --verbose; show diff in $EDITOR git commit -m <commit-message> # short for --message; inline commit message
To amend last commit with state of current index:
git commit --amend # opens up $EDITOR to amend commit message
--amend undoes the last commit. If that commit has already been
pushed elsewhere, this will create a divergence in commit history, which will
require merges, rebases, or force pushes to fix.
To abort an in-progress commit in the interactive editor, just delete leave the commit message blank.
To undo the last commit:
git reset --soft HEAD^ # shorthand for "most recent commit"
To see the commit history:
git log git log -- <path> # only show commits related to <path> git log --graph # show commit history as dag
To inspect working tree at a commit:
git checkout <hash>
HEAD to a commit:
git reset <hash> # same as --mixed git reset --soft <hash> # do not touch working tree or index git reset --mixed <hash> # restore index but not working tree; default git reset --hard <hash> # restore working tree and index
To see all local branches (
* next to current branch):
git branch git branch -a # short for --all; also shows remote branches git branch -vv # short for --verbose; also shows HEAD commit and upstream
To check out an existing branch:
git switch <branch-name> git checkout <branch-name> # also works, overloaded legacy command name
Create and switch to a new branch:
git switch -c <branch-name> git checkout -b <branch-name> # also works, overloaded legacy command name
Delete a branch:
git branch -d <branch-name> # delete branch only if fully merged into upstream git branch -D <branch-name> # delete branch forcibly
git branch -u <upstream> # short for --set-upstream-to=<upstream>
git remote # show remote names only git remote -v # show remote names and URL
git remote add <remote-name> <remote-url>
git remote rename <old> <new>
Set remote URL:
git remote set-url <remote-name> <remote-url>
To push to current branch’s upstream:
git push # fails if unable to fast-forward upstream git push -f # short for --force; forcibly updates upstream to mirror local
To push to any remote, any branch (note the lack of
git push <remote> <branch>
To download commits/refs from remote (but not do anything to any local branches):
git fetch <remote> git fetch <remote> <branch>
To merge commits into current branch:
git merge <from-local-branch> git merge <from-remote>/<branch> # should git fetch first
Fetch and merge (“pull”) commits:
git pull # from upstream remote branch git pull <remote> <branch> # from specified remote branch git fetch <remote> && git merge <remote>/<branch> # equivalent to above
Note that the above commands may create merge commits if the history between local and remote branch have diverged. The below commands avoid this:
git pull --ff-only # fail if diverged git pull --rebase # rebase automatically if diverged git pull --rebase=interactive # rebase interactively if diverged
Rebasing commit history:
git rebase # rebase current branch onto upstream branch git rebase --onto <other-branch> # rebase current branch with <other-branch> as new base
Note that rebasing does not accommodate merges (without special flags).
To amend commit history during rebase (edit, reorder, squash, etc.):
git rebase -i # short for --interactive; specify what to do for each commit
Note that any kind of rebase (via
git rebase or via
git pull --rebase)
may rewrite history. Like
git commit --amend, if your history prior to rebase
had already been published to a remote, you will need to merge, rebase, and/or
force push in order to align the diverged histories.
Commits and commit histories
Commits point to a snapshot of what a repo looks at a certain point in time, and each commit has zero or more parent commits. Commits with zero parents are root commits, while commits with two or more commits are called merge commits.
The commit history of a repo forms a directed, acyclic graph of snapshots of the repo. To visualize this DAG, run:
git log --graph
Note that all commits in this graph are reachable by traversing the parent commits from the latest commit. When all commits only have a single parent, we say that the commit history is linear. This is desirable because it totally orders all the commits, making it as if all modifications to the repo were performed sequentially.
It is often useful to work with several commit histories concurrently. In Git,
you do this with branches. A branch is just a convenient way of
referring to a particular commit, called the
HEAD commit. Whenever you make
a commit, you update the
HEAD commit to point to the new latest commit.
You can see all the branches available in your repo with the command:
There will be a
* next to the branch you are currently on. We say this branch
is “checked out”. By default, you should be checked out on the
(previously, this was named
master, but that is being phased out).
To check out another existing branch, run:
git checkout <branch-name>
This will restore your working directory to the snapshot captured by the
commit of the branch you are switching to. Note that this may fail if you have
uncommitted changes on your current branch that would be overwritten by checking
out the other branch, so you should always make sure to do that before checking
out a new branch.
Merging vs rebasing
When different commits are made to different branches, their commit histories are said to have diverged. Even if they don’t conflict (e.g., because they only make changes to different files), it still isn’t obvious how best to combine these histories.
For instance, consider two branches,
topic, with a shared history
starting at commit
topic builds commits
G off of
main has since moved on and added commits
D. This leads to the
E---F---G topic / A---B---C---D main
Note that from the perspective of
D just don’t exist; if you
git log on
topic, you will only see the following:
The easiest resolution here is to create a merge commit
M to join together
the two histories:
E---F---G---M topic / / A---B------C----D main
If there are any conflicts between
C-D, this merge commit is also
an opportunity to mark how these conflicts are resolved (recall that it is just
a snapshot of the working tree state).
Now, assume that after committing
topic, we are finally finished
working on it, and want to merge it back into
main has not
accummulated any subsequent commits since
D, then all we need to do is just
HEAD to that of
E---F---G---M---H main / / A---B------C----D
Note that this leaves behind a non-linear history that can quickly accumulate
into a sprawl as
main accummulates merge commits from other branches.
An alternative to merging is to rebase
topic’s commits onto
Consider the commit graph prior to the merge:
E---F---G topic / A---B---C---D main
Instead of reusing commits
G, we can replay those changes on
D, and create alternative commits
E'--F'--G' topic / A---B---C---D main
G' are distinct commits from
because the contents of their working tree are now based on changes introduced
G still exist in the sense that they weren’t
actually rewritten here, but are no longer accessible from the
HEAD now points to
E---F---G (abandoned) / / E'--F'--G' topic / / A---B---C---D main
Yet we still say that we are rewriting history because from the perspective of
topic, it does appear that we’ve introduced entirely new commits to replace
the old ones. And while we’re rewriting history anyway, we can introduce
additional changes as we’re creating these new commits.
We typically access these additional features by performing an interactive rebase:
git rebase -i # short for --interactive
This opens up your editor with the list of the commits the rebase is going to perform, and allows you to edit that list to instruct Git to do something different:
pick E Commit message of E pick F Commit message of F pick G Commit message of G
For instance, you can change the commit message at any point, or
pause the rebase to edit some files before resuming, etc. For instance, to
E last, after
pick F Commit message of F pick G Commit message of G pick E Commit message of E
Another thing you can do is combine commits, otherwise known as squashing
commits. This accumulates their changes into a single commit. For instance, to
G together, before performing
pick F Commit message of F squash G Commit message of G pick E Commit message of E
If you have a messy or outdated commit history, you can use interactive rebasing to tidy it up locally and bring it up to date, before pushing or merging it elsewhere.
However, be mindful that this does rewrite history, and so if your pre-rebase
commits were already published elsewhere, they will likely result in conflicts.
For instance, if you had already pushed
G upstream, you will be
unable to push your rebased branch without first pulling to merge those commits
back in, defeating the purpose of rebasing in the first place. In this case, the solution is to force push:
git push -f # short for --force
This will forcibly overwrite the upstream
HEAD to your current
those commits upstream contain any data not in your local commit history, they
will be lost, so force pushing is usually done as a last resort.