Git is the world’s most popular version control software out there at the moment and if you write code and version control it the chances are you are using it. It’s a complex tool though and there are a bewildering array of options. In this short post we will look at some options for changing commits that have been made.
Python Examples
We’ll use my pytest-examples as an example for this work but you can do this in any repository you have going. We’ll clone the repository and make a new branch called amend-fixup-tutorial
.
git clone git@github.com:ns-rse/pytest-examples.git
cd pytest-examples
git switch -c amend-fixup-tutorial
Switched to a new branch 'amend-fixup-tutorial'
The Git Dance
A typical Git work flow involves making some changes to one or more files (or adding a new one into the repository). These are staged with git add <files>
, or you can use a shortcut git add -u/--update
to add all currently tracked files that have been modified if you are not adding anything new, before committing them with a message using git commit -m "<meaningful message about content being added>"
.
Lets add a simple CONTRIBUTING.md
file to the repository.
echo "# Contributing\n\nContributions via pull requests are welcome." > CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "Adding CONTRIBUTING.md"
git log --oneline
01191a2 (HEAD -> amend-fixup-tutorial) Adding CONTRIBUTING.md
This should be familiar to users of Git whether you use the command line interface (CLI), Emacs’ amazing Git porcelain magit or any other tool such as GitKraken or the support in you IDE such as RStudio or VSCode.
Making Amends
Sometimes you will have made a commit and you realise that you want to add more to it or perhaps you forgot to run your test suite and find that on running it your tests fail so you need to make a correction. In this example we want to be more explicit about how to make contributions and let people know they should fork the branch.
echo "\n Please make a fork of this repository, make your changes and open a Pull Request." >> CONTRIBUTING.md
Now you could make a second commit…
git add -u
git commit -m "Ask for PRs via fork in CONTRIBUTING.md"
git log --oneline
9f0655b (HEAD -> amend-fixup-tutorial) Ask for PRs via fork in CONTRIBUTING.md
01191a2 Adding CONTRIBUTING.md
…and there is nothing wrong with that. However, Git history can get long and complicated when there are lots of small commits, because these two changes to CONTRIBUTING.md
are essentially the same piece of work and if we’d been thinking clearly we would have written about making forks in the first place and made a single commit.
Fortunately Git can help here as there is the git commit --amend
option which adds the staged changes to the last commit and allows you to edit the last commit message (if nothing is currently staged then you will be prompted to edit the last commit message). We can undo the last commit using git reset HEAD~1
and instead amend the first commit that added the CONTRIBUTING.md
git add -u
git commit --amend
git log --oneline
4fda15f (HEAD -> amend-fixup-tutorial) Adding CONTRIBUTING.md
cat CONTRIBUTING.md
# Contributing
Contributions via pull requests are welcome.
Please make a fork of this repository, make your changes and open a Pull Request.
We now have one commit which contains the new CONTRIBUTING.md
file that contains all the changes we wished to have in the file in the first place and our Git history is slightly more compact.
Fixing things up
Amending commits is great providing the commit you want to change is the last commit you made (i.e. HEAD
). But sometimes you might wish to correct a commit further back in your history and git commit --amend
is of no use here. Git can however help here with the git commit --fixup
command which allows you to mark a commit as being a “fix up” of an older commit. These can then be autosquashed via an interactive Git rebase.
Let’s add a few empty commits to our amend-fixup-tutorial
branch to so we can do this.
git commit --allow-empty -m "Empty commit for demonstration purposes"
git commit --allow-empty -m "Another empty commit for demonstration purposes"
git log --oneline
8061221 (HEAD -> amend-fixup-tutorial) Another empty commit for demonstration purposes
65587ce Empty commit for demonstration purposes
4fda15f Adding CONTRIBUTING.md
And let’s expand our CONTRIBUTING.md
file further.
echo "\nPlease note this repository uses [pre-commit](https://pre-commit.com) to lint the Python code and Markdown files." >> CONTRIBUTING.md
We want to merge this commit with the first one we made in this tutorial using git commit --fixup
. To do this we need to know the hash (4fda15f
see output from above git log
) or the relative reference of the commit we want which in this case is HEAD~2
as it is three commits back from the current HEAD
(which is commit 0
, most indexing in computing starts at 0
rather than 1
). Use one for the following git commit --fixup
commands (adjusting the hash to yours if you are using that option, you can find this using git log --oneline
).
git add -u
git commit --fixup 4fda15f
git commit --fixup HEAD~2
We see the commit we have just made starts with fixup!
and is then followed by the commit message that it is fixing.
git log --oneline
97711a4 (HEAD -> amend-fixup-tutorial) fixup! Adding CONTRIBUTING.md
8061221 Another empty commit for demonstration purposes
65587ce Empty commit for demonstration purposes
4fda15f Adding CONTRIBUTING.md
The final step is to perform the automatic squashing via an interactive rebase, again you can either use the hash or the relative reference.
git rebase -i --autosquash 4fda15f
git rebase -i --autosquash HEAD~2
This will open the default editor and because the --autosquash
option has been used it will already have marked the commits that need combining with fixup
. All you have to do is save the file and exit and we can check the history and look at the contents of the file.
NB If you find that the necessary commit isn’t already marked navigate to that line and delete pick
. The lines below the file you have open give instructions on how you can mark commits for different actions, in this case you can replace pick
with either f
or fixup
. Save and exit and the commits are squashed.
git log --oneline
0fda21e (HEAD -> amend-fixup-tutorial) Another empty commit for demonstration purposes
65587ce Empty commit for demonstration purposes
4fda15f Adding CONTRIBUTING.md
cat CONTRIBUTING.md
# Contributing
Contributions via pull requests are welcome.
Please make a fork of this repository, make your changes and open a Pull Request.
Please note this repository uses [pre-commit](https://pre-commit.com) to lint the Python code and Markdown files.
And you’re all done! If you were doing this for real on a repository you could now git push
or continue your work. As this was just an example we can switch branches back to main
and force deletion of the branch we created.
git switch main
git branch -D amend-fixup-tutorial
Conclusion
Git has lots of commands to help you maintain a clean history by using --amend
and --fixup
flags to git commit
and in the later case then performing an interactive git rebase -i
. This takes a little discipline to get into the practice of but once in the habit of doing so it greatly improves the readability of the Git history and avoids including commit messages such as Fixing typo
/ Linting code
/ Fixing tests
/ I've gone mad!
.
If all of this sounds completely unfamiliar to you but you would like to learn more about Git I can highly recommend the introductory course developed by Dr Anna Krystalli Git and GitHub through GitKraken : From Zero to Hero. This course is run regularly by myself and colleagues in Research Software Engineering for post-graduate researchers and staff at the University of Sheffield.
Reuse
Citation
@online{shephard2024,
author = {Shephard, Neil},
title = {Git - {Making} {Amends} and {Fixing} Things Up},
date = {2024-03-08},
url = {https://blog.nshephard.dev/posts/git-amend-fixup/},
langid = {en}
}