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
|
#!/bin/bash
#
# Copyright (c) 2012-2013, 2015 Luke Shumaker <lukeshu@sbcglobal.net>
#
set -eE
USAGE='[-h|-v|--no-tag|--tag=tag|--svn] <in-branch> <out-branch> <filters>...'
LONG_USAGE='Like filter-branch, but can be used to update branches.
This creates or updates <out-branch> from <in-branch>. If <out-branch> already
exists, it only runs new commits through the filter. This way, this can be
used to pull from a remote repository with a different layout than yours.
New commits are detected by examining a "tag" in the commit messages of
<out-branch> that can be used to tie those commits to corresponding commits in
<in-branch>. By default, the tag is "git-rewrite-id: " followed by the commit
hash of the corresponding commit; similar to the "git-svn-id" appended by
`git-svn`. However, if <in-branch> already has suitable tags in the commit
messages (such as "git-svn-id"), then that can be used, and the commit message
does not need to be edited.
-h Show this text
-v Be more verbose
--tag=<tag> Use the exiting <tag> as found in <in-branch>, instead of
the commit hash.
--no-tag Disregard a previous "--tag="; set "git-rewrite-id" to
the commit hash.
--svn An alias for "--tag=git-svn-id" (use if <in-branch> was
generated by `git-svn`).
<filters> is passed directly to `git filter-branch`, and therefore has the same
format.
'
SUBDIRECTORY_OK=Yes
OPTIONS_SPEC=
. git-sh-setup
. git-sh-i18n
require_work_tree_exists
# If $hastag is false, $ibranch's commit hashes are used as IDs
hastag=false # whether ibranch already has tags
tag='git-rewrite-id'
ibranch=''
obranch=''
wbranch=''
panic() {
echo 'panic: malformed call to internal function' >&2
exit 1
}
# Usage: verbose format arg1 arg2...
# Print the printf string, but only if in verbose mode.
# Verbose mode works by redefining this function during option parsing if -v is
# encountered.
verbose() {
:
}
################################################################################
# Usage: id2commit $branch $id
# Returns the commit on $branch with $id.
id2commit() {
[[ $# == 2 ]] || panic
local branch=$1
local id=$2
if [[ $branch == $ibranch ]] && ! $hastag; then
printf '%s\n' "$id"
else
git log "$branch" --pretty=format:'%H' --grep "${tag}: ${id}"
fi
}
# Usage: commit2id $branch $commit
# Returns the id of $commit, which is on $branch.
commit2id() {
[[ $# == 2 ]] || panic
local branch=$1
local commit=$2
if [[ $branch == $ibranch ]] && ! $hastag; then
printf '%s\n' "$commit"
else
git log "$branch" -n1 --pretty=format:'%B' "$commit" | sed -n "s|^\s*${tag}: ||p"
fi
}
# commit2commit
# Usage: c2c $from $to $commit
# Returns the commit on branch $to that has the same id as $commit, which is
# on branch $from.
c2c() {
[[ $# == 3 ]] || panic
local from=$1
local to=$2
local commit=$3
if [[ "$from" == "$to" ]]; then
# optimization
# also, properly normalizes $ibranch when $hastag=false
git log "$commit" -n1 --pretty=format:'%H'
else
id2commit "$to" "$(commit2id "$from" "$commit")"
fi
}
################################################################################
main() {
# Parse command line arguments #########################################
while true; do
case "${1:-}" in
--svn) hastag=true; tag='git-svn-id' ; shift 1 ;;
--tag=*) hastag=true; tag="${1#*=}" ; shift 1 ;;
--tag) hastag=true; tag=$2 ; shift 2 ;;
--no-tag) hastag=false; tag='git-rewrite-id'; shift 1 ;;
-h) usage; return 0;;
-v)
verbose() {
local fmt=$1
shift
printf "${fmt}\n" "$@"
}
shift
;;
*) break;;
esac
done
if [[ $# < 3 ]]; then
usage
return 1
fi
ibranch=$1
obranch=$2
shift 2
local filters=();
if ! $hastag; then
# Add a filter to append the id to the commit message.
# This is a little confusing to read because of double quoting.
filters=(--msg-filter "sed '\$a'\"${tag}: \${GIT_COMMIT}\"")
# Here it is annotated; "@" indicates characters to be taken
# literally, and "^" indicates string interpolation.
#
# Argument to git-filter-branch:
# sed '$a'"${tag}: ${GIT_COMMIT}"
# @@@@@@@@@^^^^^^@@@@@@@@@@@@@@@@
# Argument to sed:
# $a${tag}: ${GIT_COMMIT}
# @@^^^^^^@@^^^^^^^^^^^^^
fi
filters+=("$@")
# Main #################################################################
wbranch="$obranch.tmp"
local revlist
local rebase
if git checkout "$obranch" -- 2>/dev/null; then
# obranch exists, update it
echo 'Updating existing rewritten branch...'
local icommit="$(c2c "$ibranch" "$ibranch" "$ibranch")"
local ocommit="$(c2c "$obranch" "$ibranch" "$obranch")"
if [[ "$icommit" == "$ocommit" ]]; then
echo "Nothing to do"
return 0
fi
local common="$(c2c "$obranch" "$ibranch" "$obranch")"
if c2c "$ibranch" "$ibranch" "${common}^" &>/dev/null; then
revlist="$(c2c "$obranch" "$ibranch" "$obranch")^..${wbranch}"
else
# There is only one commit from $ibranch in $obranch
revlist="$wbranch"
fi
rebase=true
else
# obranch does not exist, create it
echo 'Creating new rewritten branch...'
revlist=$wbranch
rebase=false
fi
git checkout "$ibranch"
git branch -D "$wbranch" 2>/dev/null || true
git checkout -b "$wbranch"
git filter-branch -f "${filters[@]}" "$revlist"
if $rebase; then
# rebase the changes in wbranch onto obranch
echo 'Rebasing rewrites onto existing branch...'
local wcommit="$(c2c "$wbranch" "$ibranch" "$wbranch")"
if [[ "$wcommit" == "$ocommit" ]]; then
echo "Nothing to do"
return 0
fi
local commonish="$(c2c "$obranch" "$wbranch" "$obranch")"
cmd=(git rebase --onto "$obranch" "$commonish" "$wbranch")
verbose ''
verbose ' o---o---o %s' "$obranch"
verbose ' :'
verbose ' o---o---o---o---o %s' "$ibranch"
verbose ' \ :'
verbose ' `-C---o---o %s' "$wbranch"
verbose ''
verbose ' C = %s' "$commonish"
verbose ''
verbose ' %s' "$(printf '%q ' "${cmd[@]}")"
verbose ''
"${cmd[@]}"
git checkout "$obranch"
git branch -d "$wbranch"
else
git branch -m "$wbranch" "$obranch"
fi
}
main "$@"
|