Git Black Belt

Advanced usage

Topics

  • Identifying commits
  • Staging changes
  • Rewriting history
  • Merge strategies and conflict handling
  • Patches

Identifying Commits

  • Refs and symrefs
  • Relative commit names
  • Ranges
  • Refspecs
  • Bisect
  • Pickaxe

Refs

Tips of branches and tags are known as refs, names that refer to a SHA1 based object ID for an object in the git repository.

  • Refs are stored under the $GIT_DIR/refs directory (e.g. .git/refs/heads/master or in a packed format under $GIT_DIR/packed-refs)
  • Refs contain the 40 hex digit SHA1 directly or a symbolic ref to another ref (ref: refs/heads/master).

Symbolic refs

Git maintains a set of symrefs for particular purposes.

  • HEAD refers to the most recent commit on the current branch.
  • ORIG_HEAD records the previous version of HEAD during operations like merge or reset.
  • MERGE_HEAD points to the commit that is currently merged into HEAD

The rev-parse command can be used to unambiguously resolve a ref:

git rev-parse HEAD

Show the commit about to be merged into the current branch

git show ORIG_HEAD

Relative commit names

To refer to a commit relative to another commit, git supports a mechanism to refer to commits in the graph using the ~ or ^ symbols.

  • The caret ^ is used to refer to a specifc parent (there is more than one parent for merge commits) in the same generation
  • The tilde ~ is used to go back the ancestry chain (follows the first parent if there are multiple)

Example

git show HEAD^^ # refers to the first parent of the first parent

Same as

git show HEAD~2

Example

relative-names
relative-names

Translated

Can be used to traverse the commit graph without using absolute commit names.

relative-names-translated
relative-names-translated

Commit Ranges

Range: start..end is defined as the set of commits reachable from end that are not reachable from start. If either start or end are missing HEAD is assumed.

Commits on origin/master not yet on master:

git log master..origin/master

start..end is the same as using ^start end.

E.g. show everything that is on my-feature-branch not yet on master:

git log ^master my-feature-branch

Refspec

A refspec maps branch names in the local repository to branch names in a remote repository.

A refspec has the following format:

[+]<src>:<dst>

If the + is present the fast-forward check when updating refs is disabled.

  • push: <src> is the local ref being pushed, <dst> is the remote ref being updated
  • fetch: <src> is the remote ref being fetched, <dst> is the local ref being updated

Refspec f.

E.g. fetch:

