summaryrefslogtreecommitdiff
path: root/git-rewrite-branch
blob: b9c6ab3d08807fc094f80d83ef45cc3b9897dda0 (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
#!/bin/bash -eE
#
# Copyright (c) 2012-2013 Luke Shumaker <lukeshu@sbcglobal.net>
#

USAGE='[-h|-v|--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 <in-branch> already
exists, it only runs new commits through the filter.  This way, this can be
used to pull from a remote with a different layout than yours.

This will append "git-rewrite-id" to all commit messages, similar to
"git-svn-id" with `git-svn`.

    -h               Show this text
    -v               Be more verbose
    --svn            Use an existing "git-svn-id" instead of "git-rewrite-id"
                     (in-branch was generated by git-svn)

<filters> is passed directly to `git filter-branch`, and therefore has the same
format.
'
SUBDIRECTORY_OP=Yes
OPTIONS_SPEC=
. git-sh-setup
. git-sh-i18n
require_work_tree_exists

# when $gitmode is true, $ibranch's commits are used as IDs
gitmode=true
tag='git-rewrite-id'

ibranch=''
obranch=''
wbranch=''

panic() {
	echo 'panic: malformed call to internal function' >&2
	exit 1
}

# Usage: verbose arg1 arg2...
# Print the arguments, 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 ]] && $gitmode; 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 ]] && $gitmode; 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 $gitmode=true
		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)
				gitmode=false
				tag='git-svn-id'
				shift
				;;
			-h)
				usage
				return 0
				;;
			-v)
				verbose() { echo "$*"; }
				shift
				;;
			*)
				break
				;;
		esac
	done

	if [[ $# < 3 ]]; then
		usage
		return 1
	fi

	ibranch=$1
	obranch=$2
	shift 2

	local filters=();
	if $gitmode; 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 expansion.
		#
		# 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 $obranch"
		verbose '          :'
		verbose "  o---o---o---o---o $ibranch"
		verbose '       \  :'
		verbose '        `-C---o---o '"$wbranch"
		verbose
		verbose "  C = $commonish"
		verbose
		verbose "  ${cmd[*]}"
		verbose

		"${cmd[@]}"
		git checkout "$obranch"
		git branch -d "$wbranch"
	else
		git branch -m "$wbranch" "$obranch"
	fi
}

main "$@"