mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 13:40:11 +01:00
`scripts/jj_push.sh` takes the same arguments as `jj git push`, runs prek over the commits that push would send, and pushes only if they pass. It learns what is being sent by running `jj git push --dry-run` and reading back the plan, so `--bookmark`, `--change`, `--all` and the rest work without reimplementing how they select commits. Hooks that rewrite files need a commit to write into, so the checks run with the working copy on top of the commit being pushed. When the working copy is already an empty commit there, which is the common case, it is used directly; otherwise one is created, and named in the error so the fixes can be squashed. `scripts/jj_prek.sh` gets two changes. It forwards its arguments to `prek run`, so `jj_push.sh` can ask for a specific range, and it now changes to the workspace root before running. It exports `GIT_DIR`, which makes git treat the current directory as the work tree, so prek could not find its configuration from a subdirectory. `jj` does not expand aliases when completing arguments, so `jj push` completed file names. `scripts/completions` has Bash, Zsh, and Fish completions that give it the same completions as `jj git push`. `docs/project/contribution_tools.md` documents the `push` alias, and a `prek` alias for `jj_prek.sh`, with the other per-repository `jj` configuration. Both are opt-in. Assisted-by: Claude Code
113 lines
3.6 KiB
Bash
Executable File
113 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
# Exceptions. See /LICENSE for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
# Runs prek over the commits `jj git push` would send, and pushes only if they
|
|
# pass. Takes the same arguments as `jj git push`.
|
|
|
|
set -eu
|
|
|
|
JJ_PREK="$(dirname "${BASH_SOURCE[0]}")/jj_prek.sh"
|
|
|
|
# Succeeds if the working-copy commit is in the given revset. This snapshots the
|
|
# working copy, so it sees anything the hooks rewrote.
|
|
working_copy_is() {
|
|
[[ -n "$(jj log --no-graph -r "@ & ($1)" --template 'commit_id')" ]]
|
|
}
|
|
|
|
# Succeeds if the working copy is an empty, undescribed child of the target,
|
|
# which hooks can run in and write their fixes into.
|
|
working_copy_sits_on() {
|
|
working_copy_is "empty() & description(exact:\"\") & children($1)"
|
|
}
|
|
|
|
# Returns the working copy to where it started. jj discards an empty,
|
|
# undescribed commit when the working copy moves off it, so the original may be
|
|
# gone. Build a new one on the same parents in that case.
|
|
restore_working_copy() {
|
|
if [[ -n "$(jj log --no-graph --ignore-working-copy \
|
|
-r "present($ORIG_CHANGE)" --template 'commit_id')" ]]; then
|
|
jj edit --quiet "$ORIG_CHANGE"
|
|
else
|
|
jj new --quiet $ORIG_PARENTS
|
|
fi
|
|
}
|
|
|
|
# `--help` describes `jj git push`, and has nothing to check.
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-h | --help)
|
|
exec jj git push "$@"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Ask jj what the push would do. This checks the arguments and lists the commits
|
|
# being sent, without contacting the remote. A `--dry-run` already in `$@` is
|
|
# harmless here, and still suppresses the push at the end.
|
|
if ! PLAN="$(jj git push --dry-run "$@" 2>&1)"; then
|
|
echo "$PLAN" >&2
|
|
exit 1
|
|
fi
|
|
|
|
REMOTE="$(sed -n 's/^Changes to push to \(.*\):$/\1/p' <<<"$PLAN")"
|
|
|
|
# Each updated bookmark or tag reports the commit it moves to. Deletions have no
|
|
# such commit, and send nothing to check.
|
|
TARGETS="$(sed -n 's/^ \(bookmark\|tag\): .* to \([0-9a-f]\{8,\}\)\]$/\2/p' <<<"$PLAN" |
|
|
paste -sd '|')"
|
|
|
|
# Nothing to check, so just push.
|
|
if [[ -z "$REMOTE" || -z "$TARGETS" ]]; then
|
|
exec jj git push "$@"
|
|
fi
|
|
|
|
# Only the heads need checking; a head's range covers everything below it.
|
|
HEADS="$(jj log --no-graph --ignore-working-copy -r "heads($TARGETS)" \
|
|
--template 'commit_id ++ "\n"')"
|
|
|
|
ORIG_CHANGE="$(jj log --no-graph -r @ --template 'change_id')"
|
|
ORIG_PARENTS="$(jj log --no-graph -r 'parents(@)' --template 'commit_id ++ " "')"
|
|
|
|
for target in $HEADS; do
|
|
# Check with the working copy on top of the target.
|
|
made_scratch=0
|
|
if ! working_copy_sits_on "$target"; then
|
|
jj new --quiet "$target"
|
|
made_scratch=1
|
|
fi
|
|
|
|
# Check from the newest ancestor already on the remote. When there is none,
|
|
# there is no range to diff, so check every file.
|
|
base="$(jj log --no-graph --ignore-working-copy \
|
|
-r "heads(::$target & ::remote_bookmarks(remote=exact:$REMOTE))" \
|
|
--template 'commit_id ++ "\n"' | head -n 1)"
|
|
if [[ -n "$base" ]]; then
|
|
check=(--from-ref "$base" --to-ref "$target")
|
|
else
|
|
check=(--all-files)
|
|
fi
|
|
|
|
result=0
|
|
"$JJ_PREK" "${check[@]}" || result=$?
|
|
|
|
# Discard the scratch commit unless the hooks wrote something into it.
|
|
if ((made_scratch)) && working_copy_is 'empty()'; then
|
|
restore_working_copy
|
|
fi
|
|
|
|
if ((result)); then
|
|
echo >&2
|
|
if ! working_copy_is 'empty()'; then
|
|
change="$(jj log --no-graph -r @ --template 'change_id.shortest()')"
|
|
echo "Hooks changed files. They are in $change." >&2
|
|
fi
|
|
echo "Error: checks failed, nothing pushed." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
exec jj git push "$@"
|