+refs/heads/*:refs/remotes/REMOTENAME/*

All the branches from the remote are mapped to the refs/remotes/REMOTENAME namespace in the local repository.

Push the current HEAD to a branch named foo in the origin repository:

git push -n origin HEAD:refs/heads/foo

Attention:

git push -n origin :refs/heads/foo

Deletes the remote branch foo in origin!

Bisect

Execute a binary search to isolate a particular commit.

Prerequisite:

  • There is a known good and bad state and a commit that transitions the repository from one to the other
  • There is a way to determine whether a particular checked out state is either good or bad.

Bisect works by defining an initial good and bad state and then repeatedly asking the questions whether the current version is good or bad.

Bisect ff.

Bisect session:

# Start bisect session
git bisect start

# Define the good state
git bisect good stash-parent-2.0.3

# Define the bad state - e.g the current HEAD
git bisect bad
Bisecting: 1627 revisions left to test after this (roughly 11 steps)
[ce92f5fcea65142...0c180b30ae8122] Automatic merge from 2.1 -> master

# Mark the current state as either good or bad
git bisect bad
…

[4822] λ > git bisect good
769479f0cf8f242fb969e440c0b5776ba623b9eb is the first bad commit

# Wrap up the bisect session
git bisect reset

Bisect ff.

The bisect process will be captured and can be printed using the git bisect log command. If you want to start over or back track the git bisect replay command can be used to skip a number of initial steps:

# Capture the current state
git bisect log > current-state

# Start over
git bisect reset

# Edit the log file (remove a commit)
vim current-state

git bisect replay current-state

Pickaxe

git log -SNEEDLE searches though the history of file changes (i.e. diffs) that introduce or remove an instance of NEEDLE.

[4836] λ > git log --oneline -S"<activeobjects.version"
dbc67fc FECRU-2016: implement partial activeobjects support. \
    Currently no backup and restore

Staging changes

Git allows you to build up your next commit wholesale or in small, incremental steps.

Staging changes f.

git has an interactive staging mode.

λ > git add -i
       staged          unstaged path
1:    unchanged        +1/-0 git-blackbelt-session-2/index.html
2:    unchanged        +2/-0 git-blackbelt-session-2/slides.md

*** Commands ***
1: [s]tatus     2: [u]pdate    3: [r]evert    4: [a]dd untracked
5: [p]atch      6: [d]iff      7: [q]uit      8: [h]elp

Does anyone actually use that?

The good parts

Charles already showed that, worth repeating.

Choose text hunks to add to the index:

git add -p

Similar but loading the changes directly into the editor:

git add -e

Reverse

git reset -p allows reverting changes that are staged in the index on a hunk level.

git reset -p

To modify the working tree, use checkout instead of reset. E.g. select hunks to be applied to the working tree (in reverse) against the given <tree-ish>:

git checkout -p <tree-ish>

Revert the file spy.cabal to two revisions back

git checkout master~2 -- spy.cabal

Selectively discard edits not staged yet:

git checkout -p

Rewriting history

Why?

  • Split a large commit into logical, atomic commits. Combine small commits into larger ones.
  • Reorder commits into a more logical sequence.
  • Clean up commits, remove debug/development code before sharing it.
  • Edit before publish.

Word of caution: Feel free to alter history as long as no other developer has a copy of the history you are changing.

Amend commits

The "fix up the last commit" use case.

Amend the latest commit, use $EDITOR (or $GIT_EDITOR) to edit the commit message.

git commit --amend

Amend the latest commit using the given message.

git commit --amend -m "Message"

Amend the latest commit, reuse the original message.

git commit --amend --no-edit

Reset

reset resets the current HEAD to the specified state. We already covered reset -p, the --mixed (default) and --hard variants reset the current branch head to the given commit.

Drop the last commit but leave the working directory unchanged:

git reset HEAD~

Update the index and the working directory to match the given ref:

git reset --hard origin/master

Note: Any changes to tracked but not yet commited files will be lost!

Rebase

Rebase allows you to "forward port local commits to the updated head". This allows for a workflow that avoids criss-cross merges and is comparable to maintaing a set of patches on top of a branch.

git rebase origin/master

Update the current branch with changes from upstream by rebasing instead of merging:

git pull --rebase

Transplant a set of commits onto a different branch. E.g. if work on my-feature started on the master branch but it should have started on 2.2:

git rebase --onto 2.2 master my-feature

Interactive rebase

Interactive rebase allows for fine grained manipulation of local history.

git rebase -i @{u} # The upstream branch (the default since 1.7.6)

This will open up an editor (again $EDITOR or $GIT_EDITOR) with a list of commits and operations:

  0 pick a5e29a4 New commit Wed  6 Mar 2013 16:46:57 EST
  1 pick 45df508 New commit Wed  6 Mar 2013 16:46:57 EST
  2 pick 736b9fd New commit Wed  6 Mar 2013 16:46:57 EST
  3
  4 # Rebase cc1b1f3..736b9fd onto cc1b1f3
  5 #
  6 # Commands:
  7 #  p, pick = use commit
  8 #  r, reword = use commit, but edit the commit message
  9 #  e, edit = use commit, but stop for amending
 10 #  s, squash = use commit, but meld into previous commit
 11 #  f, fixup = like "squash", but discard this commit's log message
 12 #  x, exec = run command (the rest of the line) using shell

Reflog

Already mentioned in the basics section but again, worth repeating.

git reflog is the safety net for all the history rewriting operations covered on the previous slides. It is a records of changes to any ref (i.e. changes to the tips of branches).

λ > git reflog
0fc1242 HEAD@{0}: checkout: moving from my-feature to master
caba850 HEAD@{1}: rebase -i (finish): returning to refs/heads/my-feature
caba850 HEAD@{2}: commit (amend): Merge pull request #1206 from STASHDEV-3474

Operations that update the reflog include: Cloning, new commits, creating and changing branches, rebase and reset.

Merge strategies

There are several strategies that can be applied when merging one (or more) branch(es) into a target branch.

Degenerate Merges (no merge commit)

  • already up to date: All the commits from the source branch are already present in the target (e.g. merging the same branch twice)
  • fast forward: The target branch HEAD is already present in the source branch commit list. The target branch ref will simply be updated to point to the source branch tip.

Merge strategies ff.

Normal Merges (merge commit on the target branch)

  • resolve: Merge source into target by applying the changes from common ancestor to the source branch HEAD to the target branch ("three way merge").
  • recursive: Similar to resolve but handles the case where there are multiple merge bases (criss-cross merges). In this case a temporary merge of all the common merge bases will be created and used as the merge base for the final merge (will be applied recursively).
  • octopus: Can merge more than two branches. Implemented using recursive but aborts if there are any merge conflicts (equivalent to a series of normal three-way merges).

Merge strategies ff.

Speciality Merges

  • ours: This strategy merges any number of branches into a target branch but only uses the files from the target and discards all the incoming changes. This won't change the tree of the current HEAD but will create a commit that contains all the merge parents. Useful to signal that all the branches have been merged and that git doesn't need to merge the histories again.
  • subtree: When merging two branches, if the source corresponds to a subtree of the target the source will be adjusted to match the tree structure of the target (this applies to the common ancestor tree as well) before being merged.

Merging

To select one of the merge strategies use

git checkout target
git merge -s ours source

The recursive strategy accepts a number of options that can be passed via a -X option:

git merge -s recursive -Xours branch-with-conflicts
git merge -s recursive -Xtheirs branch-with-conflicts

Note: Confusingly there is an ours merge strategy and an ours option for the recursive strategies (favours our files in case of a conflict but applies the other changes).

Merge - conflict resolution

If there is a merge conflict, git provides a few handy tools to ease the pain of resolving the conflicts.

git status shows the conflicted files. The index stores a tuple of the base, ours and theirs version for each conflicted file.

git ls-files -u

diff or show can work with those versions:

git show :1:src/Foo/Bar.hs
git diff :1:src/Foo/Bar.hs :3:src/Foo/Bar.hs

Where

  • 1 is the base version (merge base/common ancestor)
  • 2 is "our" version (the version that is on the target branch)
  • 3 is "their" version (the version we are about to merge in)

Merge - conflict resolution f.

log can show you where the changes came from:

git log --merge --left-right -- src/Git/Common.hs # use -p to include the patches

Sometimes it's easier to use a visual mergetool:

git mergetool

Note: There are various mergetools git can use, I'm using DiffMerge here.

Start over?

git reset --hard ORIG_HEA

Patches

Need to transfer commits from one repository to the other without Stash or Bitbucket?

git format-patch together with git am has you covered:

Prepare a set of patches (e.g. last 3 commits):

λ > git format-patch -p HEAD~3
0001-Move-fromOctets-into-Common.patch
0002-Write-an-index-file-to-.git-index-that-matches-the-c.patch
0003-Fix-the-commit-parser-to-not-fail-on-root-commits-wi.patch

USB/E-Mail/Dropbox?

In the root of the target repository:

λ > git am --reject --whitespace=fix *.patch

(fix: fix outputs warnings for a few such errors, and applies the patch after fixing them)