To learn Git the best resource I could find was: Think-like-a-Git. Here I want to construct a short reference version of the article to look things up when specialized questions come up after reading the article.
Concepts
To see a complete visualization of the graph in your repository you can use the following command:
1 |
git log --oneline --abbrev-commit --branches=* --remotes=* --graph --decorate --color |
Consider to define a git alias in your ~/.gitconfig.
A Git commit consists of two things: (1) a pointer to the state of your code at some moment in time, and (2) zero or more pointers to “parent” commits. A Git commit is a node in a graph, and nodes can point to other nodes that came before them. A commit’s ID is a SHA-1 hash of several pieces of information: the contents of the commit, and the IDs of its parent commits. A commit is generated by first adding all relevant changes in the file-tree to the index (a virtual collection of changes) and then commiting this index to the history.
References are pointers to commits. References come in several flavors: local branch, remote branch, and tag. On disk, a local branch reference consists entirely of a file in your project’s .git/refs/heads directory. This file contains the 40-byte identifier of the commit that the reference points to. Local branch references are specific to a single repository: your local one. Commands that affect local branch references include commit, merge, rebase, and reset. Note, that e.g. merge and rebase do not however change the existing commits. Remote branch references are also specific to a single repository, but one that’s previously been defined as a remote. Commands that affect remote branch references include fetch and push. Tag references are basically like branch references that never move. As of this writing, I only know of one command that affects tags. As you might guess, it’s tag.
References make commits reachable. Commits that are not reachable are garbage collected from time to time or when git gc is issued. They are also not shown in git log. Until then you can still get back at them though by reflogs. Every reachable commit can be identified by its ID, by a reference (if it points to the commit) or by a relative reference from a reference that points to a following commit (e.g. HEAD^, HEAD^^, or HEAD~3).
Rewriting history
It is generally considered rude to rewrite history in public! But for anything local, these are the commands:
You can tack a new change on to the previous commit using git commit --amend. This keeps all of your related changes bundled together in one commit, instead of a history full of “bugfix…” commits. Because the commit ID also hashes the content of the commit, when you use git commit --amend, you’re actually building a completely different commit, and pointing your local branch reference to it instead. The first commit you made is still there on disk, and you can still get back to it, until garbage collection.
What git cherry-pick does, basically, is take a commit from somewhere else, and “play it back” wherever you are right now. Because this introduces the same change with a different parent, Git builds a new commit with a different ID.
git rebase is a shortcut that lets you pick up entire sections of a repository and move them somewhere else. git rebase foo bar is equivalent to
1 2 3 4 5 6 |
git checkout foo git checkout -b newbar git cherry-pick C D E # where C, D, E are the commits since the branch of foo and bar until the head of bar git checkout bar git reset --hard newbar git branch -d newbar |
Merge patterns:
Use the Scout pattern if you’re still unclear on exactly what git merge does, or if you think it’s likely that you’ll decide to back out of the merge:
- Make sure you’re on the right branch and that you have a clean working state. ( git status)
- Create a new branch (I often name it test_merge) and switch to it. ( git checkout -b test_merge)
- Do the merge. ( git merge spiffy_new_feature; to reset use git reset --hard
- Switch to your visualizer and predict how its view will change when you refresh it.
- Refresh your visualizer and see whether your prediction was correct.
- Are you happy with the result?
- If YES: Move your real branch forward to where the test_merge branch is. ( git checkout master, git merge test_merge)
- If NO: Delete the test_merge branch. ( git checkout master, git branch -D test_merge)
Use the Savepoint pattern if you’re pretty sure what you want to do, but just want to leave yourself an undo button in case things get too messy.
- Make sure you’re on the right branch and that you have a clean working state. ( git status)
- Create a new branch to use as a savepoint, but don’t switch to it. ( git branch savepoint)
- Do the merge. ( git merge spiffy_new_feature; to reset use git reset --hard
- Switch to your visualizer and predict how its view will change when you refresh it.
- Refresh your visualizer and see whether your prediction was correct.
- Are you happy with the result?
- If YES: Delete the savepoint. ( git branch -d savepoint)
- If NO: Reset your branch to the savepoint. ( git reset --hard savepoint)
The Black Belt: You can do the merge without the previous branching of the savepoint pattern (step 2) and afterwards cleaning it up (step 6/yes). If you want to reset you’d simply have to specify the very commit instead of the branch in step 6/no if anything goes wrong.
Detached Heads and the Reflog
This is taken from another source and I cite it word for word because I couldn’t have explained it any better:
The reflog can show you all the values that HEAD has taken in the past. Here’s a simulated example. I cloned a repo that had a master and a ‘foo’ branch. I then checked out origin/foo, ignored all the warnings, and made two commits on what I thought was the “foo” branch. Then I switched back to master and made two normal commits on master before I realised that I had ‘lost’ two commits.
At this point, running git reflog show gives me the following (the most recent HEAD value is first):
1 2 3 4 5 6 7 |
86a8ee0 HEAD@{0}: commit: my second commit on master e42c4d0 HEAD@{1}: commit: my first commit on master 58c1539 HEAD@{2}: checkout: moving from e2558a9d1527e5a76b39ffede1dc5ca9c650de01 to master e2558a9 HEAD@{3}: commit: second commit on foo 1c9dfa0 HEAD@{4}: commit: first commit on foo 802f184 HEAD@{5}: checkout: moving from master to origin/foo 58c1539 HEAD@{6}: clone: from /tmp/tmp.lv5IqjK0ZI/b |
That moving from <SHA>... is usually a sign that some commits may have been lost. In this case you can run git branch newbranch HEAD@{3} or git branch newbranch e2558a9 to save those commits. (Notice that the SHA value of HEAD@{3} is the same one mentioned in the moving from <SHA>... message on the line above it).
Commands explained
Generally speaking, when arguments come in a capital and a lower case version the upper case is the stronger of the two. Often the lower case version does some checking like “if it doesn’t exist”, while the upper case version simply overwrites. A default argument can be substituted with “–” if more positional arguments follow it.
command | explanation |
---|---|
git init | create a new .git folder for the current directory (create a new working repo) |
git init –bare | initialize current folder as bare repository, essentially a .git folder without a file-tree (create a new bare repo, cmp. What-is-a-bare-Repo) |
git clone | copy a repository and set the remote branches accordingly |
git add dir | add all changes in dir/* to the index |
git branch branchname | create branch reference to current commit |
git checkout branchname | set HEAD to branchname and update index and files (this means if branchname is HEAD (or "–") then will only undo changes not yet staged to the index) |
git checkout -b branchname | git branch branchname followed by git checkout branchname |
git reset (--mixed) commitid | set HEAD and current branchtag (if not detached) to commitid and reset the index (but keep the working tree) (for more cmp. SO) |
git reset --hard commitid | like git reset commitid but also reset all the files to the new commit (thus the index is empty afterwards) |
git show commitid | list all the contents of the given commit |