mirror of https://github.com/garrytan/gstack.git
fix(slug): avoid parent repo identity in subdirs
This commit is contained in:
parent
026751ea20
commit
942e049514
|
|
@ -8,30 +8,63 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CACHE_DIR="$HOME/.gstack/slug-cache"
|
CACHE_DIR="$HOME/.gstack/slug-cache"
|
||||||
PROJECT_DIR="$(pwd)"
|
PROJECT_DIR="$(pwd -P 2>/dev/null || pwd)"
|
||||||
# Encode absolute path as cache key: /Users/j/foo → _Users_j_foo
|
# Encode absolute path as cache key: /Users/j/foo → _Users_j_foo
|
||||||
CACHE_KEY=$(printf '%s' "$PROJECT_DIR" | tr '/' '_')
|
CACHE_KEY=$(printf '%s' "$PROJECT_DIR" | tr '/' '_')
|
||||||
CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"
|
CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"
|
||||||
|
|
||||||
# 1. Try cached slug first (guarantees consistency across sessions)
|
sanitize_slug() {
|
||||||
if [[ -f "$CACHE_FILE" ]]; then
|
printf '%s' "$1" | tr -cd 'a-zA-Z0-9._-'
|
||||||
SLUG=$(cat "$CACHE_FILE")
|
}
|
||||||
|
|
||||||
|
find_slug_override() {
|
||||||
|
local dir="$PROJECT_DIR"
|
||||||
|
while [[ -n "$dir" && "$dir" != "/" ]]; do
|
||||||
|
if [[ -f "$dir/.gstack-slug" ]]; then
|
||||||
|
head -n 1 "$dir/.gstack-slug" 2>/dev/null | tr -d '\r\n' || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
dir="$(dirname "$dir")"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Explicit project overrides beat cache and git inference. This lets users
|
||||||
|
# recover from stale slug-cache entries without editing cache internals.
|
||||||
|
OVERRIDE_SLUG=$(find_slug_override 2>/dev/null || true)
|
||||||
|
if [[ -n "$OVERRIDE_SLUG" ]]; then
|
||||||
|
SLUG=$(sanitize_slug "$OVERRIDE_SLUG")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. If no cache, compute from git remote (separated from pipeline to avoid
|
# 2. If the current directory is the git root, compute from that repo's remote.
|
||||||
# pipefail swallowing the error and producing an empty slug)
|
# If it is only a subdirectory of a parent repo, do not inherit the parent
|
||||||
|
# repo's identity; use the directory basename instead.
|
||||||
if [[ -z "${SLUG:-}" ]]; then
|
if [[ -z "${SLUG:-}" ]]; then
|
||||||
REMOTE_URL=$(git remote get-url origin 2>/dev/null) || REMOTE_URL=""
|
GIT_TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) || GIT_TOPLEVEL=""
|
||||||
if [[ -n "$REMOTE_URL" ]]; then
|
if [[ -n "$GIT_TOPLEVEL" ]]; then
|
||||||
RAW_SLUG=$(printf '%s' "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
|
GIT_TOPLEVEL=$(cd "$GIT_TOPLEVEL" 2>/dev/null && pwd -P) || GIT_TOPLEVEL=""
|
||||||
SLUG=$(printf '%s' "$RAW_SLUG" | tr -cd 'a-zA-Z0-9._-')
|
fi
|
||||||
|
if [[ -n "$GIT_TOPLEVEL" && "$GIT_TOPLEVEL" == "$PROJECT_DIR" ]]; then
|
||||||
|
REMOTE_URL=$(git remote get-url origin 2>/dev/null) || REMOTE_URL=""
|
||||||
|
if [[ -n "$REMOTE_URL" ]]; then
|
||||||
|
RAW_SLUG=$(printf '%s' "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
|
||||||
|
SLUG=$(sanitize_slug "$RAW_SLUG")
|
||||||
|
fi
|
||||||
|
elif [[ -n "$GIT_TOPLEVEL" ]]; then
|
||||||
|
SLUG=$(sanitize_slug "$(basename "$PROJECT_DIR")")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Fallback to basename only when there's truly no git remote configured
|
# 3. Cache is a fallback for transient git/remote failures, not an immutable
|
||||||
SLUG="${SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}"
|
# source of truth when override or current repo inference is available.
|
||||||
|
if [[ -z "${SLUG:-}" && -f "$CACHE_FILE" ]]; then
|
||||||
|
SLUG=$(sanitize_slug "$(cat "$CACHE_FILE")")
|
||||||
|
fi
|
||||||
|
|
||||||
# 4. Cache the slug for future sessions (atomic write, fail silently)
|
# 4. Fallback to basename only when there is no usable override, repo, or cache.
|
||||||
|
SLUG="${SLUG:-$(sanitize_slug "$(basename "$PROJECT_DIR")")}"
|
||||||
|
|
||||||
|
# 5. Cache the slug for future sessions (atomic write, fail silently)
|
||||||
if [[ -n "$SLUG" ]]; then
|
if [[ -n "$SLUG" ]]; then
|
||||||
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
||||||
CACHE_TMP=$(mktemp "$CACHE_DIR/.slug-XXXXXX" 2>/dev/null) || CACHE_TMP=""
|
CACHE_TMP=$(mktemp "$CACHE_DIR/.slug-XXXXXX" 2>/dev/null) || CACHE_TMP=""
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ALL_COMMANDS, COMMAND_DESCRIPTIONS, READ_COMMANDS, WRITE_COMMANDS, META
|
||||||
import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
|
import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
const ROOT = path.resolve(import.meta.dir, '..');
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
|
||||||
|
|
@ -972,6 +973,68 @@ describe('gstack-slug', () => {
|
||||||
expect(slug).toMatch(/^[a-zA-Z0-9._-]+$/);
|
expect(slug).toMatch(/^[a-zA-Z0-9._-]+$/);
|
||||||
expect(branch).toMatch(/^[a-zA-Z0-9._-]+$/);
|
expect(branch).toMatch(/^[a-zA-Z0-9._-]+$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subdirectory inside parent git repo falls back to basename and refreshes poisoned cache', () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-slug-subdir-'));
|
||||||
|
try {
|
||||||
|
const home = path.join(tmp, 'home');
|
||||||
|
const outer = path.join(tmp, 'workspace');
|
||||||
|
const child = path.join(outer, 'IoTopia');
|
||||||
|
fs.mkdirSync(child, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(home, '.gstack', 'slug-cache'), { recursive: true });
|
||||||
|
|
||||||
|
Bun.spawnSync(['git', 'init', '-q'], { cwd: outer });
|
||||||
|
Bun.spawnSync(['git', 'remote', 'add', 'origin', 'git@github.com:me/workspace.git'], { cwd: outer });
|
||||||
|
|
||||||
|
const cacheKey = fs.realpathSync(child).replaceAll('/', '_');
|
||||||
|
fs.writeFileSync(path.join(home, '.gstack', 'slug-cache', cacheKey), 'me-workspace');
|
||||||
|
|
||||||
|
const result = Bun.spawnSync([SLUG_BIN], {
|
||||||
|
cwd: child,
|
||||||
|
env: { ...process.env, HOME: home },
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout.toString()).toContain('SLUG=IoTopia');
|
||||||
|
expect(fs.readFileSync(path.join(home, '.gstack', 'slug-cache', cacheKey), 'utf-8')).toBe('IoTopia');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('.gstack-slug override beats git inference and stale cache', () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-slug-override-'));
|
||||||
|
try {
|
||||||
|
const home = path.join(tmp, 'home');
|
||||||
|
const project = path.join(tmp, 'project');
|
||||||
|
const nested = path.join(project, 'wp-content', 'themes', 'theme');
|
||||||
|
fs.mkdirSync(nested, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(home, '.gstack', 'slug-cache'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(project, '.gstack-slug'), 'canonical-project');
|
||||||
|
|
||||||
|
Bun.spawnSync(['git', 'init', '-q'], { cwd: nested });
|
||||||
|
Bun.spawnSync(['git', 'remote', 'add', 'origin', 'git@github.com:org/theme.git'], { cwd: nested });
|
||||||
|
|
||||||
|
const cacheKey = fs.realpathSync(nested).replaceAll('/', '_');
|
||||||
|
fs.writeFileSync(path.join(home, '.gstack', 'slug-cache', cacheKey), 'org-theme');
|
||||||
|
|
||||||
|
const result = Bun.spawnSync([SLUG_BIN], {
|
||||||
|
cwd: nested,
|
||||||
|
env: { ...process.env, HOME: home },
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout.toString()).toContain('SLUG=canonical-project');
|
||||||
|
expect(fs.readFileSync(path.join(home, '.gstack', 'slug-cache', cacheKey), 'utf-8')).toBe('canonical-project');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('eval sets variables under bash with set -euo pipefail', () => {
|
test('eval sets variables under bash with set -euo pipefail', () => {
|
||||||
const result = Bun.spawnSync(
|
const result = Bun.spawnSync(
|
||||||
['bash', '-c', 'set -euo pipefail; eval "$(./bin/gstack-slug 2>/dev/null)"; echo "SLUG=$SLUG"; echo "BRANCH=$BRANCH"'],
|
['bash', '-c', 'set -euo pipefail; eval "$(./bin/gstack-slug 2>/dev/null)"; echo "SLUG=$SLUG"; echo "BRANCH=$BRANCH"'],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue