]>
Commit | Line | Data |
---|---|---|
1b1dce4b JS |
1 | #!/bin/sh |
2 | # | |
3 | # Copyright (c) 2006 Johannes E. Schindelin | |
4 | ||
5 | # SHORT DESCRIPTION | |
6 | # | |
7 | # This script makes it easy to fix up commits in the middle of a series, | |
8 | # and rearrange commits. | |
9 | # | |
10 | # The original idea comes from Eric W. Biederman, in | |
11 | # http://article.gmane.org/gmane.comp.version-control.git/22407 | |
12 | ||
13 | USAGE='(--continue | --abort | --skip | [--onto <branch>] <upstream> [<branch>])' | |
14 | ||
15 | . git-sh-setup | |
16 | require_work_tree | |
17 | ||
18 | DOTEST="$GIT_DIR/.dotest-merge" | |
19 | TODO="$DOTEST"/todo | |
20 | DONE="$DOTEST"/done | |
21 | STRATEGY= | |
22 | VERBOSE= | |
23 | ||
24 | warn () { | |
25 | echo "$*" >&2 | |
26 | } | |
27 | ||
28 | require_clean_work_tree () { | |
29 | # test if working tree is dirty | |
30 | git rev-parse --verify HEAD > /dev/null && | |
31 | git update-index --refresh && | |
32 | git diff-files --quiet && | |
33 | git diff-index --cached --quiet HEAD || | |
34 | die "Working tree is dirty" | |
35 | } | |
36 | ||
37 | ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION" | |
38 | ||
39 | comment_for_reflog () { | |
40 | case "$ORIG_REFLOG_ACTION" in | |
41 | ''|rebase*) | |
42 | GIT_REFLOG_ACTION="rebase -i ($1)" | |
43 | export GIT_REFLOG_ACTION | |
44 | esac | |
45 | } | |
46 | ||
47 | mark_action_done () { | |
48 | sed -e 1q < "$TODO" >> "$DONE" | |
49 | sed -e 1d < "$TODO" >> "$TODO".new | |
50 | mv -f "$TODO".new "$TODO" | |
51 | } | |
52 | ||
53 | make_patch () { | |
54 | parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null) | |
55 | git diff "$parent_sha1".."$1" > "$DOTEST"/patch | |
56 | } | |
57 | ||
58 | die_with_patch () { | |
59 | make_patch "$1" | |
60 | die "$2" | |
61 | } | |
62 | ||
63 | pick_one () { | |
64 | case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac | |
65 | git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" | |
66 | parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null) | |
67 | current_sha1=$(git rev-parse --verify HEAD) | |
68 | if [ $current_sha1 = $parent_sha1 ]; then | |
69 | git reset --hard $sha1 | |
70 | sha1=$(git rev-parse --short $sha1) | |
71 | warn Fast forward to $sha1 | |
72 | else | |
73 | git cherry-pick $STRATEGY "$@" | |
74 | fi | |
75 | } | |
76 | ||
77 | do_next () { | |
78 | read command sha1 rest < "$TODO" | |
79 | case "$command" in | |
80 | \#|'') | |
81 | mark_action_done | |
82 | continue | |
83 | ;; | |
84 | pick) | |
85 | comment_for_reflog pick | |
86 | ||
87 | mark_action_done | |
88 | pick_one $sha1 || | |
89 | die_with_patch $sha1 "Could not apply $sha1... $rest" | |
90 | ;; | |
91 | edit) | |
92 | comment_for_reflog edit | |
93 | ||
94 | mark_action_done | |
95 | pick_one $sha1 || | |
96 | die_with_patch $sha1 "Could not apply $sha1... $rest" | |
97 | make_patch $sha1 | |
98 | warn | |
99 | warn "You can amend the commit now, with" | |
100 | warn | |
101 | warn " git commit --amend" | |
102 | warn | |
103 | exit 0 | |
104 | ;; | |
105 | squash) | |
106 | comment_for_reflog squash | |
107 | ||
108 | test -z "$(grep -ve '^$' -e '^#' < $DONE)" && | |
109 | die "Cannot 'squash' without a previous commit" | |
110 | ||
111 | mark_action_done | |
112 | failed=f | |
113 | pick_one -n $sha1 || failed=t | |
114 | MSG="$DOTEST"/message | |
115 | echo "# This is a combination of two commits." > "$MSG" | |
116 | echo "# The first commit's message is:" >> "$MSG" | |
117 | echo >> "$MSG" | |
118 | git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG" | |
119 | echo >> "$MSG" | |
120 | echo "# And this is the 2nd commit message:" >> "$MSG" | |
121 | echo >> "$MSG" | |
122 | git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG" | |
123 | git reset --soft HEAD^ | |
124 | author_script=$(get_author_ident_from_commit $sha1) | |
125 | case $failed in | |
126 | f) | |
127 | # This is like --amend, but with a different message | |
128 | eval "$author_script" | |
129 | export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE | |
130 | git commit -F "$MSG" -e | |
131 | ;; | |
132 | t) | |
133 | cp "$MSG" "$GIT_DIR"/MERGE_MSG | |
134 | warn | |
135 | warn "Could not apply $sha1... $rest" | |
136 | warn "After you fixed that, commit the result with" | |
137 | warn | |
138 | warn " $(echo $author_script | tr '\012' ' ') \\" | |
139 | warn " git commit -F \"$GIT_DIR\"/MERGE_MSG -e" | |
140 | die_with_patch $sha1 "" | |
141 | esac | |
142 | ;; | |
143 | *) | |
144 | warn "Unknown command: $command $sha1 $rest" | |
145 | die_with_patch $sha1 "Please fix this in the file $TODO." | |
146 | esac | |
147 | test -s "$TODO" && return | |
148 | ||
149 | HEAD=$(git rev-parse HEAD) | |
150 | HEADNAME=$(cat "$DOTEST"/head-name) | |
151 | rm -rf "$DOTEST" && | |
152 | warn "Successfully rebased and updated $HEADNAME." | |
153 | ||
154 | exit | |
155 | } | |
156 | ||
157 | do_rest () { | |
158 | while : | |
159 | do | |
160 | do_next | |
161 | done | |
162 | test -f "$DOTEST"/verbose && | |
163 | git diff --stat $(cat "$DOTEST"/head)..HEAD | |
164 | exit | |
165 | } | |
166 | ||
167 | while case $# in 0) break ;; esac | |
168 | do | |
169 | case "$1" in | |
170 | --continue) | |
171 | comment_for_reflog continue | |
172 | ||
173 | test -d "$DOTEST" || die "No interactive rebase running" | |
174 | ||
175 | require_clean_work_tree | |
176 | do_rest | |
177 | ;; | |
178 | --abort) | |
179 | comment_for_reflog abort | |
180 | ||
181 | test -d "$DOTEST" || die "No interactive rebase running" | |
182 | ||
183 | HEADNAME=$(cat "$DOTEST"/head-name) | |
184 | HEAD=$(cat "$DOTEST"/head) | |
185 | git symbolic-ref HEAD $HEADNAME && | |
186 | git reset --hard $HEAD && | |
187 | rm -rf "$DOTEST" | |
188 | exit | |
189 | ;; | |
190 | --skip) | |
191 | comment_for_reflog skip | |
192 | ||
193 | test -d "$DOTEST" || die "No interactive rebase running" | |
194 | ||
195 | git reset --hard && do_rest | |
196 | ;; | |
197 | -s|--strategy) | |
198 | shift | |
199 | case "$#,$1" in | |
200 | *,*=*) | |
201 | STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;; | |
202 | 1,*) | |
203 | usage ;; | |
204 | *) | |
205 | STRATEGY="-s $2" | |
206 | shift ;; | |
207 | esac | |
208 | ;; | |
209 | --merge) | |
210 | # we use merge anyway | |
211 | ;; | |
212 | -C*) | |
213 | die "Interactive rebase uses merge, so $1 does not make sense" | |
214 | ;; | |
215 | -v) | |
216 | VERBOSE=t | |
217 | ;; | |
218 | -i|--interactive) | |
219 | # yeah, we know | |
220 | ;; | |
221 | ''|-h) | |
222 | usage | |
223 | ;; | |
224 | *) | |
225 | test -d "$DOTEST" && | |
226 | die "Interactive rebase already started" | |
227 | ||
228 | git var GIT_COMMITTER_IDENT >/dev/null || | |
229 | die "You need to set your committer info first" | |
230 | ||
231 | comment_for_reflog start | |
232 | ||
233 | ONTO= | |
234 | case "$1" in | |
235 | --onto) | |
236 | ONTO=$(git rev-parse --verify "$2") || | |
237 | die "Does not point to a valid commit: $2" | |
238 | shift; shift | |
239 | ;; | |
240 | esac | |
241 | ||
242 | require_clean_work_tree | |
243 | ||
244 | if [ ! -z "$2"] | |
245 | then | |
246 | git show-ref --verify --quiet "refs/heads/$2" || | |
247 | die "Invalid branchname: $2" | |
248 | git checkout "$2" || | |
249 | die "Could not checkout $2" | |
250 | fi | |
251 | ||
252 | HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?" | |
253 | UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" | |
254 | ||
255 | test -z "$ONTO" && ONTO=$UPSTREAM | |
256 | ||
257 | mkdir "$DOTEST" || die "Could not create temporary $DOTEST" | |
258 | : > "$DOTEST"/interactive || die "Could not mark as interactive" | |
259 | git symbolic-ref HEAD > "$DOTEST"/head-name || | |
260 | die "Could not get HEAD" | |
261 | ||
262 | echo $HEAD > "$DOTEST"/head | |
263 | echo $UPSTREAM > "$DOTEST"/upstream | |
264 | echo $ONTO > "$DOTEST"/onto | |
265 | test t = "$VERBOSE" && : > "$DOTEST"/verbose | |
266 | ||
267 | cat > "$TODO" << EOF | |
268 | # Rebasing $UPSTREAM..$HEAD onto $ONTO | |
269 | # | |
270 | # Commands: | |
271 | # pick = use commit | |
272 | # edit = use commit, but stop for amending | |
273 | # squash = use commit, but meld into previous commit | |
274 | EOF | |
275 | git rev-list --no-merges --pretty=oneline --abbrev-commit \ | |
276 | --abbrev=7 --reverse $UPSTREAM..$HEAD | \ | |
277 | sed "s/^/pick /" >> "$TODO" | |
278 | ||
279 | test -z "$(grep -ve '^$' -e '^#' < $TODO)" && | |
280 | die "Nothing to do" | |
281 | ||
282 | cp "$TODO" "$TODO".backup | |
283 | ${VISUAL:-${EDITOR:-vi}} "$TODO" || | |
284 | die "Could not execute editor" | |
285 | ||
286 | git reset --hard $ONTO && do_rest | |
287 | esac | |
288 | shift | |
289 | done |