Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions scripts/check-affected.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
#
# --uncommitted is for a run that commits through an API from the files on
# disk rather than in the checkout, such as the issue lane: it commits the work
# tree, untracked files and file modes included, through a temporary index (the
# checkout's HEAD and index stay as they are), checks that commit out in a
# temporary worktree, and runs the checks there.
# tree, untracked files and file modes included, through a temporary index and
# object directory, checks that commit out in a scratch repository, and runs
# the checks there. Nothing is written under .git, so the checkout's HEAD,
# index and worktree list stay as they are.
#
# Each check calls the script or tool CI calls; none restates a check's logic.
# The changed set is <merge-base of HEAD and the base>..HEAD, committed work
Expand Down Expand Up @@ -100,13 +101,28 @@ fi
cd "$(git rev-parse --show-toplevel)" || exit 2

if ((uncommitted)); then
# The scratch repository has no remote, so the base is fetched and resolved
# to a commit here, and the inner run gets that commit and --no-fetch.
if [[ -z "$base" ]]; then
base="$(git rev-parse HEAD)" || exit 2
base=HEAD
elif ((fetch)) && [[ "$base" == */* ]]; then
git fetch --quiet "${base%%/*}" "${base#*/}" 2>/dev/null || export CHECK_AFFECTED_FETCH_FAILED=1
fi
base="$(git rev-parse --verify --quiet "$base^{commit}")" || {
echo "check-affected: cannot resolve the base to a commit" >&2
exit 2
}
# Nothing is written under .git, which the lane's sandbox can make read-only:
# new objects go to $snap/objects, and the commit is checked out in a scratch
# repository whose alternates borrow every other object from this one.
snap="$(mktemp -d)" || exit 2
trap 'git worktree remove --force "$snap/tree" >/dev/null 2>&1; rm -rf "$snap"' EXIT
trap 'rm -rf "$snap"' EXIT
objects="$(cd "$(git rev-parse --git-common-dir)/objects" && pwd -P)" || exit 2
index="$(git rev-parse --git-path index)" || exit 2
shallow="$(git rev-parse --git-path shallow)" || exit 2
[[ ! -f "$index" ]] || cp "$index" "$snap/index" || exit 2
mkdir "$snap/objects" || exit 2
export GIT_INDEX_FILE="$snap/index" GIT_OBJECT_DIRECTORY="$snap/objects" GIT_ALTERNATE_OBJECT_DIRECTORIES="$objects"
# The lane's sandbox masks dotfiles with /dev/null, a device git will not
# add, so only files, symlinks, repositories and deletions are staged.
# An empty pathspec would make add -A stage everything, so none means no add.
Expand All @@ -115,16 +131,19 @@ if ((uncommitted)); then
if [[ -f "$f" || -L "$f" || -d "$f" || ! -e "$f" ]]; then paths+=("$f"); fi
done < <(git ls-files -z --modified --others --exclude-standard)
if ((${#paths[@]})); then
printf '%s\0' "${paths[@]}" |
GIT_INDEX_FILE="$snap/index" git add -A --pathspec-from-file=- --pathspec-file-nul || exit 2
printf '%s\0' "${paths[@]}" | git add -A --pathspec-from-file=- --pathspec-file-nul || exit 2
fi
tree="$(GIT_INDEX_FILE="$snap/index" git write-tree)" || exit 2
tree="$(git write-tree)" || exit 2
commit="$(git -c user.name=check-affected -c user.email=check-affected@localhost \
commit-tree "$tree" -p HEAD -m 'check-affected: uncommitted work')" || exit 2
git -c core.hooksPath=/dev/null worktree add --quiet --detach "$snap/tree" "$commit" || exit 2
args=(--base "$base")
unset GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
git -c init.defaultBranch=snapshot init --quiet --template= "$snap/tree" || exit 2
printf '%s\n' "$snap/objects" "$objects" >"$snap/tree/.git/objects/info/alternates" || exit 2
# A shallow clone's boundary, so history walks stop where the clone does.
[[ ! -f "$shallow" ]] || cp "$shallow" "$snap/tree/.git/shallow" || exit 2
git -C "$snap/tree" -c core.hooksPath=/dev/null checkout --quiet --detach "$commit" || exit 2
args=(--base "$base" --no-fetch)
((run_tests)) && args+=(--tests)
((fetch)) || args+=(--no-fetch)
rc=0
(cd "$snap/tree" && bash "$tools/scripts/check-affected.sh" "${args[@]}") || rc=$?
exit "$rc"
Expand Down Expand Up @@ -172,6 +191,7 @@ run_check() {
# A remote-tracking base is refreshed first, so the stale-base check compares
# against the tip a merge would land on. A failed fetch skips that check only.
fetched=1
[[ -z "${CHECK_AFFECTED_FETCH_FAILED-}" ]] || fetched=0
if ((fetch)) && [[ "$base" == */* ]]; then
git fetch --quiet "${base%%/*}" "${base#*/}" 2>/dev/null || fetched=0
fi
Expand Down
9 changes: 8 additions & 1 deletion scripts/check-affected.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,15 @@ if bwrap --bind / / --dev /dev true 2>/dev/null; then
check "--uncommitted skips untracked and tracked device nodes and still checks the regular file beside it" \
'[[ $RC == 1 && $ERR == *"new-tool starts with #!"* && $ERR != *"can only add regular files"* ]]'
rm -f "$repo/.bash_profile"
# The lane's sandbox can leave .git read-only; the snapshot must not write there.
RC=0
OUT="$(cd "$repo" && bwrap --bind / / --dev /dev --ro-bind "$repo/.git" "$repo/.git" \
bash scripts/check-affected.sh --uncommitted --base base 2>"$tmp/err")" || RC=$?
ERR="$(cat "$tmp/err")"
check "--uncommitted runs the checks with a read-only .git" \
'[[ $RC == 1 && $ERR == *"new-tool starts with #!"* && $ERR != *"Read-only file system"* ]]'
else
ok "SKIP device-node case (bwrap cannot start here)"
ok "SKIP device-node and read-only .git cases (bwrap cannot start here)"
fi
rm -rf "$repo/new-tool"
check "--uncommitted leaves HEAD, the index and the worktree list as they were" '[[ $(state) == "$before" ]]'
Expand Down
1 change: 1 addition & 0 deletions scripts/lib/gate-entry.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ BASE_REF_ALLOW=(
'check-silent-revert.sh|git rev-parse --verify "${parent}:${file}"|captures a blob id'
'check-silent-revert.sh|git rev-parse --verify "${commit}:${file}"|captures a blob id'
'check-stale-base-overlap.sh|base_tip="$(git rev-parse "${base_ref}^{commit}")"|captures the resolved sha'
'check-affected.sh|base="$(git rev-parse --verify --quiet "$base^{commit}")"|captures the resolved sha'
"check-changelog-parity.sh|git rev-parse -q --verify 'HEAD^2'|probes for a merge commit, not a base ref"
"check-changelog-fragments.sh|git rev-parse -q --verify 'HEAD^2'|probes for a merge commit, not a base ref"
"changelog-fragments.sh|git rev-parse -q --verify 'HEAD^2'|probes for a merge commit, not a base ref"
Expand Down
Loading