Repository navigation
Expand file tree
/
Copy pathset-git-range
More file actions
executable file
·724 lines (682 loc) · 35 KB
/
Copy pathset-git-range
File metadata and controls
executable file
·724 lines (682 loc) · 35 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
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
#!/bin/sh
# Sets the start and end commits for a CI job, as shell variables.
# Works under Azure Pipelines, CircleCI, GitHub Actions, and Travis CI.
#
# This script is *sourced* by the client, which is why its name starts
# with "set-". It is preferable to `git-changes`, which computes the
# same information, but the client `eval`s its output.
# `local` is not in POSIX, but every shell that can source this file supports it.
# shellcheck disable=SC3043
# Typical use:
#
# PLUME_SCRIPTS="/tmp/${USER:-$(id -un)}/plume-scripts"
# if [ -d "$PLUME_SCRIPTS" ] ; then
# git -C "$PLUME_SCRIPTS" pull -q > /dev/null 2>&1
# else
# git clone --depth=1 -q https://github.com/plume-lib/plume-scripts.git "$PLUME_SCRIPTS"
# fi
# CI_DEFAULT_ORGANIZATION=MY-DEFAULT-ORGANIZATION
# . "$PLUME_SCRIPTS"/set-git-range
#
# Alternatively, to use a specific version of this script rather than the latest:
#
# PLUME_SCRIPTS="/tmp/${USER:-$(id -un)}/plume-scripts"
# if [ ! -d "$PLUME_SCRIPTS" ] ; then
# git clone --revision=e768754ffafad4a9f1e26c5305591d4061d890b4 --depth=1 -q https://github.com/plume-lib/plume-scripts.git "$PLUME_SCRIPTS"
# fi
# CI_DEFAULT_ORGANIZATION=MY-DEFAULT-ORGANIZATION
# . "$PLUME_SCRIPTS"/set-git-range
# Either snippet of code sets these variables:
# CI_COMMIT_RANGE: An argument to `git diff` with the range of commit IDs.
# (Don't use it with `git log`, which interprets its argument differently.)
# CI_COMMIT_RANGE_START: The start element of CI_COMMIT_RANGE, a commit ID/SHA.
# CI_COMMIT_RANGE_END: The end element of CI_COMMIT_RANGE, a commit ID/SHA.
# It also sets everything that `set-ci-org-and-branch`, which it sources, sets:
# CI_ORGANIZATION, CI_BRANCH, CI_DEFAULT_BRANCH_NAME, and CI_PULL_JSON.
#
# It sets no other variables and leaves no functions defined. It returns 0 on
# success, or nonzero if it cannot determine the commit range; in the latter
# case CI_COMMIT_RANGE, CI_COMMIT_RANGE_START, and CI_COMMIT_RANGE_END are set
# to the empty string, and the variables that `set-ci-org-and-branch` sets hold
# whatever it determined (they are empty if it did not run at all). The
# failure status is 2, except that `set-ci-org-and-branch`'s own status is
# passed through unchanged -- so 3 means that a GitHub API request failed in a
# way that re-running the job might not repeat.
#
# If called not under CI, or under CI but not in the main or toplevel
# clone, then gets information from the local repository.
# The client configures this script by setting variables rather than by passing
# command-line arguments.
# PLUME_SCRIPTS: the directory that contains these scripts. Optional.
# CI_DEFAULT_ORGANIZATION: if the script cannot determine the organization, use this one. Optional.
# CI_VERBOSE: if non-empty, report progress. Optional.
# CI_DEBUG: if non-empty, report voluminous debugging information. Optional.
# If environment variable GITHUB_PAT or GH_TOKEN is set when this script is
# called, it is used as GitHub Personal Access Token when making GitHub API
# calls. This can avoid "403 rate limit exceeded" failures.
#
# All reporting is to standard error, so that sourcing this script cannot
# corrupt output that the client is producing.
# Requires the `jq` program, either `curl` or `wget`, and the script
# `set-ci-org-and-branch` in ${PLUME_SCRIPTS}.
# PROBLEM: A CI job may itself call "git clone" and then later call this script
# from that clone, so the environment and the current directory can describe
# different repositories. Which one should win depends on what is being
# computed. For a commit ID, the directory wins, because a commit that the
# environment names need not exist in this clone; this script therefore
# verifies $GITHUB_SHA before using it. For the organization, the environment
# wins, because the job's own repository is what the client is asking about,
# and a sibling clone's origin names some other organization. Idea: check the
# directory name.
# When this file is sourced, "$0" is the client's name rather than this file's
# name; so if "$0" is this file's name, the client ran it instead of sourcing it.
case $0 in
*/set-git-range | set-git-range)
echo "set-git-range must be sourced (\`. set-git-range\`), not run." >&2
echo "To run a script instead, use \`eval \"\$(git-changes)\"\`." >&2
exit 2
;;
esac
# Compute the values from scratch, rather than reusing values that the client
# or a previous sourcing of this script left in the environment. Do it here,
# before the early returns below, rather than in `_sgr_set_git_range_body`:
# those returns happen without the body ever running, and they must not leave a
# stale range behind for a client that tests the variables instead of the status.
# The four variables that `set-ci-org-and-branch` sets are cleared here as well:
# the `return 2` below happens before that script is sourced, so nothing else
# would clear them, and they would keep a previous sourcing's values.
CI_COMMIT_RANGE=""
CI_COMMIT_RANGE_START=""
CI_COMMIT_RANGE_END=""
CI_ORGANIZATION=""
CI_BRANCH=""
CI_DEFAULT_BRANCH_NAME=""
CI_PULL_JSON=""
### Organization and branch
# Use private variables rather than defaulting PLUME_SCRIPTS itself, so that
# this script leaves no variable set in a client that did not set one.
# The default is per-user: a clone in a directory that every user can create
# would let whoever creates it first choose the code that everyone else sources.
_sgr_plume_scripts="${PLUME_SCRIPTS:-}"
if [ -z "${_sgr_plume_scripts}" ]; then
# `id -un` because USER is not set under cron and under some CI runners, and
# `id -u` because a user need not have a passwd entry. `|| :` because a
# command substitution whose command fails would, under a client's `set -e`,
# exit the client's shell before the diagnostic below could be printed.
_sgr_user="${USER:-$(id -un 2> /dev/null || :)}"
_sgr_user="${_sgr_user:-$(id -u 2> /dev/null || :)}"
if [ -z "${_sgr_user}" ]; then
# Do not fall back to a directory under /tmp that lacks a per-user
# component; the client must say where the scripts are.
echo "set-git-range: cannot determine your user name, so cannot guess where these scripts are." >&2
echo "Set variable PLUME_SCRIPTS to the directory that contains these scripts." >&2
unset _sgr_plume_scripts _sgr_user
return 2
fi
_sgr_plume_scripts="/tmp/${_sgr_user}/plume-scripts"
unset _sgr_user
fi
if [ ! -r "${_sgr_plume_scripts}/set-ci-org-and-branch" ]; then
echo "set-git-range: cannot read ${_sgr_plume_scripts}/set-ci-org-and-branch." >&2
echo "Set variable PLUME_SCRIPTS to the directory that contains these scripts." >&2
unset _sgr_plume_scripts
return 2
fi
# Source it here rather than within a function, so that its `return` cannot be
# mistaken for a return from that function. It saves and restores the client's
# shell options itself, and it sets CI_DEFAULT_BRANCH_NAME and CI_PULL_JSON,
# which the commit-range code below uses.
# `|| ...` rather than a bare command, so that a client's `set -e` does not
# make the shell exit before the cleanup below runs.
_sgr_status=0
# shellcheck source=set-ci-org-and-branch
. "${_sgr_plume_scripts}"/set-ci-org-and-branch || _sgr_status=$?
unset _sgr_plume_scripts
if [ "$_sgr_status" -ne 0 ]; then
# Unset the variable and return in a single command, because unsetting it
# afterward would lose the status.
eval "unset _sgr_status; return ${_sgr_status}"
fi
unset _sgr_status
### Functions
# Copies standard input to standard output, with the value of each variable
# whose name suggests a credential replaced. The debug dump would otherwise
# write secrets into the CI log, which is often readable by anyone who can see
# the job. Matching by name pattern rather than by an allowlist of names also
# covers secrets that this script does not know about; GitHub Actions, for
# example, defines $ACTIONS_RUNTIME_TOKEN in every job and does not mask it.
# A credential can also hide in the userinfo of a URL, under a variable whose
# name suggests nothing -- a job that cloned with a token has
# "https://USER:[email protected]/org/repo" in `remote.origin.url` -- so strip
# that too, wherever in a line it appears.
# `set-ci-org-and-branch` has its own copy of this function, because it unsets
# its functions before this script's body runs.
_sgr_redact_tokens() {
awk '
/^[A-Za-z_][A-Za-z0-9_]*=/ {
name = $0
sub(/=.*/, "", name)
if (toupper(name) ~ /TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|APIKEY|API_KEY|PRIVATE_KEY|_PAT$|^PAT$/) {
print name "=<redacted>"
next
}
}
# Excluding whitespace and quotes from the userinfo keeps a match from
# spanning a URL and a later "@" in unrelated text, which would delete
# everything between them. The userinfo also ends at the first "/", "?",
# or "#", so an "@" in a path, query, or fragment is not a credential.
{ gsub(/:\/\/[^\/?#[:space:]"'\'']*@/, "://<redacted>@"); print }
'
}
# Returns either `git rev-parse HEAD^1` if HEAD^1 exists, or `git rev-parse HEAD`.
# HEAD^1 does not exist if the clone was created with `git clone --depth=1`.
_sgr_head1() {
if git rev-parse --verify --quiet HEAD^1 > /dev/null 2>&1; then
git rev-parse HEAD^1
else
git rev-parse HEAD
fi
}
# Converts a shallow clone into a complete one.
_sgr_unshallow() {
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
git fetch --unshallow origin
fi
}
# Fetches $base_branch, the branch that a pull request is against, and sets
# $base_tip to its commit. Does nothing on a second call, because fetching a
# branch fetches its complete history, so once is enough.
# $base_branch, $base_branch_fetched, and $base_tip are locals of the caller;
# `local` is dynamically scoped, so this function reads and writes the caller's.
_sgr_fetch_base_branch() {
if [ -z "$base_branch_fetched" ] && [ -n "$base_branch" ]; then
base_branch_fetched="true"
# Test the fetch's status rather than reading FETCH_HEAD unconditionally: a
# fetch that fails before writing FETCH_HEAD (as when the network is down)
# leaves the previous fetch's commit there, which would silently yield a
# commit range computed from an unrelated branch.
if git fetch -q origin "$base_branch"; then
# Remember the tip, because a later fetch overwrites FETCH_HEAD.
base_tip=$(git rev-parse --verify -q FETCH_HEAD)
else
echo "${SCRIPT_NAME}: cannot fetch ${base_branch} from origin." >&2
fi
fi
}
# Returns true if `git diff ${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}`
# would work; it only works if their common ancestor is in the clone.
_sgr_commit_range_is_usable() {
git merge-base "$CI_COMMIT_RANGE_START" "$CI_COMMIT_RANGE_END" > /dev/null 2>&1
}
# Does all the work. Its caller gives it the shell options it needs, and
# restores the client's options afterward.
_sgr_set_git_range_body() {
local SCRIPT_NAME="set-git-range"
local verbose="$CI_VERBOSE"
local debug="$CI_DEBUG"
local base_branch=""
local base_branch_fetched=""
local base_tip=""
local default_commit_range_start
local head_in_default_branch
local keep_end
local pr_head_sha
if [ -n "$debug" ]; then
verbose="$debug"
fi
### Commit range
if [ -n "$SYSTEM_PULLREQUEST_TARGETBRANCH" ]; then
## Azure Pipelines pull request
# For CI_COMMIT_RANGE_START: HEAD = $BUILD_SOURCEVERSION is a commit created by Azure; it isn't
# in the repo. Its first child HEAD^1 is the target branch (e.g., master); use that because
# $SYSTEM_PULLREQUEST_TARGETBRANCH is not fetched into this repo.
CI_COMMIT_RANGE_START=$(_sgr_head1)
CI_COMMIT_RANGE_END=$SYSTEM_PULLREQUEST_SOURCECOMMITID
CI_COMMIT_RANGE=${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}
elif [ -n "$BUILD_SOURCEBRANCH" ]; then
# Azure Pipelines build for a branch (possibly master).
if [ "$BUILD_SOURCEBRANCHNAME" = "$CI_DEFAULT_BRANCH_NAME" ]; then
default_commit_range_start=$(_sgr_head1)
# If a build fails, we would like to continue failing subsequent
# builds until the problem is fixed. Using `git rev-parse HEAD^1`
# does not do that, because there could have been multiple pushes
# since the last successful CI job.
# Pass argument to ci-last-success.py, because in a pull request
# we are only interested in the last success on the master branch,
# not on the feature branch! The feature branch might have many
# succeeding jobs and then ci-last-success.py will return just
# part of the pull request's diffs.
## # ci-last-success.py is disabled here because it makes at least one
## # unauthenticated GitHub API request per commit -- two when the commit's
## # check runs do not show that CI failed, and more when they span several
## # pages -- so walking back far exhausts the 60-requests-per-hour limit.
# # TODO: wrap all this in a ci-last-commit.sh shell script.
# if pip3 list --format columns | tail -n +1 | grep -q '^requests ' ; then
# # echo "Python requests package is already installed" >&2
# :
# else
# (sudo pip3 install requests || pip3 install --user requests || true) > /dev/null 2>&1
# fi
# if pip3 list --format columns | tail -n +1 | grep -q '^requests ' ; then
# CI_COMMIT_RANGE_START=$(${PLUME_SCRIPTS}/ci-last-success.py ${CI_ORGANIZATION} ${CI_REPO} ${default_commit_range_start})
# if [ -z "$CI_COMMIT_RANGE_START" ] ; then
# echo "WARNING: ci-last-success.py script failed; just considering last commit." >&2
# CI_COMMIT_RANGE_START=${default_commit_range_start}
# elif ! git cat-file -e ${CI_COMMIT_RANGE_START}^{commit}; then
# # The commit is not in the repository. Maybe it is older than the
# # commits pulled into this repository.
# echo "WARNING: ci-last-success.py script returned commit not in repository; using last commit." >&2
# CI_COMMIT_RANGE_START=${default_commit_range_start}
# else
# echo "ci-last-success.py ${CI_ORGANIZATION} ${CI_REPO} succeeded with $CI_COMMIT_RANGE_START." >&2
# fi
# else
# echo "WARNING: Could not run ci-last-success.py script; just considering last commit." >&2
# CI_COMMIT_RANGE_START=${default_commit_range_start}
# fi
## Use this because the ci-last-success.py call above is disabled.
CI_COMMIT_RANGE_START=${default_commit_range_start}
else
# Verify the ref rather than parsing it blindly. If
# $CI_DEFAULT_BRANCH_NAME is empty -- as when `git ls-remote` failed
# because the network is down or `origin` needs credentials -- then
# `git fetch origin ""` succeeds (it fetches HEAD) and
# `git rev-parse "origin/"` fails but still prints "origin/" on standard
# output, which would make a commit range that no code below detects.
CI_COMMIT_RANGE_START=""
if [ -n "$CI_DEFAULT_BRANCH_NAME" ]; then
if ! git fetch origin "$CI_DEFAULT_BRANCH_NAME"; then
# A previous fetch may have left a remote-tracking ref behind, so the
# ref below can resolve even though this fetch failed. Use it anyway:
# it is a commit on the base branch, whereas the fallback below is
# merely the previous commit. But say that it may be out of date, so
# that a diff containing unexpected changes can be explained.
echo "${SCRIPT_NAME}: cannot fetch ${CI_DEFAULT_BRANCH_NAME} from origin; origin/${CI_DEFAULT_BRANCH_NAME} may be out of date." >&2
fi
if git rev-parse --verify --quiet "origin/$CI_DEFAULT_BRANCH_NAME" > /dev/null 2>&1; then
CI_COMMIT_RANGE_START=$(git rev-parse "origin/$CI_DEFAULT_BRANCH_NAME")
fi
fi
if [ -z "$CI_COMMIT_RANGE_START" ]; then
echo "${SCRIPT_NAME}: cannot resolve origin/${CI_DEFAULT_BRANCH_NAME:-<unknown default branch>}; using the previous commit instead." >&2
CI_COMMIT_RANGE_START=$(_sgr_head1)
fi
fi
CI_COMMIT_RANGE_END=$BUILD_SOURCEVERSION
CI_COMMIT_RANGE=${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}
elif [ "$TRAVIS" = "true" ]; then
## Travis CI
# $TRAVIS_COMMIT_RANGE is empty for builds triggered by the initial commit of a new branch.
CI_COMMIT_RANGE=$TRAVIS_COMMIT_RANGE
elif [ -n "$CIRCLECI" ] && [ -n "$CIRCLE_PR_NUMBER" ]; then
## CircleCI pull request
# This script computes every value that the GitHub API provides, as a
# fallback in case the (unauthenticated) request is throttled.
# "// empty" maps JSON null to the empty string; the redirection discards
# jq's complaint if the response is not JSON (as when throttling
# yields an HTML error page).
CI_COMMIT_RANGE_START=$(printf '%s' "${CI_PULL_JSON}" | jq -r '.base.sha // empty' 2> /dev/null)
CI_COMMIT_RANGE_END=$(printf '%s' "${CI_PULL_JSON}" | jq -r '.head.sha // empty' 2> /dev/null)
# The branch that the pull request is against. It is usually the default
# branch, but a pull request may be against any branch.
base_branch=$(printf '%s' "${CI_PULL_JSON}" | jq -r '.base.ref // empty' 2> /dev/null)
if [ -z "$base_branch" ]; then
base_branch=$CI_DEFAULT_BRANCH_NAME
fi
if [ -z "$CI_COMMIT_RANGE_END" ] \
|| ! git rev-parse --verify -q "${CI_COMMIT_RANGE_END}^{commit}" > /dev/null; then
# Either the GitHub API request supplied no head commit, or it supplied
# one that is not in this clone (as happens when the pull request branch
# was pushed to after this job checked it out). Use the commit that this
# job is actually testing.
CI_COMMIT_RANGE_END=${CIRCLE_SHA1:-$(git rev-parse HEAD)}
fi
if [ -n "$CI_COMMIT_RANGE_START" ] && ! _sgr_commit_range_is_usable; then
# Fetch the branch that the commit is on. Fetching by commit ID would be
# cheaper, but not every server permits it; `git fetch --depth` would be
# futile, because the commit is on a branch that this clone may not track
# at all, and it would shorten a complete clone's history.
_sgr_fetch_base_branch
if ! _sgr_commit_range_is_usable; then
if git rev-parse --verify -q "${CI_COMMIT_RANGE_START}^{commit}" > /dev/null; then
# The commit is here, but this clone's history stops after the fork
# point. Completing the clone is the only way to reach it.
_sgr_unshallow
fi
if ! _sgr_commit_range_is_usable; then
# The commit is unreachable in origin, as after a force-push, or it is
# absent from this clone and the `_sgr_unshallow` above was skipped
# because of that. Do not distinguish the two cases: the fallback
# below computes the fork point, a better range start than either.
echo "${SCRIPT_NAME}: cannot use commit range start ${CI_COMMIT_RANGE_START} from the GitHub API." >&2
CI_COMMIT_RANGE_START=""
fi
fi
fi
if [ -z "$CI_COMMIT_RANGE_START" ]; then
# Either the GitHub API request failed, or its answer is unusable. Use
# the fork point from the base branch instead. Say so, because otherwise
# the log gives no way to tell which of the two was used.
echo "${SCRIPT_NAME}: computing commit range start locally, as the fork point from ${base_branch:-the default branch}." >&2
_sgr_fetch_base_branch
if [ -n "$base_tip" ]; then
CI_COMMIT_RANGE_START=$(git merge-base "$base_tip" "$CI_COMMIT_RANGE_END" 2> /dev/null)
if [ -z "$CI_COMMIT_RANGE_START" ]; then
# In a shallow clone, `git merge-base` either gives the right answer
# or no answer, so completing the clone is needed only now. The base
# branch does not need to be fetched again; only this clone's own
# history was missing.
_sgr_unshallow
CI_COMMIT_RANGE_START=$(git merge-base "$base_tip" "$CI_COMMIT_RANGE_END" 2> /dev/null)
fi
fi
if [ -z "$CI_COMMIT_RANGE_START" ]; then
# Last resort. Under CircleCI, HEAD is the pull request's head branch,
# so HEAD^1 is the pull request's next-to-last commit and the range
# omits every earlier commit of the pull request. Warn, because the
# range is valid and non-empty, so no client will complain about it.
CI_COMMIT_RANGE_START=$(_sgr_head1)
echo "${SCRIPT_NAME}: WARNING: cannot find the fork point from ${base_branch:-the default branch}, so the commit range covers only the last commit." >&2
fi
fi
CI_COMMIT_RANGE="${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}"
elif [ -n "${GITHUB_HEAD_REF}" ]; then
# GitHub Actions pull request (no special handling is required for non-PR GitHub Actions jobs).
# For a `pull_request` event, these three are the same:
# git rev-parse HEAD
# git rev-parse remotes/pull/"${GITHUB_REF_NAME}"
# ${GITHUB_SHA}
# Set the end of the range first, so that the checks below can ask whether
# a candidate start would give `git diff` a range it can use.
CI_COMMIT_RANGE_END=${GITHUB_SHA}
if [ "${GITHUB_EVENT_NAME}" = "pull_request_target" ]; then
# GITHUB_HEAD_REF is set for `pull_request_target` too, but there
# GITHUB_SHA is the base branch's tip rather than the pull request, and
# the default checkout is the base branch. Using GITHUB_SHA as the range
# end would silently diff a base branch commit instead of the pull
# request. Use the pull request's head commit, which is in the clone
# only if the workflow checked it out explicitly.
pr_head_sha=$(printf '%s' "${CI_PULL_JSON}" | jq -r '.head.sha // empty' 2> /dev/null)
if [ -z "$pr_head_sha" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then
# The API request failed or was throttled; the event payload, which is
# a local file, describes the same pull request.
pr_head_sha=$(jq -r '.pull_request.head.sha // empty' "${GITHUB_EVENT_PATH}" 2> /dev/null)
fi
if [ -n "$pr_head_sha" ] && git rev-parse --verify -q "${pr_head_sha}^{commit}" > /dev/null; then
CI_COMMIT_RANGE_END=$pr_head_sha
else
# Leave the range end empty; the check at the end of this function
# turns that into a failure. Diffing the base branch against itself
# would be worse: it reports changes that are not the pull request's,
# and no client can tell that the answer is wrong.
echo "${SCRIPT_NAME} in $(pwd): this ${GITHUB_EVENT_NAME} job's clone does not contain the pull request's head commit ${pr_head_sha:-<unknown>}. Check out the pull request's head, or use the \`pull_request\` event." >&2
CI_COMMIT_RANGE_END=""
fi
fi
if ! CI_COMMIT_RANGE_START=$(git rev-parse "${GITHUB_BASE_REF}" 2> /dev/null); then
if ! CI_COMMIT_RANGE_START=$(git rev-parse remotes/origin/"${GITHUB_BASE_REF}" 2> /dev/null); then
if ! CI_COMMIT_RANGE_START=$(git rev-parse refs/remotes/origin/"${GITHUB_BASE_REF}" 2> /dev/null); then
if ! CI_COMMIT_RANGE_START=$(git rev-parse refs/remotes/origin/"${CI_DEFAULT_BRANCH_NAME}" 2> /dev/null); then
# No ref names the base branch, as in a shallow single-branch
# checkout. Try the base commit from the pull request description
# that `set-ci-org-and-branch` obtained, but only if `git diff`
# could use it: it is the base tip as of when the pull request was
# created, which such a checkout is the least likely to contain.
CI_COMMIT_RANGE_START=$(printf '%s' "${CI_PULL_JSON}" | jq -r '.base.sha // empty' 2> /dev/null)
if [ -n "$CI_COMMIT_RANGE_START" ] && ! _sgr_commit_range_is_usable; then
echo "${SCRIPT_NAME}: cannot use commit range start ${CI_COMMIT_RANGE_START} from the GitHub API." >&2
CI_COMMIT_RANGE_START=""
fi
if [ -z "$CI_COMMIT_RANGE_START" ]; then
echo "${SCRIPT_NAME} in $(pwd): cannot find commit range start, tried ${GITHUB_BASE_REF} and remotes/origin/${GITHUB_BASE_REF} and refs/remotes/origin/${GITHUB_BASE_REF} and refs/remotes/origin/${CI_DEFAULT_BRANCH_NAME} and the pull request description" >&2
# This is a routine failure in a shallow single-branch checkout,
# so dump the state only when asked. `set` prints the whole
# environment, and CI logs are often readable by anyone who can
# see the job.
if [ -n "$debug" ]; then
{
echo '---------------- start of set'
set | _sgr_redact_tokens
echo '---------------- end of set'
echo "git log --graph:"
git log --graph | head -n 1000
echo "git show:"
git --no-pager show | head -n 1000
} >&2
fi
CI_COMMIT_RANGE_START=$(git rev-parse "${GITHUB_SHA}"^)
fi
fi
fi
fi
fi
CI_COMMIT_RANGE="${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}"
# If this elif is reached, it's not a pull request (except maybe a re-run Azure Pipelines pull request).
elif [ -n "$GITHUB_ACTIONS" ]; then
# GitHub Actions, non-pull-request.
CI_COMMIT_RANGE_END="${GITHUB_SHA}"
fi
if [ -n "$debug" ]; then
{
echo "CI_COMMIT_RANGE_START=${CI_COMMIT_RANGE_START}"
echo "CI_COMMIT_RANGE_END=${CI_COMMIT_RANGE_END}"
echo "CI_COMMIT_RANGE=${CI_COMMIT_RANGE}"
} >&2
fi
## The above may have set all three of CI_COMMIT_RANGE, CI_COMMIT_RANGE_START,
## and CI_COMMIT_RANGE_END; or only CI_COMMIT_RANGE, whose endpoints it does
## not know (Travis); or only CI_COMMIT_RANGE_END (GitHub Actions outside a
## pull request); or none of them. The two blocks below fill in whatever is
## missing: the first computes the range when CI_COMMIT_RANGE is empty, and
## the second splits CI_COMMIT_RANGE into its endpoints when they are empty.
# Separate from "It's not a pull request" because sometimes this is not set for Travis.
if [ -z "$CI_COMMIT_RANGE" ]; then
if [ -n "$debug" ]; then
echo "Setting CI_COMMIT_RANGE from CI_BRANCH=$CI_BRANCH" >&2
fi
# Keep an end commit that the CI service supplied; currently only GitHub
# Actions outside a pull request reaches here with one set. It is the
# commit that the job is testing, which is more accurate than the tip of the
# branch; the two differ when, for example, an old run is re-run. Every
# other CI service's code above uses the commit that the service supplies,
# so this makes GitHub Actions consistent with them.
# Require the commit to be in this clone, because the environment may
# describe a different clone than the one this script is running in; see the
# PROBLEM comment at the top of this file.
if [ -n "$CI_COMMIT_RANGE_END" ] \
&& git rev-parse --verify -q "${CI_COMMIT_RANGE_END}^{commit}" > /dev/null; then
keep_end="true"
if [ -n "$debug" ]; then
echo "Keeping CI_COMMIT_RANGE_END=$CI_COMMIT_RANGE_END from the CI service" >&2
fi
else
keep_end=""
fi
# Treat the branch as the default branch only if it is known to be: an
# empty $CI_BRANCH compares equal to an empty $CI_DEFAULT_BRANCH_NAME, and
# a job where neither could be determined would then take this arm and get
# a range covering only the last commit, with nothing said about the guess.
if [ -n "$CI_BRANCH" ] && [ "$CI_BRANCH" = "$CI_DEFAULT_BRANCH_NAME" ]; then
if git show --summary HEAD | grep -q ^Merge:; then
CI_COMMIT_RANGE_START=$(git merge-base HEAD^1 HEAD^2)
else
CI_COMMIT_RANGE_START=$(_sgr_head1)
fi
if [ -z "$keep_end" ]; then
CI_COMMIT_RANGE_END=$(git rev-parse HEAD)
fi
else
if [ -z "$CI_BRANCH" ]; then
# The branch is unknown, as in a CircleCI build of a tag, which sets no
# $CIRCLE_BRANCH. Say so: the range below is a guess. It is the range
# for a branch other than the default one -- which is the safer guess,
# because it is the wider of the two ranges -- except when the commit
# being built is already contained in the default branch, in which case
# that range would be empty and the code below uses the default
# branch's range instead.
echo "${SCRIPT_NAME}: WARNING: cannot determine the branch; computing the commit range as for a branch other than ${CI_DEFAULT_BRANCH_NAME:-the default branch}." >&2
fi
# When the branch is unknown and HEAD is an ancestor of the default
# branch -- as in a build of a tag that was cut from the default branch,
# after the default branch moved on -- "origin/$CI_DEFAULT_BRANCH_NAME"
# contains HEAD, so "origin/$CI_DEFAULT_BRANCH_NAME...HEAD" is the empty
# range and a client would report no changes at all. Compute the range
# as for the default branch instead. (This command also fails, harmlessly,
# if "origin/$CI_DEFAULT_BRANCH_NAME" does not exist in this clone.)
if [ -z "$CI_BRANCH" ] \
&& git merge-base --is-ancestor HEAD "origin/$CI_DEFAULT_BRANCH_NAME" 2> /dev/null; then
head_in_default_branch="true"
if [ -n "$debug" ]; then
echo "HEAD is contained in origin/$CI_DEFAULT_BRANCH_NAME" >&2
fi
else
head_in_default_branch=""
fi
# "origin/$CI_DEFAULT_BRANCH_NAME" is accurate when the pull request was
# created, but it doesn't change when force-push occurs (as Renovate does),
# making it out of date.
case $CI_BRANCH in
renovate/*) CI_COMMIT_RANGE_START=$(_sgr_head1) ;;
*)
if [ -z "$head_in_default_branch" ] \
&& git rev-parse --verify --quiet "origin/$CI_DEFAULT_BRANCH_NAME" > /dev/null 2>&1; then
CI_COMMIT_RANGE_START=$(git rev-parse "origin/$CI_DEFAULT_BRANCH_NAME")
if [ -n "$debug" ]; then
echo "Set CI_COMMIT_RANGE_START=$CI_COMMIT_RANGE_START from git rev-parse origin/$CI_DEFAULT_BRANCH_NAME" >&2
fi
else
if git show --summary HEAD | grep -q ^Merge:; then
CI_COMMIT_RANGE_START=$(git merge-base HEAD^1 HEAD^2)
else
CI_COMMIT_RANGE_START=$(_sgr_head1)
fi
fi
;;
esac
if [ -z "$keep_end" ]; then
if [ -n "$CI_BRANCH" ]; then
CI_COMMIT_RANGE_END=$(git rev-parse "$CI_BRANCH")
else
# `git rev-parse ""` fails, which would leave the end empty and make
# this script report that it cannot determine the commit range. HEAD
# is the commit that the job is testing, whatever branch it is on.
CI_COMMIT_RANGE_END=$(git rev-parse HEAD)
fi
fi
fi
CI_COMMIT_RANGE=${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}
if [ -n "$debug" ]; then
{
echo "CI_BRANCH=${CI_BRANCH}"
echo "CI_COMMIT_RANGE_START=${CI_COMMIT_RANGE_START}"
echo "CI_COMMIT_RANGE_END=${CI_COMMIT_RANGE_END}"
echo "CI_COMMIT_RANGE=${CI_COMMIT_RANGE}"
} >&2
fi
fi
if [ -z "$CI_COMMIT_RANGE_START" ]; then
if [ -n "$debug" ]; then
echo "Setting CI_COMMIT_RANGE_START from CI_COMMIT_RANGE=$CI_COMMIT_RANGE" >&2
fi
# Split on the literal separator "..." or "..".
case $CI_COMMIT_RANGE in
*...*)
CI_COMMIT_RANGE_START=${CI_COMMIT_RANGE%%...*}
CI_COMMIT_RANGE_END=${CI_COMMIT_RANGE##*...}
;;
*..*)
CI_COMMIT_RANGE_START=${CI_COMMIT_RANGE%%..*}
CI_COMMIT_RANGE_END=${CI_COMMIT_RANGE##*..}
CI_COMMIT_RANGE="${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}"
;;
*)
# Not a range at all, but a single commit (or empty).
CI_COMMIT_RANGE_START=""
CI_COMMIT_RANGE_END=${CI_COMMIT_RANGE}
;;
esac
if [ -z "$CI_COMMIT_RANGE_START" ]; then
CI_COMMIT_RANGE_START=${CI_COMMIT_RANGE_END}
fi
fi
# Avoid errors regarding empty range
if [ "$CI_COMMIT_RANGE_START" = "$CI_COMMIT_RANGE_END" ]; then
if [ -n "$verbose" ]; then
echo "Resetting CI_COMMIT_RANGE_START because CI_COMMIT_RANGE=$CI_COMMIT_RANGE" >&2
fi
if git show --summary "$CI_COMMIT_RANGE_END" | grep -q ^Merge:; then
CI_COMMIT_RANGE_START=$(git merge-base "$CI_COMMIT_RANGE_END^1" "$CI_COMMIT_RANGE_END^2")
else
if git rev-parse --verify --quiet "$CI_COMMIT_RANGE_END^1" > /dev/null 2>&1; then
CI_COMMIT_RANGE_START=$(git rev-parse "$CI_COMMIT_RANGE_END^1")
fi
fi
# The start and end commits could still be the same, if the current branch was cloned with
# --depth=1.
CI_COMMIT_RANGE="${CI_COMMIT_RANGE_START}...${CI_COMMIT_RANGE_END}"
fi
# Every code path above is supposed to set both endpoints, but one of them
# can still be empty: for example, if this directory is not a git clone,
# every `git` command above failed. Do not let an incomplete range reach the
# client, because git does not reject one: `git diff '...'` means
# `git diff HEAD...HEAD`, which reports an empty diff rather than an error.
if [ -z "$CI_COMMIT_RANGE_START" ] || [ -z "$CI_COMMIT_RANGE_END" ]; then
echo "${SCRIPT_NAME} in $(pwd): cannot determine the commit range; CI_COMMIT_RANGE_START=\"${CI_COMMIT_RANGE_START}\" and CI_COMMIT_RANGE_END=\"${CI_COMMIT_RANGE_END}\"." >&2
CI_COMMIT_RANGE=""
CI_COMMIT_RANGE_START=""
CI_COMMIT_RANGE_END=""
return 2
fi
if [ -n "$verbose" ]; then
{
echo '---------------- start of first 10000 lines of diff:'
git --no-pager diff "${CI_COMMIT_RANGE}" | head -n 10000
echo '---------------- end of first 10000 lines of diff.'
} >&2
fi
# The diff might be empty if the last commit is the revert of the one before it.
### Set it
export CI_COMMIT_RANGE_START
export CI_COMMIT_RANGE_END
export CI_COMMIT_RANGE
if [ -n "$verbose" ]; then
{
echo "CI_COMMIT_RANGE_START=$CI_COMMIT_RANGE_START"
echo "CI_COMMIT_RANGE_END=$CI_COMMIT_RANGE_END"
echo "CI_COMMIT_RANGE=$CI_COMMIT_RANGE"
} >&2
fi
}
# Give the body shell options that suit it, then restore the client's options.
# The client's `set -u` would abort on the many CI variables that are unset
# outside their own CI service, and its `set -e` would abort on a command whose
# failure this script handles itself.
# Only the two options that this script changes are saved, and they are read
# from "$-" rather than from `set +o`: bash and BusyBox ash turn errexit off
# inside the command substitution that capturing `set +o` requires, so a
# captured `set +o` would report errexit as off and turn it off in the client.
case $- in
*e*) _sgr_had_errexit="true" ;;
*) _sgr_had_errexit="" ;;
esac
case $- in
*u*) _sgr_had_nounset="true" ;;
*) _sgr_had_nounset="" ;;
esac
set +e +u
_sgr_set_git_range_body
_sgr_status=$?
# Clean up before restoring `set -e`, which makes the shell exit here if the
# client uses `set -e` and this script failed.
unset -f _sgr_set_git_range_body _sgr_redact_tokens _sgr_head1 _sgr_unshallow \
_sgr_fetch_base_branch _sgr_commit_range_is_usable
if [ -n "$_sgr_had_errexit" ]; then
set -e
fi
if [ -n "$_sgr_had_nounset" ]; then
set -u
fi
unset _sgr_had_errexit _sgr_had_nounset
# Unset the last of this script's variables and return, in a single command
# because unsetting the variable would otherwise lose the status.
eval "unset _sgr_status; return ${_sgr_status}"