This repository was archived by the owner on Dec 19, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-mrt
More file actions
executable file
·483 lines (417 loc) · 17.1 KB
/
git-mrt
File metadata and controls
executable file
·483 lines (417 loc) · 17.1 KB
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
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
#!/bin/bash
VERSION="1.5"
# Exit with return code
# $1 - message to output in stderr
# $2 - return code
function die() {
echo "$1" >&2
exit ${2:-1}
}
#declare -A LOG_LEVELS
LOG_LEVELS=([0]="ERROR" [1]="WARN" [2]="INFO" [3]="DEBUG")
VERBOSITY=2
function log() {
if [ "$#" -eq 0 ]; then
die "ASSERT: log() called with no arguments"
elif [ "$#" -eq 1 ]; then
# default to INFO level if not set
local level=2
else
local level=${1}
shift
fi
if [ "$VERBOSITY" -ge "$level" ]; then
printf "[${LOG_LEVELS[$level]}] $@\n"
fi
}
function usage() {
printf "Usage: %s [-v|--version] [-h|--help]
[-m|--monolocation <dir>] [-d|--deploykeyfile <file>]
[-a|--allowdirs] [-p | --pushbranch]
command [<args>]\n" `basename $0`
echo "
Available options:
-v or --version - show version
-h or --help - show this help
-m or --monolocation - local directory where the monorepo will be cloned
-r or --remote - monorepo remote URL (e.g. https://...)
-d or --deploykeyfile - private deploy key to use for monorepo clone
-a or --allowdirs - allow creation of new directories in monorepo on push
-p or --pushbranch - override the name of the monorepo branch used by push
Available commands:
clone <subdir_in_monorepo> - extract a monorepo subdirectory as a local repo
push [subdir_in_monorepo] - push local repo changes as a monorepo branch
pull [subdir_in_monorepo] - update local repo from a monorepo subdirectory
status [subdir_in_monorepo] - show the local repo status compared to the remote"
exit 1
}
function set_script_vars() {
# Primary monorepo branch to use, e.g. main or master
if [ -z "${MONOBRANCH+x}" ]; then
MONOBRANCH="main"
fi
# Clone link - https default
if [ ! -z "${MONOHTTPSLINK}" ]; then
# Use existing definition if present
MONOHTTPSLINK=${MONOHTTPSLINK}
elif [ -d ".git" ] && [ "$(git config --get mrt.git_mrt_repo_url &> /dev/null)" ]; then
# Get from subrepo metadata if available
MONOHTTPSLINK="$(git config --get mrt.git_mrt_repo_url)"
elif [ ! -z "${GIT_MRT_REPO_URL+x}" ]; then
# Use environment variable
MONOHTTPSLINK=${GIT_MRT_REPO_URL}
else
# Use default location
MONOHTTPSLINK=https://github.com/NSLS2/app-deploy-epics.git
fi
# support URL without the suffix ".git"
MONOHTTPSLINK=${MONOHTTPSLINK%.git}.git
log "Using monorepo link ${MONOHTTPSLINK}"
# Location to which monorepo is cloned
if [ -z "${MONOHOME+x}" ]; then
MONOHOME="${HOME}/.monorepo"
fi
log "Using monorepo home ${MONOHOME}"
# Name of the cloned repository
MONODIR="$(echo ${MONOHTTPSLINK} | sed -nE 's|^https://.*/.*/(.*).git$|\1|p')"
[ -z ${MONODIR} ] && die "[FATAL] Failed to determine the monorepo dir name. Wrong URL?"
log "Using monorepo dir name ${MONODIR}"
# Actual path to the local monorepo
MONOPATH=$MONOHOME/$MONODIR
[ -z ${MONOPATH} ] && die "[FATAL] Failed to determine the local monorepo path. Wrong URL?"
log "Using local monorepo path ${MONOPATH}"
# Clone link - when using deploy key
MONOGITLINK="$(echo ${MONOHTTPSLINK} | sed -nE 's|^https://([^/]*)/(.*)$|git@\1:\2|p')"
[ -z ${MONOGITLINK} ] && die "[FATAL] Failed to construct monorepo git link. Wrong URL?"
log "Using monorepo git link ${MONOGITLINK}"
}
function get_valid_subdirectory() {
local subdir="$1"
local _subdirectory="$1"
[ -d ".git" ] && basepath="$(git config --get mrt.basepath)"
if [ $# -ge 2 ]; then
_subdirectory="$2"
log "Using the specified location as the monorepo subdirectory"
elif [ ! -z "$basepath" ]; then
subdir=${basepath}/${PWD##*/}
log "The monorepo subdirectory is constructed from the file .git/config"
elif [ ! -z "${GIT_MRT_BASE_PATH+x}" ]; then
subdir=$GIT_MRT_BASE_PATH/${PWD##*/}
log "The monorepo subdirectory is constructed from the variable GIT_MRT_BASE_PATH"
else
die "[FATAL] Cannot determine the monorepo subdirectory.
Must specify a subdirectory, Or type 'git config mrt.basepath base_path_in_monorepo',
Or setup the environment variable GIT_MRT_BASE_PATH"
fi
# validate subdirectory by removing extra slashes; /acc/diag//diag-mc/ --> acc/diag/diag-mc
subdir="$(echo "$subdir" | sed -E 's://*:/:g; s:^/?::g; s:/*$::g')"
log "The monorepo subdirectory is validated to $subdir"
# the eval statement below returns/sets the caller's variable subdirectory
eval $_subdirectory="'$subdir'"
}
function check_uncommited_changes () {
log "Checking for uncommited changes in local subrepo"
num_uncommited_changes=$(git status -s --untracked-files=no | wc -l)
if [ "$num_uncommited_changes" != "0" ]; then
git status --untracked-files=no
die "[FATAL] $PWD is a git repository but has uncommited changes!
Commit them first before pushing or pulling"
fi
}
function sparse_pull_mono() {
# $1 - subdirectory to sparse-checkout
log "Creating and validating local monorepo"
# Remember original dir
pushd ${PWD} &> /dev/null
# have to install the python script 'git-filter-repo' for the command 'git filter-repo'
git filter-repo --version &> /dev/null
[ $? -ne 0 ] && die "[FATAL] Please install the script 'git-filter-repo' in your \$PATH
(i.e. /usr/local/bin/git-filter-repo)"
# Make sure that monorepo home dir exists
if [ ! -d $MONOHOME ]; then
mkdir -p $MONOHOME
[ ! -d $MONOHOME ] && die "[FATAL] Failed to create $MONOHOME"
fi
cd $MONOHOME
if [ -d $MONODIR ]; then
# Sanity check in case if local monorepo dir already exists
if [ ! -d $MONODIR/.git ]; then
die "[FATAL] $MONOPATH exists but is not a git repository, determine the cause manually"
fi
fi
# Clone the monorepo if it is absent
if [ ! -d $MONODIR ]; then
#local gceargs="--filter=blob:none --sparse"
local gceargs="--sparse"
if [ ! "$DEPLOYKEYFILE" ]; then
log "Cloning monorepo from $MONOHTTPSLINK"
git clone $gceargs $MONOHTTPSLINK
else
log "Cloning monorepo from $MONOGITLINK"
git clone -c core.sshCommand="/usr/bin/ssh -i $DEPLOYKEYFILE" $gceargs $MONOGITLINK
fi
if [ $? -ne 0 ]; then
die "[FATAL] Monorepo clone failed"
fi
fi
# since the first step of all operations (clone, pull, push) is calling 'sparse_pull_mono', it
# makes sense clean-up only needs to be done here.
cleanup_monorepo
log "Performing monorepo sparse checkout"
git sparse-checkout set $1
[ $? -ne 0 ] && die "[FATAL] Monorepo sparse-checkout failed"
log "Checking for monorepo updates"
git pull -q origin $MONOBRANCH
[ $? -ne 0 ] && die "[FATAL] Monorepo pull failed"
# Restore original dir
popd &> /dev/null
}
function cleanup_monorepo() {
log "Cleaning up the local monorepo"
cd $MONOPATH
git checkout -q $MONOBRANCH
git branch | grep -v "* $MONOBRANCH" | xargs git branch -D &> /dev/null
git remote | grep -v origin | xargs git remote rm &> /dev/null
git remote prune origin
}
function extract_subdirectory() {
# $1: $subdirectory
[ -z $1 ] && die "[FATAL] Subdirectory argument is not specified"
log "Extracting the subdirectory into the temporary monorepo branch 'filter-repo'"
cd $MONOPATH
git checkout -b filter-repo || die "[FATAL] Failed to create the branch 'filter-repo'"
# Rewrite the history of $subdirectory on the temporary branch 'filter-repo' created above
# '--prune*': remove merge commits "Merge pull request ..." to get a clean history for the subrepo
# '--refs filter-repo': keep the branch 'filter-repo' (otherwise it is deleted) after rewriting
extra_args="--prune-empty=always --prune-degenerate=always"
git filter-repo --force $extra_args --subdirectory-filter "$1" --refs filter-repo
[ $? -ne 0 ] && die "[FATAL] Failed to perform filter-repo on $1"
#gitk
log "Removing 'Merge remote-tracking branch ...' to get a clean history for the subrepo"
git log -1 --oneline | grep "Merge remote-tracking branch 'temp/main' into"
if [ $? -eq 0 ]; then
#'git reset --hard HEAD^' does not work because the HEAD has two parents.
# So get the latest new commit as the parent, then do 'git reset ...'
local new_parent=( $(git log --oneline --skip 1 -1) )
git reset --merge ${new_parent[0]}
fi
}
function update_subrepo() {
[ -z $1 ] && die "[FATAL] Subrepopath argument is not specified"
log "Pulling changes from the branch 'filter-repo' into the subrepo"
cd $1
# '--rebase' solves the fast-forward problem when users forget 'git-mrt pull' before 'git-mrt push'
git pull $MONOPATH filter-repo --rebase
[ $? -ne 0 ] && die "[FATAL] Pull in the subrepo failed"
}
function update_basepath() {
[ $# -ne 2 ] && die "ASSERT: missing arguments in update_basepath()"
log "Updating subrepo basepath metadata"
#cd $subrepopath
cd $1
local subdir="$(dirname $2)"
local basepath="$(git config --get mrt.basepath)"
if [ -z "$basepath" ]; then
git config mrt.basepath $subdir
else
if [ "$basepath" != "$subdir" ]; then
die "[FATAL] The subrepo basepath is ${basepath} but expecting ${subdir}.
To update, run: git config mrt.basepath ${subdir}"
fi
fi
}
function mrt__clone() {
if [ "$1" ]; then
local subdirectory=$1
else
die 'ASSERT: mrt__clone() is called without subdirectory argument being specified'
fi
log "Preparing to extract monorepo subdir as local subrepo"
get_valid_subdirectory $1 subdirectory
local subrepopath="$(pwd)/`basename $subdirectory`"
[ -d $subrepopath ] && die "[FATAL] `basename $subdirectory` directory already exists"
sparse_pull_mono $subdirectory
[ ! -d $MONOPATH/$subdirectory ] && die "[FATAL] Monorepo $MONODIR does not contain $subdirectory"
log "Initializing a subrepo"
mkdir $subrepopath || die "[FATAL] Failed to create the subrepo dir"
cd $subrepopath
git init -q -b main
extract_subdirectory $subdirectory
update_subrepo $subrepopath
log "Writing subrepo metadata"
cd $subrepopath
git config mrt.basepath "$(dirname $subdirectory)"
log "Local subrepo clone complete"
}
function mrt__pull() {
log "Preparing to pull local subrepo updates from the monorepo"
local subrepopath=$(pwd)
[ ! -d ".git" ] && die "[FATAL] $subrepopath is not a git repository"
check_uncommited_changes
get_valid_subdirectory $1 subdirectory
sparse_pull_mono $subdirectory
[ ! -d $MONOPATH/$subdirectory ] && die "[FATAL] Monorepo $MONODIR does not contain $subdirectory"
extract_subdirectory $subdirectory
update_subrepo $subrepopath
#gitk
update_basepath $subrepopath $subdirectory
log "Local subrepo pull complete"
}
function mrt__push() {
log "Preparing to push local subrepo updates to the upstream monorepo"
local subrepopath=$(pwd)
[ ! -d ".git" ] && die "[FATAL] $subrepopath is not a git repository"
log "Determining the relevant local subrepo branch and the monorepo subdirectory"
check_uncommited_changes
local subrepobranch=$(git rev-parse --abbrev-ref HEAD)
get_valid_subdirectory $1 subdirectory
sparse_pull_mono $subdirectory
# Check if the containing subdir is already present in the index
log "Ensuring that no intermediate subdirs will be created unintentionally by the push"
cd $MONOPATH
git ls-files --error-unmatch $(dirname $subdirectory) &> /dev/null
if [ $? -eq 1 ] && [ ! -n "$ALLOWDIRS" ]; then
die "[FATAL] Performing the push would create a new intermediate monorepo subdir.
Really wanna push? Repeat your command but with '-a', i.e. '$0 push $subdirectory -a'
Or create '$(dirname $subdirectory)' subdir in the upstream monorepo directly"
fi
log "Rewriting local subrepo to a temporary repo with git filter-repo"
local tempdir="$(mktemp -d -t "${subdirectory##*/}.XXXX")"
git clone $subrepopath $tempdir
cd $tempdir
local extra_args="--replace-refs delete-no-add --preserve-commit-hashes"
git filter-repo $extra_args --force --to-subdirectory-filter $subdirectory
[ $? -ne 0 ] && die "[FATAL] git filter-repo failed to re-write history"
#gitk
log "Switching the local monorepo to a branch to be pushed"
cd $MONOPATH
if [ -z "${PUSHBRANCH+x}" ]; then
local monorepobranch=$subdirectory/$subrepobranch
else
local monorepobranch="${PUSHBRANCH}"
fi
git checkout -b "$monorepobranch" || die "[FATAL] branch switch failed, check subdirectory name"
#gitk
# If the branch exists on remote, rebase on top of it to absorb changes
# This allows multiple different pushes to the same branch by using --pushbranch
git ls-remote --exit-code --heads origin "${monorepobranch}" &> /dev/null
if [ $? -eq 0 ]; then
git pull --rebase -q origin "${monorepobranch}"
[ $? -ne 0 ] && die "[FATAL] Failed to rebase on origin branch ${monorepobranch}"
fi
log "Merging the temporary repo to the local monorepo branch"
git remote add temp $tempdir
git fetch temp
local merge_args="--allow-unrelated-histories --no-edit"
# DO NOT push if 'Already up to date.'
git merge $merge_args temp/$subrepobranch | xargs | grep "Already up to date"
[ $? -eq 0 ] && die "[INFO] No pushing because of already up to date"
# Have to merge again?
git merge $merge_args temp/$subrepobranch
if [ $? -ne 0 ]; then
log "Aborting merge"
git merge --abort
die "[FATAL] Merge failed - your local subrepo might be out of sync, try pull"
fi
log "Pushing local monorepo changes to upstream"
git push --set-upstream origin $monorepobranch
[ $? -ne 0 ] && die "[FATAL] Push to upstream failed"
update_basepath $subrepopath $subdirectory
log "Local subrepo push complete"
}
function mrt__status() {
local subrepopath=${PWD}
[ ! -d ".git" ] && die "[FATAL] ${subrepopath} is not a git repository"
get_valid_subdirectory $1 subdirectory
sparse_pull_mono $subdirectory
if [ ! -d $MONOPATH/$subdirectory ]; then
printf "\nThe remote monorepo is: ${MONOHTTPSLINK}\n"
die "The remote monorepo does not contain the subdirectory $subdirectory"
fi
local branch=$(git rev-parse --abbrev-ref HEAD)
local localcount=$(git rev-list --count ${branch})
local tempdir="$(mktemp -d -t "${subdirectory##*/}.XXXX")"
cd $tempdir
mrt__clone $subdirectory &> /dev/null
cd $subrepopath
git remote add monorepo $tempdir/${subdirectory##*/}
git fetch monorepo
git branch -u monorepo/main
git status | sed 's/git push/git-mrt push/g' | sed 's/git pull/git-mrt pull/g'
git remote remove monorepo
}
function mrt_main() {
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--version)
echo "git-mrt version $VERSION"
exit 0
;;
-m|--monolocation)
if [ "$2" ]; then
MONOHOME=$2
shift 2
else
die 'ERROR: -m|--monolocation requires a non-empty option argument'
fi
;;
-r|--remote)
if [ "$2" ]; then
MONOHTTPSLINK=$2
shift 2
else
die 'ERROR: -r|--remote requires a non-empty option argument'
fi
;;
-d|--deploykeyfile)
if [ "$2" ]; then
DEPLOYKEYFILE=$2
shift 2
else
die 'ERROR: -d|--deploykeyfile requires a non-empty option argument'
fi
;;
-a|--allowdirs)
ALLOWDIRS=1
shift
;;
-p|--pushbranch)
if [ "$2" ]; then
PUSHBRANCH=$2
shift 2
else
die 'ERROR: -p|--pushbranch requires a non-empty option argument'
fi
;;
clone|pull|push|status)
if [ -z ${cmdname+x} ]; then
cmdname=$1
shift
else
die "ERROR: '$1' command requested but '$cmdname' was already specified"
fi
;;
-*|--*)
log 0 "Unknown option '$1'"
usage
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
if [ -z ${cmdname+x} ]; then
log 0 "No command is specified"
usage
fi
set_script_vars
"mrt__$cmdname" "$@"
}
mrt_main $@