summaryrefslogtreecommitdiff
path: root/git-rewrite-branch
blob: 429070ff577e5dffa471ec3209d5731a5563f403 (plain)
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=formtat:'%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 "$@"