-
Notifications
You must be signed in to change notification settings - Fork 0
/
cg-patch
executable file
·360 lines (308 loc) · 10.4 KB
/
cg-patch
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
#!/usr/bin/env bash
#
# Apply a patch from a file, standard input, or a commit
# Copyright (c) Petr Baudis, 2005
#
# Apply a patch in a manner similar to the 'patch' tool, but while also
# handling the Git extensions to the diff format: file mode changes, file
# renames, distinguishing removal of files and empty files, etc. Newly
# created files are automatically `cg-add`ed and removed files are
# `cg-rm`oved.
#
# `cg-patch` can also automatically commit the applied patches and extract
# patches from existing commits, therefore effectively enabling you to
# 'cherrypick' certain changes from a different branch.
#
# In comparison with the 'git-apply' tool, `cg-patch` will also apply
# fuzzy patches.
#
# OPTIONS
# -------
# -c:: Automatically commit the patch
# Automatically extract the commit message and authorship information
# (if provided) from the patch and commit it after applying it
# successfully.
#
# -C COMMIT:: Cherry-pick the given commit
# Instead of applying a patch from stdin, apply and commit the patch
# introduced by the given commit. This is basically an extension of
# `cg-commit -c`, it also applies the commit diff.
#
# In combination with '-R', this does the opposite - it will revert
# the given commit and then try to commit a revert commit - it will
# prefill the headline and open the commit editor for you to write
# further details.
#
# Note that even though this is functionally equivalent to the
# cherry-picking concept present in other version control systems,
# this does not play very well together with regular merges and if
# you both cherry-pick and merge between two branches, the picking
# may increase the number of conflicts you will get when merging.
#
# -d DIRNAME:: Apply all patches in directory
# Instead of applying a patch from stdin, apply and separately commit
# all patches in the specified directory. This can be used to import
# a range of patches made by `cg-mkpatch -d`. Implies '-c'.
#
# -e:: Edit commit message before committing
# Edit the commit message before performing a commit. Makes sense
# only with '-c' or other options implying '-c' (e.g. '-m').
#
# -m:: Apply patches in a mailbox
# Applies series of patches in a mailbox fed to the command's
# standard input. Implies '-c'.
#
# -pN:: Strip path to the level N
# Strip path of filenames in the diff to the level N. This works
# exactly the same as in the `patch` tool except that the default
# strip level is not infinite but 1 (or more if you are in a
# subdirectory; in short, `cg-diff | cg-patch -R` and such always
# works).
#
# -R:: Apply in reverse
# Apply the patch in reverse (therefore effectively unapply it).
# Implies '-e' except when the input is not a tty.
#
# --resolved:: Resume -d or -m after conflicts were resolved
# In case the patch series application failed in the middle and
# you resolved the situation, running cg-patch with with the '-d' or '-m'
# argument as well as '--resolved' will cause it to pick up where it
# dropped off and go on applying. (This includes committing the failed
# patch; do not commit it on your own!) (For '-m', you don't need to
# feed the mailbox on stdin anymore.)
#
# -s, --signoff[=STRING]:: Automatically append a sign off line
# Add Signed-off-by line at the end of the commit message when
# autocommitting (-c, -C, -d or -m). Optionally, specify the exact name
# and email to sign off with by passing:
# `--signoff="Author Name <[email protected]>"`.
#
# Takes the diff on stdin (unless specified otherwise).
# Testsuite: TODO
USAGE="cg-patch [-c] [-C COMMIT] [-pN] [-R] [-m | -d DIR] [OTHER_OPTIONS] < PATCH"
. "${COGITO_LIB}"cg-Xlib || exit 1
lookover_patch()
{
local file="$1" where="$2"
local author="$(sed -n '/^\(---\|-- \)$/,$p' < "$file" | sed -n '/^author /p')"
[ "$author" ] || warn "no author info found$where, assuming your authorship"
eval "$(echo "$author" | pick_author)"
}
commit_patch()
{
local file="$1"
local -a ciargs=()
[ -z "$signoff" ] || ciargs[${#ciargs[@]}]="$signoff"
[ -z "$edit" ] || ciargs[${#ciargs[@]}]="$edit"
# FIXME: -e is broken, it won't pre-fill the message
sed '/^\(---\|-- \|diff --git .*\)$/,$d' < "$file" | cg-commit "${ciargs[@]}"
}
resume_filter()
{
sed "0,/\/$(echo "$lastpatch" | sed 's#/#\\/#g')$/d"
}
parse_mail_info()
{
local patch="$1"
while read line; do
case $line in
Author:*)
export GIT_AUTHOR_NAME="${line#* }";;
Email:*)
export GIT_AUTHOR_EMAIL="${line#* }";;
Date:*)
export GIT_AUTHOR_DATE="${line#* }";;
Subject:*)
mi_subj="${line#* }";;
esac
done <"$resume/i/$patch"
}
# Assuming that parse_mail_info() has been already ran on the patch.
commit_mail_patch()
{
local patch="$1"
local -a ciargs=()
[ -z "$signoff" ] || ciargs[${#ciargs[@]}]="$signoff"
[ -z "$edit" ] || ciargs[${#ciargs[@]}]="$edit"
cg-commit -m"$mi_subj" -M"$resume/m/$patch" "${ciargs[@]}"
}
applyargs=()
commitid=
commit=
mbox=
commitdir=
strip=$((1+$(echo "$_git_relpath" | tr -cd / | wc -c)))
reverse=
resolved=
signoff=
edit=
while optparse; do
if optparse -C=; then
commitid="$(cg-object-id -c "$OPTARG")" || exit 1
commitparent="$(cg-object-id -p "$commitid")" || exit 1
[ -z "$commitparent" ] && die "cannot pick initial commit"
[ "$(echo "$commitparent" | wc -l)" -gt 1 ] &&
die "refusing to pick merge commits"
elif optparse -c; then
commit=1
elif optparse -d=; then
commitdir="$(echo "$OPTARG" | sed 's,/*$,,')"
[ -d "$commitdir" ] || die "$commitdir: not a directory"
elif optparse -m; then
mbox=1
elif optparse -p=; then
strip="$OPTARG"
[ -n "$(echo "$strip" | tr -d 0-9)" ] &&
die "the -p argument must be numeric"
applyargs[${#applyargs[@]}]="-p$strip"
elif optparse --resolved; then
resolved=1
elif optparse -R; then
reverse=1
applyargs[${#applyargs[@]}]="-R"
elif optparse -s || optparse --signoff; then
[ "$signoff" ] || signoff="--signoff=$(git-var GIT_AUTHOR_IDENT | sed 's/> .*/>/')"
elif optparse --signoff=; then
signoff="--signoff=$OPTARG"
elif optparse -e; then
edit="-e"
else
optfail
fi
done
[ "$resolved" ] && [ -z "$commitdir" ] && [ -z "$mbox" ] &&
die "--resolved can be passed only with -d"
if [ "$commitid" ] || [ "$commit" ] || [ "$commitdir" ] || [ "$mbox" ]; then
[ "$_git_relpath" ] && die "must be ran from project root"
if [ "$commitid" ]; then
[ "$unidiff" ] && die "-u does not make sense here"
[ $strip -ne 1 ] && die "-p does not make sense here"
files="$(mktemp -t gitpatch.XXXXXX)"
git-diff-tree -m -r "$commitparent" "$commitid" | cut -f 2- >"$files"
if local_changes_in "$files"; then
rm "$files"
die "cherry-pick blocked by local changes"
fi
eval "afiles=($(cat "$files" | sed -e 's/"\|\\/\\&/g' -e 's/^.*$/"&"/'))"
rm "$files"
ciargs=()
if ! [ "$reverse" ]; then
ciargs=(-c "$commitid")
else
ciargs=(-m "Revert ${commitid:0:12}")
if tty -s; then
ciargs[${#ciargs[@]}]="-e"
fi
reverse=-R
fi
[ -z "$signoff" ] || ciargs[${#ciargs[@]}]="$signoff"
[ -z "$edit" ] || ciargs[${#ciargs[@]}]="$edit"
if ! cg-diff -p -r "$commitid" | cg-patch "${applyargs[@]}"; then
echo "Cherry-picking $commitid failed, please fix up the rejects." >&2
echo "You can use cg-commit -c $commitid to commit afterwards (that will" >&2
echo "reuse the commit message and authorship); throw in -e to add own comments." >&2
exit 1
fi
cg-commit "${ciargs[@]}" "${afiles[@]}"
fi
if [ "$commit" ]; then
[ "$reverse" ] && die "cannot do -R here"
local_changes &&
die "cannot auto-commit patches when the tree has local changes"
file="$(mktemp -t gitpatch.XXXXXX)"
cat >"$file"
lookover_patch "$file"
cg-patch "${applyargs[@]}" <"$file" || exit 1
commit_patch "$file"
rm "$file"
fi
if [ "$commitdir" ]; then
[ "$reverse" ] && die "cannot do -R here"
resume="$commitdir/.cg-patch-resume"
if [ -s "$resume" ]; then
if [ ! "$resolved" ]; then
echo "cg-patch: previous import in progress" >&2
echo "Use --resolved to resume after conflicts." >&2
echo "Cancel the resume by deleting $resume" >&2
exit 1
fi
echo "Resuming import of $commitdir:"
filter=resume_filter
lastpatch="$(cat "$resume")"
echo "* $lastpatch"
commit_patch "$commitdir/$lastpatch"
rm -f "$resume"
elif local_changes; then
die "cannot auto-commit patches when the tree has local changes"
else
echo "Importing $commitdir:"
filter=cat
fi
find "$commitdir" -name '[0-9]*-*' | sort | "$filter" | \
while read -r file; do
echo "* ${file#$commitdir/}"
lookover_patch "$file" " in $file"
if ! cg-patch "${applyargs[@]}" < "$file"; then
echo "${file#$commitdir/}" > "$resume"
echo "cg-patch: conflicts during import" >&2
echo "Rerun cg-patch with the -d... --resolved arguments to resume after resolving them." >&2
echo "Cancel the resume by deleting $resume" >&2
exit 1
fi
commit_patch "$file"
done
fi
if [ "$mbox" ]; then
[ "$reverse" ] && die "cannot do -R here"
resume="$_git/info/cg-patch-mresume"
if [ -d "$resume" ]; then
if [ ! "$resolved" ]; then
echo "cg-patch: previous import in progress" >&2
echo "Use --resolved to resume after conflicts." >&2
echo "Cancel the resume by deleting $resume" >&2
exit 1
fi
echo "Resuming import of mailbox:"
lastpatch="$(cat "$resume/last")"
parse_mail_info "$lastpatch"
echo
echo "* $lastpatch $mi_subj"
commit_mail_patch "$lastpatch"
rm "$resume/a/$lastpatch"
elif local_changes; then
die "cannot auto-commit patches when the tree has local changes"
else
echo "Importing mailbox:"
mkdir -p "$resume" || exit 1
mkdir -p "$resume/a"
mkdir -p "$resume/i"
mkdir -p "$resume/m"
mkdir -p "$resume/p"
git-mailsplit -o"$resume/a" >/dev/null
fi
for patch in "$resume"/a/*; do
patch="${patch#$resume/a/}"
[ "$patch" = "*" ] && break # Out of patches
echo "$patch" >"$resume/last"
git-mailinfo "$resume/m/$patch" "$resume/p/$patch" \
<"$resume/a/$patch" >"$resume/i/$patch"
parse_mail_info "$patch"
echo
echo "* $patch $mi_subj"
if ! cg-patch "${applyargs[@]}" < "$resume/p/$patch"; then
echo "cg-patch: conflicts during import" >&2
echo "Rerun cg-patch with the -m --resolved arguments to resume after resolving them." >&2
echo "Cancel the resume by deleting $resume" >&2
exit 1
fi
commit_mail_patch "$patch"
rm "$resume/a/$patch"
done
rm -r "$resume"
fi
exit
fi
# We want to run patch in the subdirectory and at any rate protect other
# parts of the tree from inadverent pollution.
[ -n "$_git_relpath" ] && cd "$_git_relpath"
exec git-apply --index --reject "${applyargs[@]}"