mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
fix(upgrade): refuse an install whose enclosing project tracks its files (#1022)
A project that commits gstack's files as plain blobs puts the old ones back on every checkout of its own, so VERSION froze behind HEAD while gstack-upgrade reported the install current. bin/gstack-ff-update, the one move the auto-updater and /gstack-upgrade share, now refuses with exit 6 (nothing changed) when the enclosing repository tracks non-gitlink paths under the install, or when VERSION differs from HEAD (eol-normalized). gstack-upgrade runs the same checks through --preflight before any fetch; the auto-updater holds the update with a banner and a troubleshooting anchor. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
5ba425af4b
commit
fdd5169a34
7 files changed
+210
-11
No files matched your search
+41
-3
@@ -36,7 +36,14 @@
|
||||
# exit 3 autostash-unrecoverable: (edits kept where they are)
|
||||
# exit 4 not-fast-forward:
|
||||
# exit 5 index-unreadable:
|
||||
# exit 6 tracked-by-project: / version-reverted: (#1022, the preflight)
|
||||
# exit 75 ff-busy: (another update holds this install's lock)
|
||||
#
|
||||
# gstack-ff-update --preflight <dir> runs only the exit-6 checks (exit 0 or 6);
|
||||
# bin/gstack-upgrade calls it before any fetch. A project that tracks the
|
||||
# install's files as plain blobs puts old ones back on every checkout of its
|
||||
# own, so VERSION freezes behind HEAD while updates seem to succeed; a
|
||||
# submodule gitlink is the supported shape and passes.
|
||||
# A rescue also prints the inspect, export and delete commands on stderr.
|
||||
|
||||
set +e
|
||||
@@ -49,15 +56,46 @@ ff_flatten() {
|
||||
printf '%s' "$1" | tr '\n\r\t' ' ' | sed "s/${_ff_esc}\[[0-9;?]*[ -\/]*[@-~]//g" | tr -d '\000-\037\177' | head -c "${2:-200}"
|
||||
}
|
||||
ff_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
||||
refuse() { printf '%s: %s\n' "$2" "$(ff_flatten "$3" 300)"; exit "$1"; }
|
||||
refuse() { printf '%s: %s\n' "$2" "$(ff_flatten "$3" "${4:-300}")"; exit "$1"; }
|
||||
|
||||
[ $# -eq 2 ] && [ -n "$1" ] && [ -n "$2" ] || refuse 2 usage "gstack-ff-update <install dir> <revision>"
|
||||
DIR=$1 REV=$2
|
||||
[ $# -eq 2 ] && [ -n "$1" ] && [ -n "$2" ] || refuse 2 usage "gstack-ff-update <install dir> <revision> | --preflight <install dir>"
|
||||
PREFLIGHT=0
|
||||
if [ "$1" = --preflight ]; then PREFLIGHT=1 DIR=$2 REV=HEAD; else DIR=$1 REV=$2; fi
|
||||
cd -- "$DIR" 2>/dev/null || refuse 2 not-a-checkout "cannot enter $DIR"
|
||||
DIR=$(pwd -P)
|
||||
g() { git -c core.fsmonitor=false -c core.splitIndex=false "$@"; }
|
||||
[ "$(g rev-parse --is-inside-work-tree 2>/dev/null)" = true ] && [ -z "$(g rev-parse --show-prefix 2>/dev/null)" ] \
|
||||
|| refuse 2 not-a-checkout "$DIR is not the top of a git checkout"
|
||||
|
||||
# ── Preflight (#1022) ──
|
||||
# The repository enclosing the install's parent, probed with the caller's git
|
||||
# environment cleared; any git error there means not tracked.
|
||||
_PF=$(
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_OBJECT_DIRECTORY
|
||||
cd -- "$DIR/.." 2>/dev/null || exit 0
|
||||
_top=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
_rel="$(git rev-parse --show-prefix 2>/dev/null)${DIR##*/}" || exit 0
|
||||
_n=0
|
||||
while IFS= read -r _l; do
|
||||
case "$_l" in (''|'160000 '*) ;; (*) _n=$((_n + 1)) ;; esac
|
||||
done <<EOF_PF
|
||||
$(git -C "$_top" --literal-pathspecs ls-files -s -- "$_rel" 2>/dev/null)
|
||||
EOF_PF
|
||||
[ "$_n" -gt 0 ] && printf '%s\n%s\n%s\n' "$_top" "$_rel" "$_n"
|
||||
)
|
||||
if [ -n "$_PF" ]; then
|
||||
_top=$(printf '%s\n' "$_PF" | sed -n 1p) _rel=$(printf '%s\n' "$_PF" | sed -n 2p) _n=$(printf '%s\n' "$_PF" | sed -n 3p)
|
||||
refuse 6 tracked-by-project "$_top tracks $_n files of the gstack install at $_rel, so every checkout there puts old gstack files back (VERSION frozen, #1022); nothing was changed; fix: git -C $(ff_quote "$_top") rm -r -q --cached -- $(ff_quote "$_rel"), add /$_rel/ to $(ff_quote "$_top/.gitignore") and commit both on every branch you check out (or vendor gstack as a git submodule), then git -C $(ff_quote "$DIR") stash push -m 'files reverted by the project' (keeps every edit) and re-run" 900
|
||||
fi
|
||||
if g cat-file -e HEAD:VERSION 2>/dev/null; then
|
||||
g diff --quiet HEAD -- VERSION 2>/dev/null
|
||||
if [ $? -eq 1 ]; then
|
||||
_have=$(head -1 VERSION 2>/dev/null | tr -d '\r') _want=$(g show HEAD:VERSION 2>/dev/null | head -1 | tr -d '\r')
|
||||
refuse 6 version-reverted "VERSION says ${_have:-nothing} but the checkout is at $_want: something outside git put old gstack files back (an enclosing project's checkout, #1022); nothing was changed; fix: git -C $(ff_quote "$DIR") stash push -m 'files reverted by the project' (keeps every edit), then re-run" 900
|
||||
fi
|
||||
fi
|
||||
[ "$PREFLIGHT" -eq 1 ] && exit 0
|
||||
|
||||
TARGET=$(g rev-parse -q --verify "$REV^{commit}" 2>/dev/null) || refuse 2 target-unresolvable "$REV does not name a commit in $DIR"
|
||||
COMMON=$(g rev-parse --path-format=absolute --git-common-dir 2>/dev/null) \
|
||||
|| COMMON=$(cd -- "$(g rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P) \
|
||||
|
||||
@@ -246,6 +246,8 @@ case "$_P_PULL" in
|
||||
echo "gstack auto-update: update held ($_P_PULL); nothing was installed or changed, and your current hooks keep running. gstack retries at the next check. Fix now ($_WHERE): $_FF_FIX (setup checks every hook it registers). https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#auto-update-hook-check-unavailable" ;;
|
||||
unmerged-files*)
|
||||
echo "gstack auto-update: update held ($_P_PULL); an earlier merge, rebase or stash pop left conflicts in the install, and an update would bury them. Fix now ($_WHERE): $_FF_FIX (saves every edit, the unmerged files included, in a rescue ref before it updates). https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#auto-update-unmerged-files" ;;
|
||||
tracked-by-project*|version-reverted*)
|
||||
echo "gstack auto-update: update held ($_P_PULL); nothing was installed or changed. gstack retries at the next check. The full fix: bash $_Q/bin/gstack-ff-update --preflight $_Q. https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#auto-update-tracked-by-project" ;;
|
||||
autostash-unrecoverable*)
|
||||
echo "gstack auto-update: update held ($_P_PULL); your local edits are where they were and nothing was installed. Look first: git -C $_Q status --short. Fix now ($_WHERE): $_FF_FIX (once the cause above is gone). https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#auto-update-autostash-unrecoverable" ;;
|
||||
*)
|
||||
@@ -500,7 +502,7 @@ CALLER_PID=$(gstack_harness_pid "$PPID")
|
||||
log_entry "AUTOSTASH_CONFLICT_RECOVERED tree_reset=1 kept_stash=${4##*/} kept_ref=$4 $5"
|
||||
rescue_note "$4" "${5#unmerged=}" ;;
|
||||
0:*) ;;
|
||||
3:*) HELD=$FF_OUT PULL_EXIT=0 ;;
|
||||
3:*|6:*) HELD=$FF_OUT PULL_EXIT=0 ;;
|
||||
75:*) log_entry "SKIP ff_busy $(ff_flatten "$FF_OUT" 200)"; rm -f "$PULL_ERR_FILE" 2>/dev/null; exit 0 ;;
|
||||
*) printf '%s\n' "${FF_OUT:-bin/gstack-ff-update exited $PULL_EXIT}" > "${PULL_ERR_FILE:-/dev/null}" ;;
|
||||
esac
|
||||
|
||||
+6
-2
@@ -21,7 +21,9 @@
|
||||
# is not a gstack checkout, HEAD is detached or not on the remote default
|
||||
# branch (a pin), any tracked byte is dirty, a local commit diverged, the
|
||||
# incoming release's Bun floor holds, or another updater holds the setup
|
||||
# lock; never stashes, never resets. An unreachable remote is
|
||||
# lock; never stashes, never resets. Before any fetch it also refuses when
|
||||
# bin/gstack-ff-update --preflight does: an enclosing project tracks the
|
||||
# install's files, or VERSION was put back behind HEAD (#1022). An unreachable remote is
|
||||
# UPGRADE_FETCH_FAILED (exit 3). While another live session holds a workflow
|
||||
# lease on this installation (bin/gstack-lease live) it refuses with
|
||||
# UPGRADE_BUSY (exit 3); GSTACK_UPGRADE_FORCE=1 overrides. Prints
|
||||
@@ -50,7 +52,7 @@ case "${1:-}" in
|
||||
--check) CHECK=1 ;;
|
||||
--ff-only-no-reinstall) REINSTALL=0 ;;
|
||||
'') ;;
|
||||
-h|--help) sed -n '2,38p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
-h|--help) sed -n '2,41p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "gstack-upgrade: unknown argument '$1'" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -71,6 +73,8 @@ bounded() { if command -v timeout >/dev/null 2>&1; then timeout "${GSTACK_UPGRAD
|
||||
TOPLEVEL=$(git -C "$ROOT" rev-parse --show-toplevel 2>/dev/null)
|
||||
[ -f "$ROOT/VERSION" ] && [ -f "$ROOT/setup" ] && [ -n "$TOPLEVEL" ] && gstack_path_same "$TOPLEVEL" "$ROOT" \
|
||||
|| refuse "$ROOT is not a gstack checkout" "run gstack-upgrade from a git checkout of https://github.com/garrytan/gstack"
|
||||
PF=$(bash "$ROOT/bin/gstack-ff-update" --preflight "$ROOT" 2>/dev/null)
|
||||
[ $? -eq 6 ] && { echo "gstack-upgrade: $PF (UPGRADE_REFUSED)" >&2; exit 3; }
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
HOST=$(git -C "$ROOT" remote get-url origin 2>/dev/null); HOST="${HOST#*://}"; HOST="${HOST#*@}"; HOST="${HOST%%[/:]*}"
|
||||
R="user ran gstack-upgrade"
|
||||
|
||||
@@ -1109,6 +1109,26 @@ conflict yourself instead, finish or abort your merge (`git -C '<dir>' merge
|
||||
--abort`, `git -C '<dir>' checkout -- <file>`); gstack retries at the next
|
||||
check.
|
||||
|
||||
<a id="auto-update-tracked-by-project"></a>
|
||||
### `gstack auto-update: update held (tracked-by-project: ...)` / `(version-reverted: ...)` / `gstack-upgrade: tracked-by-project: ... (UPGRADE_REFUSED)`
|
||||
|
||||
**Meaning.** Your gstack install sits inside another git project, and that
|
||||
project has committed gstack's files as ordinary files. Every time you switch
|
||||
branches or check out files in that project, git puts its old copies of
|
||||
gstack's files back, so VERSION and other files fall behind the install's own
|
||||
HEAD while updates seem to succeed (#1022). `version-reverted` is the same
|
||||
symptom seen from the install: its VERSION disagrees with HEAD. Neither
|
||||
`gstack-upgrade`, `/gstack-upgrade` nor the auto-updater moves the install
|
||||
until it is fixed. A project that holds gstack as a git submodule is fine.
|
||||
|
||||
**Fix.** Print the exact commands with
|
||||
`bash <install>/bin/gstack-ff-update --preflight <install>`. They do three
|
||||
things: untrack the install in the project (`git rm -r --cached`), add it to
|
||||
the project's `.gitignore`, and commit both on every branch you check out
|
||||
(or vendor gstack as a submodule instead). Then
|
||||
`git -C <install> stash push` puts the install's own files back and keeps
|
||||
any edit you made in the stash, and the next update proceeds.
|
||||
|
||||
<a id="auto-update-autostash-unrecoverable"></a>
|
||||
### `gstack auto-update: update held (autostash-unrecoverable: <reason>); your local edits are where they were and nothing was installed. Look first: git -C '<dir>' status --short. Fix now (...)` / `autostash-unrecoverable: <reason>`
|
||||
|
||||
|
||||
@@ -264,6 +264,33 @@ describe.skipIf(process.platform === 'win32')('gstack-ff-update: moving the tree
|
||||
expect(git(fx.install, 'log', '-1', '--format=%an <%ae>|%cn <%ce>', ref)).toBe('gstack rescue <gstack@localhost>|gstack rescue <gstack@localhost>');
|
||||
});
|
||||
|
||||
// Value: protects=the one move every upgrade path shares refuses (exit 6, nothing changed) when an enclosing project tracks the install or
|
||||
// VERSION was put back behind HEAD, even with nothing to move (#1022); fails_when=the preflight is skipped, runs after "already", or counts a gitlink;
|
||||
// why_new=no case had an enclosing project; seam=none
|
||||
test('preflight: a tracking project or a reverted VERSION refuses with exit 6 and changes nothing; a gitlink and --preflight on a clean install pass', () => {
|
||||
const fx = makeFixture();
|
||||
const old = git(fx.install, 'rev-parse', 'HEAD');
|
||||
expect(spawnSync('bash', [FF, '--preflight', fx.install], { encoding: 'utf8', env: isolatedEnv(fx.base), timeout: 60_000 }).status).toBe(0);
|
||||
git(fx.base, 'init', '-q', '-b', 'main');
|
||||
git(fx.base, 'update-index', '--add', '--cacheinfo', `160000,${old},install`);
|
||||
expect(spawnSync('bash', [FF, '--preflight', fx.install], { encoding: 'utf8', env: isolatedEnv(fx.base), timeout: 60_000 }).status).toBe(0);
|
||||
git(fx.base, 'update-index', '--force-remove', 'install');
|
||||
git(fx.base, 'update-index', '--add', '--', 'install/VERSION', 'install/notes.txt');
|
||||
release(fx, { 'notes.txt': 'top UPSTREAM\nmiddle\nbottom\n' });
|
||||
const tracked = ff(fx);
|
||||
expect(tracked.status, tracked.stdout + tracked.stderr).toBe(6);
|
||||
expect(tracked.stdout).toStartWith(`tracked-by-project: ${fx.base} tracks 2 files of the gstack install at install,`);
|
||||
expect(tracked.stdout.trim().split('\n')).toHaveLength(1);
|
||||
expect(git(fx.install, 'rev-parse', 'HEAD')).toBe(old);
|
||||
fs.rmSync(path.join(fx.base, '.git'), { recursive: true, force: true });
|
||||
expect(ff(fx).status).toBe(0);
|
||||
write(fx, 'VERSION', '1.0.0\n');
|
||||
const reverted = ff(fx);
|
||||
expect(reverted.status).toBe(6);
|
||||
expect(reverted.stdout).toStartWith('version-reverted: VERSION says 1.0.0 but the checkout is at 1.1.0');
|
||||
expect(read(fx, 'VERSION')).toBe('1.0.0\n');
|
||||
});
|
||||
|
||||
test('refusals: not a fast-forward, an unresolvable target, and a missing checkout', () => {
|
||||
const fx = makeFixture();
|
||||
write(fx, 'VERSION', 'local\n');
|
||||
|
||||
@@ -19,7 +19,7 @@ const REPO = path.resolve(import.meta.dir, '..');
|
||||
const DEPS = [
|
||||
'bin/gstack-upgrade', 'bin/gstack-bun-version.sh', 'bin/gstack-paths', 'bin/gstack-state-root.sh',
|
||||
'bin/gstack-egress-lib.sh', 'bin/gstack-egress-receipt', 'lib/egress-receipt.ts', 'lib/state-root.ts',
|
||||
'bin/gstack-lease', 'bin/gstack-harness-pid.sh', 'bin/gstack-install-verdict.sh',
|
||||
'bin/gstack-lease', 'bin/gstack-harness-pid.sh', 'bin/gstack-install-verdict.sh', 'bin/gstack-ff-update',
|
||||
];
|
||||
const hosts: CapyHost[] = [];
|
||||
const children: ChildProcess[] = [];
|
||||
@@ -40,7 +40,7 @@ const write = (file: string, text: string, mode = 0o644) => {
|
||||
fs.writeFileSync(file, text, { mode });
|
||||
};
|
||||
|
||||
function fixture() {
|
||||
function fixture(opts: { parent?: 'files' | 'gitlink' } = {}) {
|
||||
const host = capyHost({ systemBun: '1.4.2' });
|
||||
hosts.push(host);
|
||||
const git = (args: string[], cwd: string) => {
|
||||
@@ -68,8 +68,18 @@ function fixture() {
|
||||
const a = git(['rev-parse', 'HEAD'], seed);
|
||||
git(['init', '-q', '--bare', '-b', 'main', bare], host.base);
|
||||
git(['push', '-q', bare, 'main'], seed);
|
||||
const root = path.join(host.base, 'gstack');
|
||||
const project = path.join(host.base, 'project');
|
||||
const root = opts.parent ? path.join(project, '.claude/skills/gstack') : path.join(host.base, 'gstack');
|
||||
fs.mkdirSync(path.dirname(root), { recursive: true });
|
||||
git(['clone', '-q', '--single-branch', '--depth', '1', `file://${bare}`, root], host.base);
|
||||
if (opts.parent) {
|
||||
// #1022: a project that committed the install's files as plain blobs (or,
|
||||
// the supported shape, as a submodule gitlink) beside the nested .git.
|
||||
git(['init', '-q', '-b', 'main'], project);
|
||||
if (opts.parent === 'files') git(['update-index', '--add', '--', ...git(['ls-files'], root).split('\n').map(f => `.claude/skills/gstack/${f}`)], project);
|
||||
else git(['update-index', '--add', '--cacheinfo', `160000,${a},.claude/skills/gstack`], project);
|
||||
git(['commit', '-qm', 'vendor gstack'], project);
|
||||
}
|
||||
write(path.join(seed, 'VERSION'), '1.0.1\n');
|
||||
git(['commit', '-qam', 'B'], seed);
|
||||
const b = git(['rev-parse', 'HEAD'], seed);
|
||||
@@ -78,7 +88,7 @@ function fixture() {
|
||||
const run = (args: string[] = [], extra: Record<string, string> = {}) =>
|
||||
spawnSync('bash', [path.join(root, 'bin/gstack-upgrade'), ...args], { cwd: host.home, encoding: 'utf8', env: { ...env, ...extra }, timeout: 60_000 });
|
||||
return {
|
||||
host, root, bare, a, b, run, git: (args: string[]) => git(args, root),
|
||||
host, root, bare, a, b, run, project, git: (args: string[]) => git(args, root), parentGit: (args: string[]) => git(args, project),
|
||||
calls: () => fs.existsSync(log) ? fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean) : [],
|
||||
};
|
||||
}
|
||||
@@ -396,6 +406,70 @@ describe('gstack-upgrade', () => {
|
||||
expect(f.calls()).toEqual(['setup --refresh-registered', `doctor --check --require runtime,pins --project ${fs.realpathSync(project)}`]);
|
||||
});
|
||||
|
||||
// Value: protects=an install whose enclosing project tracks its files is refused before it moves, so a project checkout cannot revert VERSION behind an upgraded HEAD (#1022);
|
||||
// fails_when=the enclosing-project check is dropped or matches a submodule gitlink; why_new=no case had an enclosing repository; seam=none
|
||||
test('a project that tracks the install\'s files refuses --check and default with UPGRADE_REFUSED naming the untrack fix (#1022)', () => {
|
||||
const f = fixture({ parent: 'files' });
|
||||
const reflog = f.git(['reflog']);
|
||||
for (const args of [['--check'], []]) {
|
||||
const r = f.run(args);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(3);
|
||||
expect(r.stderr).toContain('(UPGRADE_REFUSED)');
|
||||
expect(r.stderr).toContain(`${f.project} tracks`);
|
||||
expect(r.stderr).toContain(`git -C '${f.project}' rm -r -q --cached -- '.claude/skills/gstack'`);
|
||||
expect(r.stderr.trim().split('\n').filter(l => l.startsWith('gstack-upgrade: '))).toHaveLength(1);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.a);
|
||||
expect(f.git(['reflog'])).toBe(reflog);
|
||||
}
|
||||
expect(f.calls()).toEqual([]);
|
||||
f.parentGit(['rm', '-r', '-q', '--cached', '--', '.claude/skills/gstack']);
|
||||
f.git(['stash', 'push', '-m', 'files reverted by the project']);
|
||||
const fixed = f.run();
|
||||
expect(fixed.status, fixed.stdout + fixed.stderr).toBe(0);
|
||||
expect(fixed.stdout).toContain('gstack-upgrade: upgraded 1.0.0 -> 1.0.1');
|
||||
});
|
||||
|
||||
// Value: protects=the supported vendoring shape (a submodule gitlink) still upgrades; fails_when=the #1022 check counts a gitlink as tracked files;
|
||||
// why_new=negative control for the case above; seam=none
|
||||
test('a project that holds the install as a submodule gitlink upgrades normally', () => {
|
||||
const f = fixture({ parent: 'gitlink' });
|
||||
const r = f.run();
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(f.git(['rev-parse', 'HEAD'])).toBe(f.b);
|
||||
});
|
||||
|
||||
// Value: protects=a working-tree VERSION that disagrees with HEAD is never reported current (the frozen VERSION of #1022);
|
||||
// fails_when=the current path trusts the VERSION file without comparing it to HEAD; why_new=every current case had a clean tree; seam=none
|
||||
test('a VERSION reverted behind HEAD is not current: --check and default refuse naming both versions (#1022)', () => {
|
||||
const f = fixture();
|
||||
expect(f.run(['--ff-only-no-reinstall']).status).toBe(0);
|
||||
fs.writeFileSync(path.join(f.root, 'VERSION'), '1.0.0\n');
|
||||
for (const args of [['--check'], []]) {
|
||||
const r = f.run(args);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(3);
|
||||
expect(r.stdout).not.toContain('gstack-upgrade: current');
|
||||
expect(r.stderr).toContain('VERSION says 1.0.0 but the checkout is at 1.0.1');
|
||||
expect(r.stderr).toContain('(UPGRADE_REFUSED)');
|
||||
}
|
||||
expect(f.calls()).toEqual([]);
|
||||
f.git(['stash', 'push', '-q', '-m', 'files reverted by the project']);
|
||||
const fixed = f.run(['--check']);
|
||||
expect(fixed.status, fixed.stdout + fixed.stderr).toBe(0);
|
||||
expect(fixed.stdout).toContain('gstack-upgrade: current 1.0.1');
|
||||
});
|
||||
|
||||
// Value: protects=a Windows checkout with core.autocrlf (VERSION written CRLF) is not mistaken for a reverted VERSION;
|
||||
// fails_when=the VERSION check compares raw bytes instead of git's eol-normalized diff; why_new=the check is new; seam=none
|
||||
test('a CRLF VERSION under core.autocrlf=true is current, not reverted', () => {
|
||||
const f = fixture();
|
||||
expect(f.run(['--ff-only-no-reinstall']).status).toBe(0);
|
||||
f.git(['config', 'core.autocrlf', 'true']);
|
||||
fs.writeFileSync(path.join(f.root, 'VERSION'), '1.0.1\r\n');
|
||||
const r = f.run(['--check']);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain('gstack-upgrade: current 1.0.1');
|
||||
});
|
||||
|
||||
test('every bin a trailer or fix line names exists in the repo', () => {
|
||||
const sources = ['bin/gstack-capy-install', 'bin/gstack-doctor-check.sh', 'bin/gstack-skill-end', 'bin/gstack-upgrade', 'docs/capy-install.md',
|
||||
'capy/skill/gstack/SKILL.md', 'capy/skill/gstack/scripts/install.sh'];
|
||||
|
||||
@@ -166,10 +166,44 @@ describe.skipIf(process.platform === 'win32')('gstack-session-update: held-updat
|
||||
expect(git(fx.install, 'rev-parse', 'HEAD')).toBe(target);
|
||||
});
|
||||
|
||||
// Value: protects=the auto-updater holds, instead of rescuing and re-updating forever, an install whose enclosing project tracks its files (#1022);
|
||||
// fails_when=gstack-ff-update's exit 6 is not a hold, or the banner's command does not print the full fix; why_new=no case had an enclosing project; seam=none
|
||||
test('tracked-by-project: the update holds with HEAD unchanged; the banner names the preflight, which prints the full fix', async () => {
|
||||
const fx = makeFixture();
|
||||
const old = git(fx.install, 'rev-parse', 'HEAD');
|
||||
release(fx, 'top UPSTREAM\nmiddle\nbottom\n');
|
||||
git(fx.base, 'init', '-q', '-b', 'main');
|
||||
git(fx.base, 'update-index', '--add', '--', ...git(fx.install, 'ls-files').split('\n').map((f) => `it's an install/${f}`));
|
||||
spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env(fx), timeout: 20_000 });
|
||||
await waitFor(fx, /Z HELD tracked-by-project: /);
|
||||
expect(git(fx.install, 'rev-parse', 'HEAD')).toBe(old);
|
||||
expect(git(fx.install, 'for-each-ref', 'refs/gstack/rescue')).toBe('');
|
||||
const lines = banner(fx).trim().split('\n');
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0]).toStartWith(`gstack auto-update: update held (tracked-by-project: ${fx.base} tracks`);
|
||||
expect(lines[0]).toContain('troubleshooting.md#auto-update-tracked-by-project');
|
||||
const cmd = lines[0].match(/The full fix: (bash .*?)\. https:/)![1];
|
||||
const r = run(fx, cmd);
|
||||
expect(r.status).toBe(6);
|
||||
expect(r.stdout).toContain(`git -C ${quoted(fx.base)} rm -r -q --cached -- ${quoted("it's an install")}`);
|
||||
});
|
||||
|
||||
// Value: protects=a project holding the install as a submodule gitlink keeps auto-updating; fails_when=the #1022 check counts a gitlink;
|
||||
// why_new=negative control for the case above; seam=none
|
||||
test('a gitlink in the enclosing project is not a hold: the update lands', async () => {
|
||||
const fx = makeFixture();
|
||||
const target = release(fx, 'top UPSTREAM\nmiddle\nbottom\n');
|
||||
git(fx.base, 'init', '-q', '-b', 'main');
|
||||
git(fx.base, 'update-index', '--add', '--cacheinfo', `160000,${git(fx.install, 'rev-parse', 'HEAD')},it's an install`);
|
||||
spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env(fx), timeout: 20_000 });
|
||||
await waitFor(fx, /Z UPDATED /);
|
||||
expect(git(fx.install, 'rev-parse', 'HEAD')).toBe(target);
|
||||
});
|
||||
|
||||
test('every anchor the lines link to exists', () => {
|
||||
const src = fs.readFileSync(SCRIPT, 'utf8');
|
||||
const anchors = [...src.matchAll(/troubleshooting\.md#([a-z0-9-]+)/g)].map((m) => m[1]!);
|
||||
for (const a of ['auto-update-unmerged-files', 'auto-update-autostash-unrecoverable', 'auto-update-rescued-edits']) expect(anchors).toContain(a);
|
||||
for (const a of ['auto-update-unmerged-files', 'auto-update-autostash-unrecoverable', 'auto-update-rescued-edits', 'auto-update-tracked-by-project']) expect(anchors).toContain(a);
|
||||
for (const a of anchors) expect(DOC, a).toContain(`<a id="${a}"></a>`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user