Skip to content

git workflow for largish teams

Mart Sõmermaa edited this page Dec 30, 2017 · 92 revisions
There should be one -- and preferably only one -- obvious way to do it.
                                       The Zen of Python, by Tim Peters

Based on Scott Chacon's Pro Git book and Vincent Driessen's blog post.

Overview

There is a central code repository that contains two main branches with infinite lifetime:

  • master
  • devel

master is the stable branch. The current state of master is always production-ready. Production environment runs on the master branch.

devel is the integration branch with the latest delivered development changes for the next release. Staging environment and automated tests run on devel.

When source code in the devel branch reaches a stable point and is ready to be released, all of the changes are merged into master and then tagged with a release number. When changes are merged into master, this is a new production release by definition.

Task branches

Development does not happen on devel directly. Instead task branches (also known as feature or topic branches) are used to develop new features for or fix defects in the upcoming release. Task branches keep task-related changes self-contained and facilitate reviewing.

Task branches branch off from and track devel. They are shared if multiple developers work on the same task, otherwise local.

devel changes are regularly merged into task branches, either by rebase if the merge is trivial (a fast-forward that does not introduce conflicts) or by real merge otherwise.

When a task is finished, it is first reviewed and then merged back into devel. The merge is always explicitly recorded (i.e. --no-ff is used).

Hotfix branches

When a defect that must be repaired immediately is discovered in the production release, a hotfix branch is directly branched off from master.

