How I use git

I use git every day for nearly everything that I am doing that creates files that change and benefit from either a change history or an archival storage. These are the main guidelines I follow when using it, especially for software projects. Everything I advocate here is just the way I do it because that’s how I have done for a long while. It might not be the best way to accomplish the tasks. It might not fit the way you work with version control. However, three-plus decades of using git and other source control management systems has directed me to using it in these ways.

Commands

There is a large set of possible commands in the git set. You can list them all with the help commands:

  • git help
  • git help --all

The commands that I use most often are, roughly ordered by the frequency of use:

  • git commit
  • git status
  • git pull
  • git push
  • git rebase
  • git merge
  • git fetch --all
  • git branch
  • git switch
  • git reset
  • git log
  • git reflog
  • git init

Several of these are used by way of alias definitons or wrapped in a set of helper scripts that I have grown over the years. For example, I rarely use git log raw like that. It will be used when it use git tree where tree is an alias I have defined that shows a pretty log with a nifty history graph.

Workflows

I generally work in a style of Trunk-based Development where main is kept current as frequently as possible (daily or more) and any branches created are only used for a very short time (daily at most) while working on a new bit of code. Some would not consider this Trunk-based since branches are created in some cases. I do consider it such because the only code that matters is on main and in-progress work is merged into main very frequently. Using working branches generally makes it easier for projects with multiple teams or pairs in the team working on the same code.

Certainly if there is no need to create a branch, then a branch should not be created. If you can pull main, add to the code (which includes testing it thouroughly), and then push it back to the remote, by all means do so. If you need to hand off the in-progress code for any reason (driver swap, have to go to meeting, have to take the dog for a walk, etc.) then creating a branch to push to for the hand off is the best way to handle that.

However, it is important to have the discipline to keep the life of the branch as short as possible. A day is generally as long as I care to hold on to a branch. You should resist with great vigor any call to use any of the Git Flow types of branch management schemes. Any system where there is a develop branch and a separate production branch or a branch for each release, should be viewed with disgust. These schemes are bound to bring more friction than to the delivery system.

Linear History

The main branch should always have a Linear History. Working branches should be deleted as soon as they are merged into main.

%%{init: { 'gitGraph': {'showCommitLabel': false}} }%% gitGraph commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit

I really dislike seeing merge commits in main. This is so ugly and can cause complications when doing any git history analysis.

%%{init: { 'gitGraph': {'showCommitLabel': false}} }%% gitGraph commit commit commit branch A commit commit checkout main commit branch B commit checkout main commit branch C commit commit checkout B commit commit checkout main commit commit merge A commit merge B commit merge C

I let git enforce this by setting the fast-forward-only configuration in the git config.

[merge]
    ff = only

When this is set, git will not perform a merge that requires any possible conflict resolution.

There are rare cases where something has gone awry and requires an actual merge with resolutons. To override the config setting, add the --ff command line option to the merge command.

git merge -ff that_problem_branch

I have never seen a case where this is really necessary, however. Any time where it this seems needed, you can create a branch to resolve the issues then fast-forward merge that branch to main.

Rebasing

Working branches are rebased to main frequently during the working phase and expecially before being merged back to main so that the Linear History of main is preserved. If there are no new commits on main since the working branch was created, the rebase will be zero work. If there are some changes, then the conflict resolution will happen on the branch and should be simple because the branches are not long-lived. Once the branch has been rebased to main it can be fast-forward merged to maintain the Linear History of main.

--- title: Before Rebase --- gitGraph commit commit commit branch do_a_thing checkout do_a_thing commit id:"A" commit id:"B" checkout main commit id:"C" commit id:"D"
--- title: After Rebase --- gitGraph commit commit commit commit id:"C" commit id:"D" branch do_a_thing checkout do_a_thing commit id:"A" commit id:"B"

There are those who say rebase is evil (or at least Mostly Bad) because it destroys history. I say rebase is a tool and, as with any tool, can be used for either good or evil. I use it for the good of keeping working branches up to date with main while work continues and for the good of maintaining the Linear History of main.

Squash Working branches

I also usually squash working branches that have had many commits because those commits are usually not helpful history. No one needs to see five commits with the comment "typo" or 10 commits that are "created a failing test" followed by the matching 10 which are "made the test pass". All of those commits on the working branch get squashed down to one commit with a clear comment on what feature was added to to the product.

I use a version of these steps to squash a branch that was made from main

git reset main
git add -A
git commit --edit -m "Some useful description"

There is actually more to it than that, but that is the gist. If main has had any commits added since the branch you have to make sure to reset to either have rebased before the squash or to reset to the commit where the branch was made. I also do some work to gather all the commit messages from the working branch into the final comment message as well as any other commit authors when we are working in a group and passing the branch around during driver switches.

You could also use an interactive rebase or the --squash option on a merge. I tend not to use those because I generally want to resolve the rebase to main separately from the squash, usually will squash all the commits, and want to run tests after the squash but before merging to main.

End Note

That’s the meat of it. There’s quite a lot you can do with a git repository, but most of the time you don’t need much. Even the above short list is more than you will use on the normal days. For those you likely only need pull, commit, and push.

I’m here if you ever want to talk about ways to use git in your world.