git workflow for largish teams
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.
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.
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).
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 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.
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.
# 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.
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.
git clone git@host:project.git
cd project
... apply settings as above ...
git checkout --track origin/devel
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
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
Familiarize yourself with ancestry references
- see the Pro Git book
-
HEAD~2
andHEAD^^
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
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
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
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
Create an annotated tag
git tag -a v1.1.1 -m "Tagged version 1.1.1"
Share it upstream
git push --tags
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/