e42.uk Circle Device

 

Quick Reference

git Workflow

git Workflow

Revised to make use of tagging for releases and branches for work in progress. 2021-10-05. Deleting remote branches and squashing commits. 2022-03-09.

To begin with there is one branch, master or main, the HEAD of this branch is where working but not release ready code is available. Releases are tagged commits on this branch. In general the master branch should always compile and all automated tests should be successful when run against the code here.

Set up a project on a server

Create a bare git repository on a server to which you have ssh access. If you wish to share this project with other people include the --shared flag which will allow the project to be shared with all users in the group to which the directory belongs. In the example below the project is owned by ben and shared with users in the developers group:

cd /home/git
mkdir dht.git
chown ben:developers dht.git
cd dht.git
su ben
git init --bare --shared .

The repository just created does not have any branches or commits, it also does not have a working tree (files to track). There is no need for the remote repository to have a working tree since a working tree is only required for reading or writing files in the repository.

Create a local repo and track the remote

On the development machine:

mkdir dht
cd dht
git init .
echo "# The DHT Project" > README.md
echo "" >> README.md
echo "An example for making a git repository" >> README.md
echo "* text=auto" > .gitattributes
echo "*.md text eol=lf" >> .gitattributes
git add .
git commit -m "Initial Commit"
git remote add origin ssh://ben@192.168.0.2/home/git/dht.git
git push --set-upstream origin master

Although the above created a new local repository and then pushed the changes to the remote, an existing repository could be linked to the remote in a similar way.

The .gitattributes file is important when sharing the repository with other developers and text editors. The definition here will convert different line endings into unix line endings upon commit of a matching file to the repository. In this case if the README.md file (or any file ending in .md) had dos line endings (crlf) they would be converted to unix (lf) when committed to the local repository (line endings in the work tree will not be changed).

git push --set-upstream origin master sets the current branch to track the master branch at origin (the remote repository). This is useful when fetching and pushing as the specific locations and branches can be omitted.

Multiple Remote Repositories

First ensure that one remote repository is setup and working. Add a bare repository on remote (of course this step may be performed with a different UI but for the sake of this tutorial this is how one might do it manually):

cd /home/git
mkdir dht.git
cd dht.git
git init --bare --shared .

Add the remote as origin-backup:

git remote add origin-backup ssh://backup-server.e42.uk/home/git/dht.git

Then push the branch master to the remote server:

git push origin-backup master

To push to all remote repositories xargs can be used in combination with git remote and git push:

git remote | xargs -L1 git push

The above will only push the current branch (as you would expect).

StackOverlow Question with related answers

Some more examples

# Push master to all remotes
git remote | xargs -L1 -I R git push R master
# Push all to all remotes
git remote | xargs -L1 git push --all

Make an alias then use git pushall:

git config --global alias.pushall '!git remote | xargs -L1 git push --all'

Clone a project locally

Once the repository is available remotely it may be cloned to begin work.

To begin work on a project clone it locally:

git clone ssh://ben@192.168.0.2/home/git/dht.git dht
cd dht
cat README.md

The above should display the contents of the README.md file created and committed earlier.

The local repository now has a master branch and can be worked on. This local master branch is automatically setup to track master at origin. Often this is a good time to run the automated tests (if there are any) to ensure that you are not working with a broken build.

Making changes

To make changes to the repository a branch should be created. To create a branch:

git checkout -b feature/findnode

After the above command is executed a new branch is created based on the current commit. It is also possible to create a branch and switch to it using the long form:

git branch feature/findnode HEAD
git checkout feature/findnode

All branches may be listed:

git branch -a

Creating and switching branches will not overwrite any modified files in the work tree.

Here the branch is created to add a feature but of course bugs may also be fixed in a branch.

Once in the feature/findnode branch changes can be made, committed and pushed to the remote for testing, review etc whilst the contents of the master branch remain unchanged.

Make some changes and commit them:

echo "TODO: add docs for trimstring" >> README.md
echo "*.c text eol=lf" >> .gitattributes
mkdir src
mkdir testsrc
vi src/trimstring.h src/trimstring.c testsrc/trimstring_tests.c
git add .
git commit -m "Adding trimstring functionality"

At this point it may be appropriate for a fellow developer to review the work or to offer assistance if trimming a string becomes too complicated. To allow that other developer to access the changes on this branch the commits and the branch must be pushed to the server:

git push --set-upstream origin feature/findnode

The above command will do three things:

  • Write the commits on the current branch to the remote repository
  • Create a branch in the remote repository called feature/findnode
  • Create a link between the remote branch specified on the command line (feature/findnode) and the current branch in the local repository. When present on a local branch this branch is known as a tracking branch in that is tracks the remote branch.

The local changes can be seen in the .git/config file:

[branch "feature/findnode"]
    remote = origin
    merge = refs/heads/feature/findnode

