Help! I created too many changes and want to make separate commits – git add interactive, patching and branching

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?

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:

Shell

There are several options available in this case:

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:

Shell

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:

Shell

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:

Shell

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, press p for patch, them file number to patch and <ENTER> to confirm; or
  • git 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:

Plain Text

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.
  • 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 of y, just repeat the patching.
  • If you pressed y instead of n, you can use the complementary command git 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 ( ) into removal 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 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9

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:

Plain Text

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 2
Line 3
-Line 4
+Line IVa
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IVa
Line 5
Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9

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 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line IVa
+Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IVa
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9

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:

Plain Text

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 2
Line 3
-Line 4
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line 5
Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9

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 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
+Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9

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:

Plain Text

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 2
Line 3
Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line 4
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9

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 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9

 

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:

Plain Text

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 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
Line 1
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9

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 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
 Line 1
+Line 2
Line 3
Line IV
Line 5
Line 6
Line 7
Line 8
Line 9
 Line 1
-Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9

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:

Plain Text

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 2
+Line 2a
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 2a
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
Line 1
Line 2
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9

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 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
 Line 1
Line 2
-Line 2a
Line 3
Line IV
Line 5
Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
+Line 2a
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9
 Line 1
Line 2
Line 3
-Line 4
+Line IV
Line 5
-Line 6
+Line VI
Line 7
Line 8
Line 9

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:

Plain Text

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:

Plain Text

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:

Shell

Now we have to save all pending changes to a commit:

Shell

Let’s go back to the previous branch (master in my case) and clean the working directory:

Shell

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:

Shell

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:
Shell
  • selectively apply changes from the temp branch from all files or selected file/path:
Shell

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).

Leave a Reply

avatar
  Subscribe  
Notify of