Help! I created too many changes and want to make separate commits – git add interactive, patching and branching
There are cases when you try to add a feature, but before it’s done, a new problem arises and you try to resolve it. At the end of the day you end up with several things solved, but no commits yet. Some people prefer big and messy commits that include all the work for the past week, but I tend to have one issue solved in one commit. What are your options to keep a clean repository in this situation?
Table of contents
- Introduction
- Manually undo and redo files
- Use git’s interactive mode
- Quick introduction to git interactive mode
- Git patch
- Manually editing a patch
- Example
- Change the added lines (+) contents
- Do not stage added lines (+) by removing them
- Do not stage removed lines (-) by changing them into context ( )
- Changing context lines ( ) into removal lines (-)
- Adding new lines: (+)
- Adding, deleting or modifying context lines ( )
- Adding, deleting or modifying removal lines (-)
- Quick introduction to git interactive mode
- Use an auxiliary branch
Introduction
Let’s consider my case. I did performance analysis and it badly turned out I did at least three changes without committing anything. I wanted to have one performance improvement in one commit.
This was my repository after the changes:
There are several options available in this case:
By the way, I’ve put together a small collection of tools I use regularly — things like time tracking, Git helpers, database utilities, and a few others that make development a bit smoother.
Manually undo and redo files
You can copy the entire (or part of) the project to a separate folder, and then undo all pending changes:
Now you can manually copy the changes you want one by one from the other folder to your project – either entire files or single lines.
Use git’s interactive mode
Git add includes a very useful interactive mode to deal with similar cases.
Quick introduction to git interactive mode
After running git add -i
, the git interactive mode is started. It presents you with several options:
Option 1 status displays this summary, which shows all paths that are present in the repository (note that, compared to git status
, untracked files are not shown here).
For every path, you can see how many lines have been staged and unstaged. For instance, FileParser.cs
has unstaged 79 lines added and 9 lines removed.
Option 2 update allows adding the entire file(s) to the staged area. It’s the same as using git add <path>
.
After selecting the option, you have to provide numbers of the files to be added, separated by the ENTER
key. For example, to stage the Fileparser.cs
file, press: 2
or u
or update
to enter the update menu, next 3<ENTER>
to select the FileParser.cs
file and then hit just <ENTER>
to finish adding. As a result, the file was staged:
Note that it is possible to add several files, by providing their numbers: 3<ENTER>
, 4<ENTER>
, 1<ENTER>
, <ENTER>
. Typing *<ENTER>
selects all files.
Option 3 revert allows removing the entire file(s) from the staged area. it’s the same as using git reset HEAD <path>
.
Option 4 add untracked allows adding untracked files to the index. It works the same as 2 update, but lists only the untracked files. This operation can be undone by using the 3 revert option.
Option 5 patch will be described in the next section.
Option 6 diff shows the diff for the selected file.
Option 7 quit returns back to the terminal.
Git patch
The patching mode can be entered by either calling:
git add -i
, pressp
for patch, them file number to patch and<ENTER>
to confirm; orgit add -p [<path>]
, a path can be optionally provided. Without it, all files will be selected.
The patching mode shows one “hunk” of changes at once and provides numerous options:
Not all options are available at every time, they depend on the available context.
The most interesting options are:
- add:
- y – same as
git add
, but applied only to a part of the file. It adds the hunk to the index. - a – does the same to this and all later hunks in this file. Useful if you know the rest of the file should be added to the index too.
- y – same as
- skip:
- n – skip the hunk. It won’t be added to the index, it will remain in the working area.
- d – skip this and all later hunks in this file. Useful if you know the rest of the file should not be added.
- advanced:
- s – if given hunk contains both changes that should be added and skipped, this option allows you to divide this hunk into smaller ones. Unfortunately, this option does not always end with the size of hunks you’d want, and in this case you have to manually edit it.
- e – it allows you editing the hunk and apply only the lines you want. See below for details.
- K – go to the previous hunk in this file
- q – stop the process. All items added to the index will remain there.
Unfortunately, there is no option to freely navigate back and forth, so if you make a mistake, you can’t always go back to fix it. If the K
option is available, just use it. Otherwise, try the following hints:
- If you pressed
n
instead ofy
, just repeat the patching. - If you pressed
y
instead ofn
, you can use the complementary commandgit reset -p
to undo a hunk, or just undo the whole file with the 3 revert option in the main menu.
Manually editing a patch
There is also an advanced option to edit a hunk before adding it – press e
in the patch mode, or run git add -e [<path>]
.
The most common and safest things are adding new changes and temporarily undoing some changes:
- change the added lines (
+
) contents - do not stage added lines (
+
) by removing them - do not stage removed lines (
-
) by changing them into context (
Beware though, that not all edits are safe and obvious. According to the docs:
There are also more complex operations that can be performed. But beware that because the patch is applied only to the index and not the working tree, the working tree will appear to “undo” the change in the index. For example, introducing a new line into the index that is in neither the HEAD nor the working tree will stage the new line for commit, but the line will appear to be reverted in the working tree.
Examples of such operations:
- changing context lines (
-
) - adding new lines: (
+
)
And here are the examples of forbidden operations that will make the patch impossible to apply due to invalid line context:
- adding, deleting or modifying context lines (
- adding, deleting or modifying removal lines (
-
)
Example
Let’s examine such changes. Let’s assume there is a simple file in a repository:
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
and we changed Line 4
to Line IV
and Line 6
to Line IV
:
Before |
After |
Diff |
Line 1 |
Line 1 |
Line 1 |
The following examples will describe changes made during git patch edit mode (git add -e
).
Change the added lines (+) contents
What will happen after changing the line that was already changed (+
)?
Summary:
The areas’ contents:
Edited patch git add -e |
Index git show -p `git ls-files --stage | awk '{print $2}'` |
Working Dir cat file.txt |
Line 1 |
Line 1 |
Line 1 |
and the diffs:
Head git show |
Working vs Index git diff |
Head vs Index git diff --cached |
Head vs Working git diff HEAD |
Line 1 |
Line 1 |
Line 1 |
Line 1 |
Conclusion: changing the added line (+
) includes the change in the index, but the current file does not contain that change. Thus, further adding the file to the index (git add
) will overwrite that change. I don’t find a use case for this.
Do not stage added lines (+) by removing them
What will happen after removing the line that was already changed (+
)?
Summary:
The areas’ contents:
Edited patch git add -e |
Index git show -p `git ls-files --stage | awk '{print $2}' |
Working Dir cat file.txt |
Line 1 |
Line 1 |
Line 1 |
and the diffs:
Head git show |
Working vs Index git diff |
Head vs Index git diff --cached |
Head vs Working git diff HEAD |
Line 1 |
Line 1 |
Line 1 |
Line 1 |
Conclusion: removing the added line (+
) will not include them in the index. However, it will still be present in the working file. It’s good if you want to exclude a change in one commit, but include it in another.
Do not stage removed lines (-) by changing them into context ( )
What will happen after changing the line that was already removed (-
) into a context line (
)?
Summary:
The areas’ contents:
Edited patch git add -e |
Index git show -p `git ls-files --stage | awk '{print $2}' |
Working Dir cat file.txt |
Line 1 |
Line 1 |
Line 1 |
and the diffs:
Head git show |
Working vs Index git diff |
Head vs Index git diff --cached |
Head vs Working git diff HEAD |
Line 1 |
Line 1 |
Line 1 |
Line 1 |
Conclusion: changing the removed line (-
) into a context line (
) will not include the removal to the index. It makes little sense in the case of a change (consisting of the pair of the removed and added line), but it may be useful in the case of simple removal of lines. Then the index won’t include the removal, which will still be present in the working directory and possible to be added to a next commit.
Changing context lines ( ) into removal lines (-)
What will happen after converting the context line (
) into a removal line (-
)?
Summary:
The areas’ contents:
Edited patch git add -e |
Index git show -p `git ls-files --stage | awk '{print $2}' |
Working Dir cat file.txt |
Line 1 |
Line 1 |
Line 1 |
and the diffs:
Head git show |
Working vs Index git diff |
Head vs Index git diff --cached |
Head vs Working git diff HEAD |
Line 1 |
Line 1 |
Line 1 |
Line 1 |
Conclusion: converting a context line (
) into a removal line (-
) will not stage that change, but it will be present in the working file. Useful if you want to split the change into two commits.
Adding new lines: (+)
What will happen after adding new + lines?
Summary:
The areas’ contents:
Edited patch git add -e |
Index git show -p `git ls-files --stage | awk '{print $2}' |
Working Dir cat file.txt |
Line 1 |
Line 1 |
Line 1 |
and the diffs:
Head git show |
Working vs Index git diff |
Head vs Index git diff --cached |
Head vs Working git diff HEAD |
Line 1 |
Line 1 |
Line 1 |
Line 1 |
Conclusion: introducing a new addition (+
) will add it only to the index, but it won’t be present in the working directory, so it will be removed with next git add
.
Adding, deleting or modifying context lines ( )
What will happen after modifying the context line (
)?
Summary:
Conclusion: such changes will usually end with an invalid patch that cannot be applied. No wonder, git cannot recognize which lines were changed if the context is invalid.
Adding, deleting or modifying removal lines (-)
What will happen after modifying the removal lines (-
)?
Summary:
Conclusion: this is the same situation as previously, because the removed lines were part of the context.
Use an auxiliary branch
Back to the main methods of dealing with too vast changes. The third option I wanted to write about is putting the changes to an auxiliary branch, and then applying the necessary changes to the main branch.
This approach is similar to the first one with making a copy to a separate folder, but now we stay in the git ecosystem.
The first step is making a new temporary branch:
Now we have to save all pending changes to a commit:
Let’s go back to the previous branch (master
in my case) and clean the working directory:
At this stage, all pending commits should be only in the temp
branch, and the current branch should no contain those changes. We can verify that by calling:
The second command will list all differences between the current and the temp
branch.
Now we’re ready to pick and commit separate changes. There are two sets of commands useful here:
- check the differences for all files or selected file/path:
- selectively apply changes from the
temp
branch from all files or selected file/path:
The last commands work like git add -p
, but taking the source from temp
branch. It’s now easy to copy only the lines we want for one commit by using the command described previously: y
(apply a hunk), s
(split a hunk into smaller ones) and e
(manually edit a hunk to e.g. select just one line).
Super useful, and detailed, thanks a lot!