-
Notifications
You must be signed in to change notification settings - Fork 26
/
git-cms-merge-topic
executable file
·374 lines (352 loc) · 11.7 KB
/
git-cms-merge-topic
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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
#!/bin/bash -e
case `uname` in
Linux) ECHO="echo -e" ;;
*) ECHO="echo" ;;
esac
check_command(){
declare -A ARG_INDICES
ARG_INDICES["cms-merge-topic"]="0 1 2 3 4 5 6 7 8 10"
ARG_INDICES["cms-rebase-topic"]="0 1 2 3 4 5 6 8 9 10"
ARG_INDICES["cms-checkout-topic"]="0 1 2 8 10"
ARG_INDICES["cms-squash-topic"]="0 1 2 3 4 8 10 11 12"
COMMAND_TO_CHECK=$1
IND_TO_CHECK=$2
OPTNAME=$3
if [ -z "$IND_TO_CHECK" ]; then
echo "${ARG_INDICES[$COMMAND_TO_CHECK]}"
# check if index is in allowed list
elif [[ " ${ARG_INDICES[$COMMAND_TO_CHECK]} " =~ " ${IND_TO_CHECK} " ]]; then
return 0
else
$ECHO "Unsupported option $OPTNAME"
exit 1
fi
}
usage() {
COMMAND_NAME=$1
CODE=$2
POS_ARG="[<github-user>:]{<branch>|<pull-request-id>}"
# positional arg is not required for squash
if [ "$COMMAND_NAME" = "cms-squash-topic" ]; then
POS_ARG="[$POS_ARG]"
fi
$ECHO "git $COMMAND_NAME [options] $POS_ARG"
$ECHO
$ECHO "Options:"
$ECHO "-h, --help \tthis help message"
$ECHO
ARGS=()
ARGS[0]="-d, --debug \tenable debug output"
ARGS[1]=" --https \taccess GitHub over https (default)"
ARGS[2]=" --ssh \taccess GitHub over ssh"
ARGS[3]=" --no-backup \tdon't create backup branch"
ARGS[4]=" --backup-name \tspecify suffix for backup branch (default = _backup)"
ARGS[5]="-s, --strategy \tspecify strategy when merging"
ARGS[6]="-X, --strategy-option \tspecify strategy option when merging"
ARGS[7]=" --no-commit \tdo not do the final commit when merging"
ARGS[8]="-o, --old-base \tspecify old base for merge-base or rebase (not used by default)"
ARGS[9]="-n, --new-base \tspecify new base for rebase (default = current branch)"
ARGS[10]="-u, --unsafe \tdo not perform checkdeps at the end\n \t(default: header, python)"
ARGS[11]=" --current \tsquash the current branch"
ARGS[12]="-m, --message \tspecify new message for squashed commit (instead of using prepopulated message from original commits)"
for IND in $(check_command $COMMAND_NAME); do
$ECHO "${ARGS[$IND]}"
done
exit $CODE
}
# colors and formatting
RED='\033[31m'
NORMAL='\033[0m'
# get default protocol from git config, or use "mixed" by default
PROTOCOL=$(git config --get cms.protocol || echo mixed)
if [ "$PROTOCOL" != "https" ] && [ "$PROTOCOL" != "ssh" ] && [ "$PROTOCOL" != "mixed" ]; then
$ECHO "Unsupported value $PROTOCOL in cms.protocol (choose https, ssh, mixed)"
exit 1
fi
DEBUG=0
INITOPTIONS="" # options passed to git cms-init
BACKUP=true
BACKUP_NAME=_backup
COMMAND_NAME=$(basename $0 | sed -e's/^git-//')
if [ "$COMMAND_NAME" != "cms-merge-topic" ]; then
NOMERGE=true
else
INITOPTIONS="--upstream-only"
fi
while [ $# -gt 0 ]; do
case $1 in
# for backward compatibility (now default)
-A|--all-deps)
shift
;;
# "hidden" option to pass to git cms-init
-q|--quiet|-z)
INITOPTIONS="$INITOPTIONS $1"
DEBUG=0
shift
;;
# preserving order from usage()
-d|--debug)
check_command $COMMAND_NAME 0 $1
INITOPTIONS="$INITOPTIONS $1"
DEBUG=1
shift
;;
--https )
check_command $COMMAND_NAME 1 $1
INITOPTIONS="$INITOPTIONS $1"
PROTOCOL=https
shift
;;
--ssh )
check_command $COMMAND_NAME 2 $1
INITOPTIONS="$INITOPTIONS $1"
PROTOCOL=ssh
shift
;;
--no-backup )
check_command $COMMAND_NAME 3 $1
BACKUP=""
shift
;;
--backup-name )
check_command $COMMAND_NAME 4 $1
BACKUP_NAME=$2
shift; shift
;;
-s | --strategy )
check_command $COMMAND_NAME 5 $1
MERGE_STRATEGY="-s $2"
shift; shift
;;
-X | --strategy-option )
check_command $COMMAND_NAME 6 $1
STRATEGY_OPTION="-X $2"
shift; shift
;;
--no-commit )
check_command $COMMAND_NAME 7 $1
NO_COMMIT=--no-commit
shift
;;
-o | --old-base )
check_command $COMMAND_NAME 8 $1
OLD_BASE=$2
shift; shift
;;
-n | --new-base )
check_command $COMMAND_NAME 9 $1
NEW_BASE=$2
shift; shift
;;
-u|--unsafe)
check_command $COMMAND_NAME 10 $1
UNSAFE=true
shift
;;
--current )
check_command $COMMAND_NAME 11 $1
# settings related to using current branch for squash
BRANCH=$(git rev-parse --abbrev-ref HEAD)
LOCAL_BRANCH="$BRANCH"
BRANCH_DEFAULTED=true
UNSAFE=true
shift
;;
-m | --message )
check_command $COMMAND_NAME 12 $1
MESSAGE=$2
shift; shift
;;
-h|--help)
usage $COMMAND_NAME 0;;
-*)
echo "Unknown option $1" ; exit 1 ;;
*)
if [ ! X$BRANCH = X ]; then
echo "Unexpected extra argument $1" ; exit 1
fi
# Handle branch options:
#
# - Generic personal branch or pull request: `<user-name>:<branch-name>|<pull-request-id>`
# - Generic cms-sw branch: `<branch-name>`
# - Pull request: `<pull-request-id>`
GITHUB_USER=cms-sw
BRANCH=$1
LOCAL_BRANCH=$1
# if the branch contains a colon, split the first part to be the github user
if [[ $BRANCH =~ ^.+:.+$ ]]; then
GITHUB_USER=`echo $BRANCH | cut -f1 -d:`
BRANCH=`echo $BRANCH | cut -f2 -d:`
LOCAL_BRANCH=$BRANCH
fi
# if the branch is a number, assume it to be a pull request
if [[ $BRANCH =~ ^[0-9]+$ ]]; then
PULL=$BRANCH
BRANCH=refs/pull/$PULL/head
LOCAL_BRANCH=pull/$PULL
fi
shift
;;
esac
done
if [ "$BRANCH" == "" ]; then
usage $COMMAND_NAME 1
fi
BASH_FULL_VERSION=$((${BASH_VERSINFO[0]} * 10000 + ${BASH_VERSINFO[1]} * 100 + ${BASH_VERSINFO[2]}))
if (( BASH_FULL_VERSION >= 40100 )); then
# bash 4.1 or newer
if [ $DEBUG == 0 ]; then
# send debug messages to /dev/null
exec {debug}> /dev/null
else
# send debug messages to stderr
exec {debug}>&2
# pass the debug option to subcommands
DEBUG_OPT=-d
# enable shell tracing
set -x
fi
else
# bash 4.0 or older
debug=12
if [ $DEBUG == 0 ]; then
# send debug messages to /dev/null
exec 12> /dev/null
else
# send debug messages to stderr
exec 12>&2
# pass the debug option to subcommands
DEBUG_OPT=-d
# enable shell tracing
set -x
fi
fi
TEMP_BRANCH_WORD=$(echo $COMMAND_NAME | cut -d'-' -f2)
TEMP_BRANCH=${TEMP_BRANCH_WORD}-attempt
# initialize the local repository
if [ -z "$CMSSW_BASE" ]; then
echo "CMSSW environment not setup, please run 'cmsenv' before 'git $COMMAND_NAME'."
exit 1
fi
if ! [ -d $CMSSW_BASE/src/.git ]; then
git cms-init $INITOPTIONS
fi
cd $CMSSW_BASE/src
if [ -z "$NEW_BASE" ]; then
NEW_BASE=$(git rev-parse --abbrev-ref HEAD)
fi
git fetch . +HEAD:$TEMP_BRANCH || { echo "You are on a failed $TEMP_BRANCH_WORD branch. Do \"git branch\" and checkout the one you were on."; exit 1; }
if [ "$(git status --porcelain --untracked=no | grep '^[ACDMRU]')" ]; then
$ECHO "${RED}Error:${NORMAL} there are staged but not committed changes on your working tree, please commit or stash them."
exit 1
fi
if [ "$PROTOCOL" = "ssh" ]; then
[email protected]:$GITHUB_USER/cmssw.git
else
REPOSITORY=https://github.com/$GITHUB_USER/cmssw.git
fi
FULL_BRANCH=$GITHUB_USER/$BRANCH
# if squashing current branch, default base is CMSSW release
if [ "$BRANCH_DEFAULTED" = "true" ]; then
FULL_BRANCH=$BRANCH
if [ -z "$OLD_BASE" ]; then
OLD_BASE=$CMSSW_VERSION
fi
# temp branch will not be used
TEMP_BRANCH=
else
# check if the "branch" is actually an annotated tag, and dereference it
COMMIT=`git ls-remote -t $REPOSITORY $BRANCH^{} | cut -c -40`
if [ -z "$COMMIT" ]; then
COMMIT=$BRANCH
fi
# Fetch the branch specified from github and replace merge-attempt with it.
# The + is used to force the merge-attempt branch to be updated.
git fetch -n $REPOSITORY +$COMMIT:$FULL_BRANCH
# Save the name of the current branch.
CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD`
# Attempt a merge in a separate branch
git checkout $TEMP_BRANCH >&${debug}
fi
if [ -n "$OLD_BASE" ]; then
MERGE_BASE_BRANCH=$OLD_BASE
else
MERGE_BASE_BRANCH=$CURRENT_BRANCH
fi
MERGE_BASE=`git merge-base $FULL_BRANCH $MERGE_BASE_BRANCH`
if [ "$BRANCH_DEFAULTED" != "true" ]; then
git cms-sparse-checkout $DEBUG_OPT $MERGE_BASE $FULL_BRANCH
git read-tree -mu HEAD
fi
# optional backup (not for checkout-topic)
if [ "$BACKUP" = "true" ] && [ "$COMMAND_NAME" != "cms-checkout-topic" ]; then
git branch -f ${LOCAL_BRANCH}${BACKUP_NAME} $FULL_BRANCH
fi
# in no-merge case, just checkout a new branch
if [ "$NOMERGE" = "true" ]; then
if [ "$BRANCH_DEFAULTED" != "true" ]; then
git checkout -B $LOCAL_BRANCH $FULL_BRANCH
echo "Created branch $LOCAL_BRANCH to follow $BRANCH from repository $GITHUB_USER"
fi
# now try a rebase if desired
if [ "$COMMAND_NAME" = "cms-rebase-topic" ]; then
if [ -n "$OLD_BASE" ]; then
$ECHO "git rebase $MERGE_STRATEGY $STRATEGY_OPTION --onto $NEW_BASE $OLD_BASE $LOCAL_BRANCH"
git rebase $MERGE_STRATEGY $STRATEGY_OPTION --onto $NEW_BASE $OLD_BASE $LOCAL_BRANCH
else
git rebase $MERGE_STRATEGY $STRATEGY_OPTION $NEW_BASE $LOCAL_BRANCH
fi
# or a squash
elif [ "$COMMAND_NAME" = "cms-squash-topic" ]; then
# save the list of authors
SQUASH_AUTHOR="$(git config --get user.name) <$(git config --get user.email)>"
# git log w/ specified format prints:
# Co-authored-by: Author <email>
# commit raw subject + body (which might also contain Co-authored-by lines, possibly indented)
# sed -n suppresses printouts by default
# then selects lines matching: any number of spaces + "Co-authored-by: " (which is then removed)
# i = case-insensitive, p = print
# finally, sort removes duplicates, and grep removes the current user (who will already be the author of the squash commit)
readarray -t COAUTHORS < <(git log $MERGE_BASE_BRANCH..$FULL_BRANCH --format="Co-Authored-by: %an <%ae>%n%B" | sed -n 's/^ *Co-authored-by: //ip' | sort -u | grep -v "$SQUASH_AUTHOR")
# by default, automatically populate commit message
git reset --hard $MERGE_BASE
git merge --squash "HEAD@{1}"
if [ -n "$MESSAGE" ]; then
git commit -m "$MESSAGE"
else
GIT_EDITOR=true git commit
fi
# amend to include extra authors
if [ ${#COAUTHORS[@]} -gt 0 ]; then
COAUTHOR_MESSAGE=$(printf 'Co-authored-by: %s\n' "${COAUTHORS[@]}")
git commit --amend --message="$(git show --format=%B --no-patch HEAD)" --message="$COAUTHOR_MESSAGE"
fi
fi
# otherwise, perform merge
else
git merge $NO_COMMIT $MERGE_STRATEGY $STRATEGY_OPTION --no-ff -m "Merged $BRANCH from repository $GITHUB_USER with cms-merge-topic" $GITHUB_USER/$BRANCH || { echo "Unable to merge branch $BRANCH from repository $GITHUB_USER." ; exit 1; }
if [ ! X$NO_COMMIT = X ]; then
echo \"--no-commit\" specified: not committing and leaving you on the $TEMP_BRANCH branch.\n Use git-status to check changes. ; exit 0
fi
git checkout $CURRENT_BRANCH
# Add the missing files.
git read-tree -mu HEAD
# This should always be a FF commit.
git merge --ff $TEMP_BRANCH >&${debug}
fi
# Delete the branch used for merge
if [ -n "$TEMP_BRANCH" ]; then
git branch -D $TEMP_BRANCH >&${debug} || true
fi
# Do checkdeps unless not specified.
if [ ! "X$UNSAFE" = Xtrue ]; then
git cms-checkdeps -a -A
fi
# check if topic branch is behind release branch
if [ "$COMMAND_NAME" = "cms-checkout-topic" ] || ( [ "$COMMAND_NAME" = "cms-squash-topic" ] && [ "$BRANCH_DEFAULTED" != "true" ] ); then
NBEHIND=$(git rev-list $LOCAL_BRANCH..$CURRENT_BRANCH | wc -l)
if [ "$NBEHIND" -gt 0 ]; then
$ECHO "Warning: $LOCAL_BRANCH is behind $CURRENT_BRANCH. You may not be able to compile or run."
fi
fi