IMPORTANT NOTE: the last argument to the git push command (feature/findnode) is not the name of the local branch, rather the name of the remote branch that the current branch should be pushed to. It is probably best to keep the names of local and remote branches the same.

Merging Changes from master

Before review all changes that may have been made on master should be merged into the feature branch (in this case feature/findnode). To merge the changes:

git checkout master
git fetch
git merge

These commands perform the following actions:

  • Switch to master, the local copy which may be old
  • fetch changes from the origin
  • merge changes that are not present on the local repository

The merge will always be a fast-forward (all the changes are in the feature branch) and in this case a git pull will perform the fetch and merge steps.

The master branch is now up-to-date on the local repository. At this point it it worthwhile running the tests to ensure that the local environment is sane and locate any problems that may have been introduced in the mean time.

The next step is to merge the changes from master into the feature branch:

git checkout feature/findnode
git merge master

These steps will restore the feature/findnode branch and then merge any changes present in the master branch. This may involve conflicts that should be resolved in the normal way.

Once all conflicts are resolved and all the tests pass the work is ready for review.

Reviewing work on a different branch

In order to see the new branches for an already cloned repository a fetch must be performed.

git fetch

New branches should be listed as output to this command:

From ssh://ben@192.168.0.2/home/git/dht.git
 * [new branch]      feature/findnode -> origin/feature/findnode

All branches can be listed as above with:

git branch -a

checkout the desired branch:

git checkout feature/findnode

At this point the work tree will reflect the state of the branch as in the local repository. If the branch was new when fetched the state will be up-to-date with origin. By checking out a branch that is present on remote, as above, the branch is set up to be tracked. In the same way as git push --set-upstream did earlier, please see .git/config.

If the branch was already known then a fetch on a different branch will only fetch changes to that branch and any new branches existing branches must be updated by fetching individually.

git fetch

Fetching all branches is tricky... see the stack overflow link in references for more detail... and be careful!

Merging Completed Work

Once complete the work on the feature branch can be merged into the master branch. This process should be a fast-forward as the conflicts with master were resolved before the review. These steps assume that the master and feature branch (feature/findnode) are up-to-date. These instructions use a squash commit which will create a single commit for the whole feature branch. See references for detail on squashing commits.

git checkout master
git merge --squash feature/findnode
git commit -m "Allow the dht to find a node"

Then remove the branches from remote and local (in that order).

git push origin --delete feature/findnode
git branch -D feature/findnode

Squashing Some Commits

When your work meanders it is often nice to squash some commits together but not all of them. This can be a little challenging as this is a form of rewriting history (often not a good idea in git) but given the following scenario:

$ git log --oneline --abbrev-commit
8dd0d7f (HEAD -> master) Don't install the static library
a59c9d1 Update build system to include hashing functions
a8d4067 Framework first hashing functions
9dfc9a9 Add instructions for build system to readme
ae5df72 Create initial build system
9c473f2 Initial Commit

The aim will be to squash 9dfc9a9 Add instructions for build system to readme with ae5df72 Create initial build system. squash means to take the two commits and make them into one. After executing git rebase a file will be loaded with the default editor, change this file to squash 9dfc9a9 into ae5df72 then save and close:

$ git rebase -i HEAD~5
pick ae5df72 Create initial build system
squash 9dfc9a9 Add instructions for build system to readme
pick a8d4067 Framework first hashing functions
pick a59c9d1 Update build system to include hashing functions  
pick 8dd0d7f Don't install the static library

When the file is closed another file will be presented which contains the message that will be added to the new commit, in this example the contents are deleted entirely and the new commit message will be Create meson build system (to make the log more obvious):

$ git log --oneline --abbrev-commit
d6dcc3e (HEAD -> master) Don't install the static library
e37f906 Update build system to include hashing functions
8424df9 Framework first hashing functions
5df3d42 Create meson build system
9c473f2 Initial Commit

Notice that the commit hash has changed and the initial commit is now visible in the last 5 commits.

git worktree

Checking out multiple branches at the same time in different directories and having the ability to make changes as desired to each. Beware, this can get confusing quickly.

There are many ways to do this but I like this very simple way:

Step 1. Clone the repo:

git clone --no-checkout ssh://blah/repo.git

Step 2. Enter the repo

cd repo

Step 3. Create a dummy branch (this is part of the confusing bit)

git switch -c dummy

Step 4. Checkout a branch into a directory

git worktree add branch-1

Step 5. Checkout another branch into another directory

git worktree add branch-2-dir branch-2

Step 6. List the checked out branches

git worktree list
/home/user/repo               0000000 [dummy]
/home/user/repo/branch-1      1111111 [branch-1]
/home/user/repo/branch-2-dir  2222222 [branch-2]

See the SO link for further detail.

References

Quick Links: Techie Stuff | General | Personal | Quick Reference