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 originmerge
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]
Step 7. Remove checked out branches
rm -r branch-1
git worktree prune
Or, since git 2.17:
git worktree remove branch-1
See the SO link for further detail.