When the fix is finished, the hotfix branch needs to be merged back both into master and devel. The merge to master is always explicitly recorded (i.e. --no-ff is used). However, devel can be either rebased on or merge the changes from master (if the rebase does not change history and break developers' cloned repositories) or the hotfix branch can be explicitly merged.

Hotfix branches are usually local.

Release branches

Release branches are branched from devel after it is deemed suitable for production and the team has done a final review of the prospective changes. The branch contains finishing touches related to release management: incrementing the version number, packaging and documentation updates etc.

As with hotfix branches above, when the release branch is finished, it needs to be merged back both into master and devel. Similar considerations apply.

After the merge, the version is tagged on master. See the Semantic Versioning Specification for the recommended tagging format.

Reference

This section contains command reference for working with git according to the principles outlined above.

Additionally, you should explore using git stash on your own.

Recommended git settings

# Global settings

# essential sane defaults
git config --global push.default simple
git config --global pull.ff only

# conveniencies
git config --global alias.hist 'log --graph --date=short --pretty=format:"%h %ad | %s%d [%an]"'
git config --global alias.lg "log --graph --abbrev-commit --color --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'"
git config --global alias.undo-commit 'reset --soft HEAD^'

# change comment character to use # for referring to issues
git config --global core.commentchar ';'

# less essential
git config --global color.ui true
git config --global alias.deleteless-diff 'diff -M --diff-filter=ACMRTUXB'
# git config --global core.whitespace "trailing-space,space-before-tab" # enabled by default
git config --global apply.whitespace "fix"
git config --global merge.tool meld # or kdiff3
git config --global mergetool.prompt false
# git config --global mergetool.keepBackup false # does not preserve .orig files with conflict markers

# less relevant
# git config --global branch.autosetupmerge true
# git config --global branch.autosetuprebase always # use rebase when pulling
# repo-specific settings
# git config branch.devel.mergeoptions '--no-ff'
# git config branch.master.mergeoptions '--no-ff'

Additionally, consistent CRLF handling is essential, but depends on the project conventions.

Windows-specific settings

If the project is Windows-only or you need to adapt to CRLF-land, use CRLF throughout:

git config core.autocrlf false
git config core.safecrlf true # or 'warn'
git config core.whitespace cr-at-eol

Or, even better, use a .gitattributes file in the root of the repository, see the recommendation from Daniel Jomphe at StackOverflow.

Avoid problems with file name cases:

git config core.ignorecase true

Use either TortoiseMerge, WinMerge, KDiff3 or P4Merge. P4Merge can be downloaded from Perforce Downloads. All these tools need a wrapper script that does argument substitution for /dev/null. Paste this to a Git Bash shell to automatically create that script:

export TOOL=kdiff3
export TOOL_EXE='/c/Program\ Files/KDiff3/kdiff3.exe'

mkdir -p ~/bin

cat <<EOF > ~/bin/${TOOL}tool.sh
#!/bin/bash
NULL="\$TMP/${TOOL}.NULL" && touch "\$NULL"

ARGS=()
for arg in "\$@"; do { [[ "\$arg" == "/dev/null" ]] && arg="\$NULL"; ARGS+=("\$arg"); } done

${TOOL_EXE} "\${ARGS[@]}"

rm -f "\$NULL"
EOF

If you have a recent Git, then configuring the chosen tool to be the default for git difftool and git mergetool is quite straightforward:

git config --global merge.tool ${TOOL}
git config --global mergetool.${TOOL}.path ${TOOL}tool.sh # assume ~/bin is in PATH

Configuring WinMerge is more involved, so that is not covered here.

Setting up

git clone git@host:project.git
cd project
... apply settings as above ...
git checkout --track origin/devel

Everyday work on a task branch

git checkout -b task-1 devel
... work ...
git diff # review
git add foo
git commit -m "Changed foo."
# or use `git commit -am` to autocommit all changed files
git push --set-upstream origin task-1 # share with others
... others change task-1 and push ...
git pull --rebase
... devel has changed upstream ...
git checkout devel
git pull --rebase
# if rebase fails, use git rebase --abort and git merge origin/devel
git checkout task-1
git rebase devel # NB! use merge if non-fast-forward or if it is important to mark that merge has occurred

When the task is done and has been reviewed, merge it to devel and delete it if it is no longer needed

git diff devel # review while on task-1
git checkout devel
git merge --no-ff task-1 # always generate a merge commit

git branch -d task-1
git push origin :task-1

Branching

Create a local branch task-1 and share it (by creating a remote branch)

git checkout -b task-1 devel
git push --set-upstream origin task-1

Create a local branch task-2 that tracks an existing remote branch

git fetch origin # refresh branch list
git checkout --track origin/task-2
# or just `git checkout task-2` in git since version 1.7.x

Create a local branch task-3 from uncommitted changes in the checkout

git stash
git stash branch task-3

Clean up the list of remote branches (retain only the ones that still exist)

git remote prune origin

Pull changes from the central repository into the current branch (note that --rebase is used to avoid polluting the timeline with unnecessary micro-merges)

git pull --rebase # optionally with remote and branchname, e.g. origin devel

Delete the shared branch task-1

git checkout devel
git branch -d task-1
git push origin :task-1

Diffs and history

Familiarize yourself with ancestry references

  • see the Pro Git book
  • HEAD~2 and HEAD^^ both refer to the second ancestor of current head.

Use log or hist for examining history.

Use show to see the contents of a commit.

Use git difftool -d instead of plain diff when doing larger code reviews. Graphical tools are helpful. -d opens the full diff in one window instead of popping up new difftool windows for each file.

Use git diff devel for ordinary task branch diffs (while on the task branch).

Changes in the topic branch only if devel has unmerged changes

git diff devel...
# equivalent to `git diff devel...HEAD`

Changes in local branch compared with remote branch

git diff @{u}...

Changes in devel since the topic branch task-1 was started off it

git diff task-1...devel

Detect renames and don't show deleted files

git deleteless-diff # alias for -M --diff-filter=ACMRTUXB, also explore --find-copies-harder -B -C
# Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R), type changed (T),
# Unmerged (U), Unknown (X), pairing Broken (B)

Show a semantic tree state identifier, useful for labeling (like svnversion, only better)

git describe # also --all --dirty --long --abbrev=8

Merging and rebasing

Use rebase only when it does not break other developers' history.

When merging to devel or master, use --no-ff

git checkout devel
git merge --no-ff task-1 # always generate a merge commit

If you know that the merge will be non-trivial, merge without committing and review the diff before the final commit

git merge --no-commit --no-ff task-1
# review changes now, amend as necessary
git commit

Undo a merge (works for rebase as well, but better use --abort as below)

git reset --hard ORIG_HEAD # or HEAD@{1}

If a rebase does not just work, it's easier to abort and do a merge instead

git rebase devel
# conflicts occur
git rebase --abort
git merge devel

When a merge conflict occurs and you want to use the upstream version of the file

git checkout --theirs filename # or --ours for the version in current branch
git add filename

Graphical merge tools, especially meld, make merge conflict resolution a pleasant experience

sudo apt-get install meld # or k3diff
git mergetool

Committing and amending

Manually pick what will be committed

git add --patch

Abandon changes that have not been committed

git reset --hard # add origin/branchname to abandon changes in local branch

Interactively pick changes that will be abandoned

git checkout --patch

Unstage changes that have not been committed

git reset

Interactively pick changes that will be unstaged

git reset --patch

Undo last commit

git reset --soft HEAD^

Meld staged changes into the last commit

git commit --amend # use -a and -m as with ordinary `commit`

Extract specific files as they were in another commit

git checkout HEAD~2 -- path/to/file

Concatenate, remove, reorder or undo commits (CAREFUL: rewrites history)

git rebase --interactive HEAD~2 

Uncommit latest changes from a single file

git reset HEAD^ -- the-file
git commit --amend # commit other changes, but not the-file

Reverting (undoing a changeset with a new commit)

git revert HEAD^ # use --no-commit to avoid automatic commit

Using the reflog

For undoing catastrophic mistakes (e.g. recovering lost commits), git keeps the reflog, a journal of repository states and actions that change the state. Any state that is stored in the reflog can be rolled back.

Find out the state identifier before the mistake:

git reflog

Roll back to that state - be careful to switch to the right branch as well.

# git checkout task-1 - if the state is on another branch
git reset --hard {{ref}} # where ref is the shasum or HEAD@\{NUMBER_FROM_REFLOG_HERE\} from reflog output

Tagging

Create an annotated tag

git tag -a v1.1.1 -m "Tagged version 1.1.1"

Share it upstream

git push --tags

Miscellany

Remove untracked files and directories

git clean -ndx # Don't actually remove anything, just show what would be done.
git clean -fdx # CAREFUL with this!

Let git do internal cleanup

git gc

Export a sub-path

find some-path -type f -print0 | xargs -0 git checkout-index -f --prefix=/some/other/path/

Useful links