Git Usage Guide
We essentially follow the GitHub Flow branching strategy. See also trunk-based development for background.
In short:
-
a single, main branch, at present named
master
, always provides a buildable, usable version of the software passing all checks. -
changes should be made on a separate branch split from
master
and merged back intomaster
. -
short-lived branches proposing small changes are preferred to long-lived branches
-
careful staging and communication is encouraged for work that must be broad-reaching or highly interdependent.
-
versions are marked with tagged commits on
master
.
We collaborate on Git projects through https://gitlab.com/TargetRWE/.
Branching and merging
Branching guidelines are
-
All proposed changes should be made in a branch separate from
master
, sometimes called feature branches. -
Feature branches should originate from
master
. -
Branches should strive to be short-lived and propose small changes, if possible.
-
In cases where a large number of changes must be made in one branch (e.g. for code correctness), extra coordination among team members would be a good idea to avoid painful conflicts.
-
Merges back to
master
are handled via merge requests (MRs) on GitLab. -
MRs are reviewed and merged in quickly, in the ideal.
-
Branch commits are squashed on merge.[1]
Example
In this example, you
-
Fetch updates from the remote.
-
Create a new local branch, e.g.
new-do
, from the HEAD commit oforigin/master
. -
Check out
new-do
and add some commits. -
Push the local branch to the remote,
origin/new-do
.
git fetch
git switch -c new-do origin/master
git push -u origin new-do
Note: The git switch
statement above will automatically set new-do
to track origin/master
. You do not want that. The subsequent line corrects that by creating origin/new-do
and setting the local new-do
to track it.
Best practices for feature branches
Note: These are subject to change as we learn what works best for us.
-
Only one developer commits to a feature branch.
-
Always branch from an up-to-date
origin/master
. -
If you must update your feature for relevant changes, take them from
origin/master
not other feature branches. -
Strive for "short-lived" branches with smallish changes where possible:
-
"Short-lived" means a branch that has existed for a week or less, though clearly that is more of a guideline than a strict standard. "Long-lived" therefore means a branch created more than a week ago. The window here is somewhat arbitrary based on our recent pace of development. Some recommend striving to keep feature branches for only a day’s work.
-
If your feature branch lags behind
origin/master
by more than a couple of commits, particularly version-tagged or relevant commits, consider splitting up the work into multiple branches so that your work-to-date can be merged more quickly intoorigin/master
. "Relevant" means work that might conflict with yours. -
If your branch is more than a couple of days old because you are stuck, reach out for help on Slack, in a meeting, or on GitLab in the appropriate MR.
-
Communicate with others whose branches have inter-related work.
-
Consider adding a sequencing order to the associated version milestone description, if both features are to be part of the same version’s work. See Managing versions.
-
Reference dependent MRs or issues in your MR’s description.
Collaboration in more complex work
Some extra care is needed when your feature branch includes changes highly dependent or disruptive on others' work. That is more likely to occur in long-running branches.
Some tips for how to handle those situations:
-
Break up the work into smaller MRs, with one feature branch for each. If the disruptive component can be isolated, it will be easier to complete quickly and to resolve merge conflicts if needed.
-
If your work is part of a version-release milestone, which it likely will be, write up a workflow plan for whose work should be merged before the other. See Milestones.
-
For changes that are likely to be highly disruptive to other people’s work, such as a large filesystem reorganization, you could suggest all other work stop until your changes are merged.
-
In some cases, it might be reasonable for one person to work on a group of interelated changes, meaning that person is assigned to all of the related issues and corresponding feature-branch MRs.
Conflicts in merge requests
The branching flow above should reduce the chances of merge conflicts, or at least reduce their complexity, by
-
relying on
origin/master
as the common branch point and source of up-to-date code, -
creating short-lived and focused feature branches, and
-
merging in MRs quickly.
Together these produce a fairly linear Git history that is easier for Git and GitLab to merge without conflict.
Conflicts can be reduced further by
-
communicating with colleagues when doing inter-dependent work on feature branches, e.g. staging sequences of feature branches with dependent changes as part of a release milestone,
-
taking extra care to ensure changes that necessarily are broad-reaching, such as filesystem reorganizations, are done in isolation and with the knowledge that doing so will produce nasty conflicts for any unmerged feature branches.
Best practices for resolving merge conflicts
Conflicts do happen, and here are some best practices for managing them in a way that should work reasonably well with GitHub Flow:
-
git rebase origin/master
on your local feature branch should be the cleanest way to update your work. -
git fetch
before you rebase.
Software versions
Versions are marked as specific states of the origin/master
branch, using annotated tagged commits. These typically are tracked in milestones on GitLab and the version tag added to origin/master
once all work is complete.
Versioning guidelines are
-
Use annotated tags in Git using the
-a
flag, as ingit tag -a v1
.[2] -
Tag labels should be of the form
v*
, and the associated tag message need not have any particular content.
Managing versions
Since versions are simply snapshots of origin/master
at a given commit, we need a process to decide who adds the version tag and when.
Note: To manage versions you will either need to have a local master
tracking origin/master
or will need to add tags through the GitLab user interface.
The workflow for managing versions is as follows:
-
Create a GitLab milestone for
vX
, versionX
of a given repo’s software. -
Associate MRs and their respective feature branches with the milestone.
-
Once all feature branches are merged for a given milestone, create a
bump-vX
branch to increase the version number. Changes required there will differ by project. -
After
bump-vX
has been merged intoorigin/master
, add a tag pointing to the resulting commit. In other words, thebump-vX
merge commit marks the state oforigin/master
giving versionvX
.
Note that you can add a tag to any past commit, so if we forget to add a version tag immediately after the milestone is complete we can add it later. See Creating tags.
Milestones
We track work on the next version of our software with GitLab milestones.
-
Milestones should be named for the version.
-
A version tag, say for version "vX", should be assigned as soon as the
bump-vX
branch of a given milestone is merged intoorigin/master
. -
MRs and issues that are not to be completed as part of this version should be removed from the milestone.
-
It might be helpful to assign a person in the milestone description to make the final
bump-vX
branch merge and subsequent tag associated with a milestone and version. -
In versions with inter-dependent feature branches, it might make sense to suggest a work flow in the milestone description for the affected branches, e.g.
FeatB → FeatC → FeatF
.
Creating tags
All version tags must be annotated tags with name of the form vX
, where X
is the version number. They need not have any particular tag message.
You can create annotated tags locally then push them to GitLab like so
git tag -a "vX" <commit>
git push --tags
Leaving <commit>
blank tags the current branch HEAD. git push --tags
pushes only tags, not commits. You can push tags to protected branches on GitLab, unlike for commits.[3]
Additional tips and comments:
-
git tag --list
or view tags in the project’s Repository menu in GitLab. -
Tag names must be unique among tags. Branches and tags can share names, but beware: Both serve as references to a commit, and doing so will create an ambiguous reference.
-
You can add multiple annotated tags to a single commit.
Creating tags with GitLab
Create them through the GitLab web UI for a project at Repository → Tags → New tag
.
-
If you do not wish to tag the HEAD commit of a branch, you can copy-paste a commit hash. The UI here is awkward though.
-
You must include a tag message to create an annotated commit.
Deleting and modifying tags
To delete a tag named "vX" from your local branch, then delete it from the remote, do
git tag -d vX
git push origin --delete vX
The simplest way to rename / replace a tag is to delete it and add it again with the new tag name.
You can add multiple tags to the same commit, as well. To add a second tag called versionX
referencing the same commit as vX
, do [4]
git tag -a versionX vX^{}
Projects referencing our software
If software is published to an external package registry, downstream projects should prefer to get the software from there. interval-algebra
, for example, is hosted on https://hackage.haskell.org, and the version of that package used would be handled by the cabal
package manager.
Sometimes, projects using the software will need to reference a Git repository rather than some other source.
In that case, projects should reference a particular version-tagged commit of the repository. Doing so avoids instability that comes with always pulling in unwanted changes from the HEAD commit, which might include merge commits from feature branches for the next version in progress.
Hot fixes
Conceptually, a hot fix is a change to software already in use that must be pushed immediately, e.g. a critical bug fix.
In some workflows, feature branches do not target master
but instead target version branches or a development branch. There, a hotfix branch would directly target master
and is intended to address some problem in production immediately.
In this workflow, hot fixes are simply branches targeted for merge into master
that retroactively are included in the previous version (as defined by a tagged commit) and are not part of a version milestone. Unlike feature branches, which are incorporated via MR, hot fixes must be incorporated into master
in a particular way.
Note on public software
This procedure is not appropriate for software published to hackage or any other public package registry, e.g. crates.io. Downstream projects using such software should already be using the versions provided by the registry, and any bug fixes would be incorporated in some versioned release in the usual manner.
This procedure only applies to software, such as asclepias
, for which some internal GitLab-hosted repository is the source.
Procedure for incorporating hot fixes
In this workflow, hot fixes require special care because commits from a version in progress might already have been merged into master
. Projects using our software via Git repository must reference a particular version-tagged commit, to avoid such unwanted changes prior to the next version-tagged commit.[5]
Therefore, a hot fix is implemented by rewriting the history of master
to insert the change directly after the version-tagged commit it modifies. You must then and update the version tag to point to the commit in which the hot fix is included. Those changes are made locally, and the history of origin/master
must be updated with a forced push.
Note only maintainers will be allowed to force push to a protected origin/master
on GitLab, and force pushes must be allowed in the repo settings.
The following commands implement this procedure for a hot fix to version "vX", as though the same person is both creating the hot fix and pushing it to GitLab. See Example of hot fix for clarity.
# Checkout version that needs fixing
git checkout vX
git switch -c hotfix-vX
git push -u origin hotfix-vX
# Make changes, commit them, share with remote etc. Note that you'll need to
# squash manually if you wish to do so, since we are not merging in the usual
# way.
<changes made to fix bug>
# Rebase master to hotfix-vX so that hotfix commits are made immediately after vX commit
git switch master
git pull
git rebase --rebase-merges hotfix-vX master
# Update the version tag. Note the new merge-base is the last commit from hotfix-vX now.
git tag -d vX
git tag -a vX $(git merge-base hotfix-vX master)
# Update the remote. You will need special privileges to force-push to protected master.
# The forced tag push is needed to overwrite the existing tag vX in the remote.
# Alternatively, you could delete the vX tag in the remote in the previous
# step. See the "Deleting and modifying tags" section.
git push --force
git push --force --tags
Example of hot fix
For clarity, the following is a more complete example of implementing a hot fix.
In this example, we suppose some project is targeting the v1.0
version of master
. That version includes a bug that must be fixed immediately. master
, though, already includes two commits (hashes efgh
and ijkl
) from features to be included in v1.1
.
Two people will be involved: The first, "Brownan Brend," will make the change in branch hotfix-v1
. The second is a privileged user, "Saulley Brad," who will update the version tag and the master
branch in the remote.
The relevant history of origin/master
before including the hot fix is
merge commit abcd (tag: v1.0) --> merge commit efgh --> merge commit ijkl (HEAD)
Note efgh
and ijkl
are merge commits, since they are incorporated into master
via MRs. That will not be the case for the hot-fix-related commit.
When we’re done, the history will be
merge commit abcd --> commit mnop (tag: v1.0, hotfix-v1) --> merge commit efgh --> merge commit ijkl (HEAD)
-
Brownan Brend creates
hotfix-v1
from thev1.0
commit ofmaster
, sharing it with the remote.
git switch master
git pull
git checkout "v1.0"
git switch -c hotfix-v1
git push -u origin hotfix-v1
-
Brownan Brend fixes the bug in commit with hash
mnop
, and pushes the changes to the remote.
< fix changes >
git commit -am "fixed bug xxxx"
git push
The relevant history for hotfix-v1
now is
merge commit abcd (tag: v1.0) --> commit mnop (HEAD)
-
Saulley Brad updates their local master to include the fix, via rebase.
git switch master
# Note this should implicitly fetch first, so you have the new origin/hotfix-v1
git pull
# git merge-base origin/hotfix-v1 master is the tagged commit v1.0
git rebase --rebase-merges origin/hotfix-v1 master
It is possible to have merge conflicts from work in commits efgh
, ijkl
related to v1.1
in progress. In cases where merge conflicts are expected, it might make sense for either Saulley or Brownan to do all of these tasks themselves, rather than splitting the load.
The history for master
in Saulley’s local repo now looks like this,
merge commit abcd (tag: v1.0) --> commit mnop (hotfix-v1) --> merge commit efgh --> merge commit ijkl (HEAD)
-
Saulley Brad updates the version tag, then updates
master
in the remote
This step redefines v1.0
of the software to include the hot fix.
git tag -d "v1.0"
# merge-base now is commit mnop
git tag -a "v1.0" $(git merge-base hotfix-v1 master)
git push --force
git push --force --tags
The relevant history for origin/master
is now
merge commit abcd --> commit mnop (tag: v1.0, hotfix-v1) --> merge commit efgh --> merge commit ijkl (HEAD)
Now, any project referring to commit with tag v1.0
will be using a version in which the fix is applied.
Development on v1.1
proceeds as normal, though any unmerged feature branches will need to rebase to origin/master
to get the updated history.
Simple linear workflow
In this case, we have two feature branches that are sequenced one after another. Both are part of the v0.2
milestone. Once merged we branch bump-v0.2
, make all version-bumping changes, and tag the merge commit of bump-v0.2
on origin/master
.
Overlapping branches
Here, we’ve complicated the workflow slightly by creating a long-running branch (feature-two
), which originally was intended to be completed in milestone v0.2
but ends up being delayed to milestone v0.3
.
Possible conflicts
Though it is not ideal to have longer-running branches, they often happen. If feature-two
is not dependent on the other features, there likely will be no conflicts when the former attempts to merge.
If feature-two
's attempt to merge creates conflicts, the branch’s author should be able to resolve them with a rebase to origin/master
, since the latter’s history is simple and linear. Otherwise, the author can use whatever methods they deem appropriate, e.g. merge origin/master
into feature-two
, cherry-pick certain commits, etc.
Note: Whatever commits there might be associated with updating feature-two
for the work in v0.2
are not shown.
To avoid such difficulties, the author could delay branching feature-two
until feature-three
is merged, or otherwise could better coordinate their work with the author of feature-three
.
Managing versions
The use of bump-vX
branches helps to organize which commits are marked for a given version. Features to be included in a given version are managed using the GitLab milestones feature. The version tag must manually be added to the merge commit for bump-vX
. See Creating tags.
In the pictured example,
-
Initially,
feature-one
,feature-two
andfeature-three
are all part of thev0.2
milestone. -
feature-two
wasn’t ready or perhaps was dependent onfeature-three
, say, and was delayed tov0.3
. -
feature-one
andfeature-three
were included in the milestone associated withv0.2
. -
feature-two
is the sole feature branch included inv0.3
.
Tags can be added retroactively, should we forget to do so before beginning work on v0.4
, and they can be deleted and remade altogether in case of error.
Note the author of feature-two
could consider simply ditching the branch, re-branching and re-doing the work if the changes from v0.2
would make updating the history difficult.
What you do not see here
The workflow above is still perfectly consistent with these guidelines. With that in mind, it’s worth noting what the diagram does not show:
-
There are no direct commits on
origin/master
outside of feature branches. All commits onorigin/master
should be via merge requests on GitLab from feature branches. -
All feature branches originate from
origin/master
and not other feature branches.
Tips & Tricks
Tags
You can checkout a tag like you would a branch. This creates a detached head, from which you can make commits or branch. With version tags, you can therefore access the state of master
for a particular version and create a branch directly from that state.
Here’s an example of using a version tag to create an example branch v1-example
from version v1
of master
.
git checkout v1
git switch -c v1-example
# do some work now
git add .
git commit -m "added an example about v1"
Finding common ancestors
git merge-base <commit refs>
displays the commit that is the common ancestor of all <commit refs>
. For example, you can find the commit at which feature-branch-one
was branched from master
using git merge-base master feature-branch-one
.
Visualizing branch structure
GitLab has some tools for visualizing branch structure, but the commandline tool has more flexibility and features.
To create a graph of the history for <commit refs>
commit references in tree-like order, with some nice annotations, you can do
git log <commit refs> --graph --pretty --topo-order --decorate
Branch names in <commit refs>
will default to the HEAD commit as usual. References can include tags or any other Git commit reference syntax.
Git allows you to alias more complex commands by adding them to your ~/.gitconfigure
file. That is helpful in particular with git log
commands that use the more advanced formatting features. Adding the following example text to your ~/.gitconfigure
file creates several aliases for git log
commands with the given specifications. These commands would be invoked with git lg
, git lg1
etc.
[alias]
lg = !"git lg1"
lg1 = !"git lg1-specific --all"
lg2 = !"git lg2-specific --all"
lg3 = !"git lg3-specific --all"
lg1-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(auto)%d%C(reset)'
lg2-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(auto)%d%C(reset)%n'' %C(white)%s%C(reset) %C(dim white)- %an%C(reset)'
lg3-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset) %C(bold cyan)(committed: %cD)%C(reset) %C(auto)%d%C(reset)%n'' %C(white)%s%C(reset)%n'' %C(dim white)- %an <%ae> %C(reset) %C(dim white)(committer: %cn <%ce>)%C(reset)'
change-commits = "!f() { VAR=$1; OLD=$2; NEW=$3; shift 3; git filter-branch --env-filter \"if [[ \\\"$`echo $VAR`\\\" = '$OLD' ]]; then export $VAR='$NEW'; fi\" $@; }; f"
Running GitLab jobs locally
The gitlab-runner
application can be used to run jobs locally.
Use the exec
command.
gitlab-runner exec can (currently) only run one job at a time.
|
See here for instructions on installing gitlab-runner on your machine.
|
Here is an example of using the shell
executor to run the check-source
job in asclepias.
% gitlab-runner exec shell check-source
Runtime platform arch=amd64 os=darwin pid=21602 revision=f0a95a76 version=14.5.0
WARNING: You most probably have uncommitted changes.
WARNING: These changes will not be tested.
Running with gitlab-runner 14.5.0 (f0a95a76)
Preparing the "shell" executor
Using Shell executor...
executor not supported job=1 project=0 referee=metrics
Preparing environment
Running on TGTRWE-LT-0081...
Getting source from Git repository
Fetching changes...
Initialized empty Git repository in /Users/bsaul/Documents/novisci/software/asclepias/builds/0/project-0/.git/
Created fresh repository.
Checking out 775b60a6 as 146-docs...
Skipping Git submodules setup
Executing "step_script" stage of the job script
$ ./scripts/lint.sh
No hints
$ ./ci/ci-check-format.sh
Job succeeded
Metrics tracking workflow
We hope to generate a few metrics to track:
-
How well we are following the workflow described above.
-
To what extent we have avoided "problematic" merges and other Git-related problems.
Exactly how best to do so is not clear and is subject to ongoing debate. This section will include a description of such metrics and means to generate them once we have settled on some.
^{}
reference syntax is to ensure the tag references the commit and not the tag itself. From git help revisions
"A suffix ^ followed by an empty brace pair means the object could be a tag, and dereference the tag recursively until a non-tag object is found."
development
branch in the workflow.