Git Stash Bisection

Introduction

Sometimes, when we submitted a pull request, we might realize that the changes in the pull request failed some tests. In some cases, the changes from the pull request can be grouped and each group should not fail any test in principle, we will have to identify which exact group of changes caused the failure. Ideally, each group of changes should become a separate commit, so we can use the bisect feature of Git, i.e., git bisect to find which commit introduced the failure. However, in practice, this is usually not the case, and we will still have to identify the problematic changes from one commit.

In this blog post, I would like to discuss how to use Git stash, i.e., git stash, to help us perform bisection to root cause the problematic changes.

Git Stash Bisection

Suppose we have a commit that introduces changes in five files, including 0.cpp, 1.cpp, 2.cpp, 3.cpp, and 4.cpp. The change of each file is independent from the others and we want to identify which file’s change caused the test failure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ git log -p
commit 71d47065c1f4deb1a68a12b3a13a8c492bdfcc36 (HEAD -> main, origin/main, origin/HEAD)
Author: leimao <dukeleimao@gmail.com>
Date: Thu Aug 14 20:32:11 2025 -0700

Add Files

diff --git a/0.cpp b/0.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/1.cpp b/1.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/2.cpp b/2.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/3.cpp b/3.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/4.cpp b/4.cpp
new file mode 100644
index 0000000..e69de29

commit 37d3ac9cce852faaf6d6a850b946bbcc97e1c9f9 (make_changes, changes)
Author: Lei Mao <dukeleimao@gmail.com>
Date: Thu Aug 14 18:57:08 2025 -0700

Initial commit

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c5e3993
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Test-Repo
\ No newline at end of file

Git stash git stash allows us to temporarily save changes that are not ready to be committed and remove them from the Git repository. In our case, we have already pushed the commit of the changes. So we will have to revert the commit locally for debugging.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git reset --soft HEAD~1
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

We could list the name of changed files using git diff.

1
2
3
4
5
6
$ git diff --name-only --cached
0.cpp
1.cpp
2.cpp
3.cpp
4.cpp

The number of changed files and half of the changed files can be obtained using common bash commands.

1
2
3
count=$(git diff --name-only --cached | wc -l)
first_half=$(( (count + 1) / 2 ))
second_half=$(( count - first_half ))

To stash the first half or the second half of the changed files, we can use the following commands.

1
2
git stash -m "bisection_stashed" -- $(git diff --name-only --cached | head -n $first_half)
git stash -m "bisection_stashed" -- $(git diff --name-only --cached | tail -n $second_half)

After stashing half of the changed files, we will run unit tests to see if the unit tests will pass. If the unit tests fail, the culprit changes must be in the remaining files. Otherwise the culprit changes must be in the stashed files, and we will pop the stash to get the changes back and stash the files that have passed the unit tests. We will continue counting the number of changed files and half of the changed files and stashing half of them until only one file is left, which contains the culprit change.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

$ git stash list
$ count=$(git diff --name-only --cached | wc -l)
$ first_half=$(( (count + 1) / 2 ))
$ second_half=$(( count - first_half ))
$ git stash -m "bisection_stashed" -- $(git diff --name-only --cached | head -n $first_half)
Saved working directory and index state On main: bisection_stashed
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 3.cpp
new file: 4.cpp
$ git stash list
stash@{0}: On main: bisection_stashed
$ # Run unit test to test the second half of the changed files.
$ git stash pop stash@{0}
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

Dropped stash@{0} (e2344fe3b43a8bd2b0dfbb82416e375ac3b5d730)
$ git stash -m "bisection_stashed" -- $(git diff --name-only --cached | tail -n $second_half)
Saved working directory and index state On main: bisection_stashed
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp

$ git stash list
stash@{0}: On main: bisection_stashed
$ # Run unit test to test the first half of the changed files.
$ git stash pop stash@{0}
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

Dropped stash@{0} (5d97a0104195f03ead0e4267f74cd760a821703e)

In practice, it might not be the case that each group of changes only contains one changed file. Then we will have to manually specify groups.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
$ group_0=$(ls 0.cpp 1.cpp 2.cpp)
$ group_1=$(ls 3.cpp 4.cpp)
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

$ git stash -m "bisection_stashed" -- $group_0
Saved working directory and index state On main: bisection_stashed
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 3.cpp
new file: 4.cpp

$ git stash list
stash@{0}: On main: bisection_stashed
$ git stash pop stash@{0}
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

Dropped stash@{0} (286fda539a46c10ffb4e9bcc34246a90e8af7b45)
$ git stash -m "bisection_stashed" -- $group_1
Saved working directory and index state On main: bisection_stashed
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp

$ git stash list
stash@{0}: On main: bisection_stashed
$ git stash pop stash@{0}
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: 0.cpp
new file: 1.cpp
new file: 2.cpp
new file: 3.cpp
new file: 4.cpp

Dropped stash@{0} (c055776963343c797d03e70a0222ff7b968e9c03)
Author

Lei Mao

Posted on

08-17-2025

Updated on

08-17-2025

Licensed under


Comments