rebase: implement --[no-]autostash and rebase.autostash

This new feature allows a rebase to be executed on a dirty worktree or index. It works by creating a temporary "dangling merge commit" out of the worktree and index changes (via 'git stash create'), and automatically applying it after a successful rebase or abort. rebase stores the SHA-1 hex of the temporary merge commit, along with the rest of the rebase state, in either .git/{rebase-merge,rebase-apply}/autostash depending on the kind of rebase. Since $state_dir is automatically removed at the end of a successful rebase or abort, so is the autostash. The advantage of this approach is that we do not affect the normal stash's reflogs, making the autostash invisible to the end-user. This means that you can use 'git stash' during a rebase as usual. When the autostash application results in a conflict, we push $state_dir/autostash onto the normal stash and remove $state_dir ending the rebase. The user can inspect the stash, and pop or drop at any time. Most significantly, this feature means that a caller like pull (with pull.rebase set to true) can easily be patched to remove the require_clean_work_tree restriction. Signed-off-by: Ramkumar Ramachandra <artagnon@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ramkumar Ramachandra committed May 12, 2013 at 17:26 UTC 587947750bd73544a6a99811f0ddfd64e1ff1445
4 files changed +208 -3
Documentation/config.txt
+8
@@ -1867,6 +1867,14 @@ rebase.stat::
1867 rebase.autosquash::
1868 If set to true enable '--autosquash' option by default.
1869
1870 +rebase.autostash::
1871 + When set to true, automatically create a temporary stash
1872 + before the operation begins, and apply it after the operation
1873 + ends. This means that you can run rebase on a dirty worktree.
1874 + However, use with care: the final stash application after a
1875 + successful rebase might result in non-trivial conflicts.
1876 + Defaults to false.
1877 +
1878 receive.autogc::
1879 By default, git-receive-pack will run "git-gc --auto" after
1880 receiving data from git-push and updating refs. You can stop
Documentation/git-rebase.txt
+10
@@ -208,6 +208,9 @@ rebase.stat::
208 rebase.autosquash::
209 If set to true enable '--autosquash' option by default.
210
211 +rebase.autostash::
212 + If set to true enable '--autostash' option by default.
213 +
214 OPTIONS
215 -------
216 --onto <newbase>::
@@ -394,6 +397,13 @@ If the '--autosquash' option is enabled by default using the
397 configuration variable `rebase.autosquash`, this option can be
398 used to override and disable this setting.
399
400 +--[no-]autostash::
401 + Automatically create a temporary stash before the operation
402 + begins, and apply it after the operation ends. This means
403 + that you can run rebase on a dirty worktree. However, use
404 + with care: the final stash application after a successful
405 + rebase might result in non-trivial conflicts.
406 +
407 --no-ff::
408 With --interactive, cherry-pick all rebased commits instead of
409 fast-forwarding over the unchanged ones. This ensures that the
git-rebase.sh
+42 -3
@@ -13,6 +13,7 @@ git-rebase --continue | --abort | --skip | --edit-todo
13 Available options are
14 v,verbose! display a diffstat of what changed upstream
15 q,quiet! be quiet. implies --no-stat
16 +autostash! automatically stash/stash pop before and after
17 onto=! rebase onto given branch instead of upstream
18 p,preserve-merges! try to recreate merges instead of ignoring them
19 s,strategy=! use the given merge strategy
@@ -64,6 +65,7 @@ apply_dir="$GIT_DIR"/rebase-apply
65 verbose=
66 diffstat=
67 test "$(git config --bool rebase.stat)" = true && diffstat=t
68 +autostash="$(git config --bool rebase.autostash || echo false)"
69 git_am_opt=
70 rebase_root=
71 force_rebase=
@@ -143,6 +145,29 @@ move_to_original_branch () {
145 esac
146 }
147
148 +finish_rebase () {
149 + if test -f "$state_dir/autostash"
150 + then
151 + stash_sha1=$(cat "$state_dir/autostash")
152 + if git stash apply $stash_sha1 2>&1 >/dev/null
153 + then
154 + echo "$(gettext 'Applied autostash.')"
155 + else
156 + ref_stash=refs/stash &&
157 + >>"$GIT_DIR/logs/$ref_stash" &&
158 + git update-ref -m "autostash" $ref_stash $stash_sha1 ||
159 + die "$(eval_gettext 'Cannot store $stash_sha1')"
160 +
161 + gettext 'Applying autostash resulted in conflicts.
162 +Your changes are safe in the stash.
163 +You can run "git stash pop" or "git stash drop" it at any time.
164 +'
165 + fi
166 + fi
167 + git gc --auto &&
168 + rm -rf "$state_dir"
169 +}
170 +
171 run_specific_rebase () {
172 if [ "$interactive_rebase" = implied ]; then
173 GIT_EDITOR=:
@@ -153,8 +178,7 @@ run_specific_rebase () {
178 ret=$?
179 if test $ret -eq 0
180 then
156 - git gc --auto &&
157 - rm -rf "$state_dir"
181 + finish_rebase
182 fi
183 exit $ret
184 }
@@ -248,6 +272,9 @@ do
272 --stat)
273 diffstat=t
274 ;;
275 + --autostash)
276 + autostash=true
277 + ;;
278 -v)
279 verbose=t
280 diffstat=t
@@ -348,7 +375,7 @@ abort)
375 ;;
376 esac
377 output git reset --hard $orig_head
351 - rm -r "$state_dir"
378 + finish_rebase
379 exit
380 ;;
381 edit-todo)
@@ -487,6 +514,18 @@ case "$#" in
514 ;;
515 esac
516
517 +if test "$autostash" = true && ! (require_clean_work_tree) 2>/dev/null
518 +then
519 + stash_sha1=$(git stash create "autostash") ||
520 + die "$(gettext 'Cannot autostash')"
521 +
522 + mkdir -p "$state_dir" &&
523 + echo $stash_sha1 >"$state_dir/autostash" &&
524 + stash_abbrev=$(git rev-parse --short $stash_sha1) &&
525 + echo "$(eval_gettext 'Created autostash: $stash_abbrev')" &&
526 + git reset --hard
527 +fi
528 +
529 require_clean_work_tree "rebase" "$(gettext "Please commit or stash them.")"
530
531 # Now we are rebasing commits $upstream..$orig_head (or with --root,
t/t3420-rebase-autostash.sh new
+148
@@ -0,0 +1,148 @@
1 +#!/bin/sh
2 +#
3 +# Copyright (c) 2013 Ramkumar Ramachandra
4 +#
5 +
6 +test_description='git rebase --autostash tests'
7 +. ./test-lib.sh
8 +
9 +test_expect_success setup '
10 + echo hello-world >file0 &&
11 + git add . &&
12 + test_tick &&
13 + git commit -m "initial commit" &&
14 + git checkout -b feature-branch &&
15 + echo another-hello >file1 &&
16 + echo goodbye >file2 &&
17 + git add . &&
18 + test_tick &&
19 + git commit -m "second commit" &&
20 + echo final-goodbye >file3 &&
21 + git add . &&
22 + test_tick &&
23 + git commit -m "third commit" &&
24 + git checkout -b unrelated-onto-branch master &&
25 + echo unrelated >file4 &&
26 + git add . &&
27 + test_tick &&
28 + git commit -m "unrelated commit" &&
29 + git checkout -b related-onto-branch master &&
30 + echo conflicting-change >file2 &&
31 + git add . &&
32 + test_tick &&
33 + git commit -m "related commit"
34 +'
35 +
36 +testrebase() {
37 + type=$1
38 + dotest=$2
39 +
40 + test_expect_success "rebase$type: dirty worktree, non-conflicting rebase" '
41 + test_config rebase.autostash true &&
42 + git reset --hard &&
43 + git checkout -b rebased-feature-branch feature-branch &&
44 + test_when_finished git branch -D rebased-feature-branch &&
45 + echo dirty >>file3 &&
46 + git rebase$type unrelated-onto-branch &&
47 + grep unrelated file4 &&
48 + grep dirty file3 &&
49 + git checkout feature-branch
50 + '
51 +
52 + test_expect_success "rebase$type: dirty index, non-conflicting rebase" '
53 + test_config rebase.autostash true &&
54 + git reset --hard &&
55 + git checkout -b rebased-feature-branch feature-branch &&
56 + test_when_finished git branch -D rebased-feature-branch &&
57 + echo dirty >>file3 &&
58 + git add file3 &&
59 + git rebase$type unrelated-onto-branch &&
60 + grep unrelated file4 &&
61 + grep dirty file3 &&
62 + git checkout feature-branch
63 + '
64 +
65 + test_expect_success "rebase$type: conflicting rebase" '
66 + test_config rebase.autostash true &&
67 + git reset --hard &&
68 + git checkout -b rebased-feature-branch feature-branch &&
69 + test_when_finished git branch -D rebased-feature-branch &&
70 + echo dirty >>file3 &&
71 + test_must_fail git rebase$type related-onto-branch &&
72 + test_path_is_file $dotest/autostash &&
73 + ! grep dirty file3 &&
74 + rm -rf $dotest &&
75 + git reset --hard &&
76 + git checkout feature-branch
77 + '
78 +
79 + test_expect_success "rebase$type: --continue" '
80 + test_config rebase.autostash true &&
81 + git reset --hard &&
82 + git checkout -b rebased-feature-branch feature-branch &&
83 + test_when_finished git branch -D rebased-feature-branch &&
84 + echo dirty >>file3 &&
85 + test_must_fail git rebase$type related-onto-branch &&
86 + test_path_is_file $dotest/autostash &&
87 + ! grep dirty file3 &&
88 + echo "conflicting-plus-goodbye" >file2 &&
89 + git add file2 &&
90 + git rebase --continue &&
91 + test_path_is_missing $dotest/autostash &&
92 + grep dirty file3 &&
93 + git checkout feature-branch
94 + '
95 +
96 + test_expect_success "rebase$type: --skip" '
97 + test_config rebase.autostash true &&
98 + git reset --hard &&
99 + git checkout -b rebased-feature-branch feature-branch &&
100 + test_when_finished git branch -D rebased-feature-branch &&
101 + echo dirty >>file3 &&
102 + test_must_fail git rebase$type related-onto-branch &&
103 + test_path_is_file $dotest/autostash &&
104 + ! grep dirty file3 &&
105 + git rebase --skip &&
106 + test_path_is_missing $dotest/autostash &&
107 + grep dirty file3 &&
108 + git checkout feature-branch
109 + '
110 +
111 + test_expect_success "rebase$type: --abort" '
112 + test_config rebase.autostash true &&
113 + git reset --hard &&
114 + git checkout -b rebased-feature-branch feature-branch &&
115 + test_when_finished git branch -D rebased-feature-branch &&
116 + echo dirty >>file3 &&
117 + test_must_fail git rebase$type related-onto-branch &&
118 + test_path_is_file $dotest/autostash &&
119 + ! grep dirty file3 &&
120 + git rebase --abort &&
121 + test_path_is_missing $dotest/autostash &&
122 + grep dirty file3 &&
123 + git checkout feature-branch
124 + '
125 +
126 + test_expect_success "rebase$type: non-conflicting rebase, conflicting stash" '
127 + test_config rebase.autostash true &&
128 + git reset --hard &&
129 + git checkout -b rebased-feature-branch feature-branch &&
130 + test_when_finished git branch -D rebased-feature-branch &&
131 + echo dirty >file4 &&
132 + git add file4 &&
133 + git rebase$type unrelated-onto-branch &&
134 + test_path_is_missing $dotest &&
135 + git reset --hard &&
136 + grep unrelated file4 &&
137 + ! grep dirty file4 &&
138 + git checkout feature-branch &&
139 + git stash pop &&
140 + grep dirty file4
141 + '
142 +}
143 +
144 +testrebase "" .git/rebase-apply
145 +testrebase " --merge" .git/rebase-merge
146 +testrebase " --interactive" .git/rebase-merge
147 +
148 +test_done