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
8 changes: 4 additions & 4 deletions org.coloradomesh.MeshClient.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ modules:
path: .
- flatpak/generated-sources.json
- type: archive
url: https://github.com/pnpm/pnpm/releases/download/v11.21.0/pnpm-linux-x64.tar.gz
sha256: aadc489ce4473c2af0fec06a5c19e113b5793d404eac49853c8153bf4b0d8263
url: https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-linux-x64.tar.gz
sha256: 4c592fa410eb23b69691a9efb9bf21c87c15b3e9d88c6ec8acdd354a0eb8de71
dest: pnpm-vendor
# pnpm tarball has root-level `pnpm` + `dist/`; default strip-components:1 drops the binary.
strip-components: 0
only-arches: [x86_64]
- type: archive
url: https://github.com/pnpm/pnpm/releases/download/v11.21.0/pnpm-linux-arm64.tar.gz
sha256: 64eb219b008f7a4c176d81fbce919c20b0b7e093e815cbb669d46e681451f43b
url: https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-linux-arm64.tar.gz
sha256: f1426231f365bdfd46c15fa3d1211c3936ee2c4e557afd304f6c66dbf1b2a8bf
dest: pnpm-vendor
strip-components: 0
only-arches: [aarch64]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@
"vitest-axe": "1.0.0-pre.5",
"zustand": "^5.0.15"
},
"packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
"engines": {
"node": ">=22.13.0",
"pnpm": ">=11.0.0"
Expand Down
48 changes: 24 additions & 24 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 21 additions & 5 deletions scripts/clone-ratspeak-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,31 @@ ensure_repo() {
ENSURE_REPO_SELECTED_REF="${target_ref}"

if [[ -n "$(git -C "${dir}" status --porcelain)" ]]; then
# Overlays leave siblings dirty after a successful float — allow that when already
# on the target tip. Refuse only when checkout would rewrite a dirty tree.
# Overlays leave checkouts dirty after a successful float — allow that when already
# on the target tip. When floating/pinning to a new SHA:
# - Default .rsstack workspace is disposable overlay cache → hard-reset, then checkout
# (overlays are re-applied after ensure_repo returns for RNS/LXMF).
# - External WORKSPACE_ROOT (sibling clones with real WIP) still refuses unless
# RS_STACK_DISCARD_DIRTY=1.
if [[ "${current_head}" == "${target_sha}" ]]; then
echo "warning: ${dir} has uncommitted changes; already at ${target_ref} (${target_sha:0:12}), skipping checkout" >&2
return 0
fi
echo "error: ${dir} has uncommitted changes; refuse to float/pin to ${target_ref} (${target_sha:0:12}) from ${current_head:0:12} (stash or reset, then re-run)" >&2
git -C "${dir}" status --short >&2 || true
exit 1
local discard_dirty=0
if [[ "${RS_STACK_DISCARD_DIRTY:-}" == '1' ]]; then
discard_dirty=1
elif [[ "$(basename "${WORKSPACE_ROOT}")" == '.rsstack' ]]; then
discard_dirty=1
fi
if [[ "${discard_dirty}" -eq 1 ]]; then
echo "warning: ${dir} has uncommitted changes; discarding to float/pin to ${target_ref} (${target_sha:0:12}) from ${current_head:0:12}" >&2
git -C "${dir}" reset --hard HEAD > /dev/null
git -C "${dir}" clean -fd > /dev/null
else
echo "error: ${dir} has uncommitted changes; refuse to float/pin to ${target_ref} (${target_sha:0:12}) from ${current_head:0:12} (stash or reset, or set RS_STACK_DISCARD_DIRTY=1)" >&2
git -C "${dir}" status --short >&2 || true
exit 1
fi
fi

if [[ "${current_head}" != "${target_sha}" ]]; then
Expand Down
95 changes: 86 additions & 9 deletions scripts/clone-ratspeak-stack.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,28 @@ function createLocalRemote({ defaultBranch = 'main', pinTag = null } = {}) {
return { remote, tipSha, pinSha };
}

function runEnsureRepo({ remoteUrl, destDir, pinRef = '' }) {
function runEnsureRepo({ remoteUrl, destDir, pinRef = '', env = {}, mergeStderr = false }) {
// Plain strings so bash ${...}/$(...) is not JS template interpolation.
const ensureCall =
'ensure_repo ' +
JSON.stringify(destDir) +
' ' +
JSON.stringify(remoteUrl) +
' ' +
JSON.stringify(pinRef) +
" 'rsLXST'" +
(mergeStderr ? ' 2>&1' : '');
const script = [
'set -euo pipefail',
'source ' + JSON.stringify(cloneScriptPath),
'ensure_repo ' +
JSON.stringify(destDir) +
' ' +
JSON.stringify(remoteUrl) +
' ' +
JSON.stringify(pinRef) +
" 'rsLXST'",
ensureCall,
'echo "SELECTED=${ENSURE_REPO_SELECTED_REF}"',
'echo "MODE=$(format_repo_mode "${ENSURE_REPO_SELECTED_REF}" ' + JSON.stringify(pinRef) + ')"',
'echo "SHA=$(git -C ' + JSON.stringify(destDir) + ' rev-parse HEAD)"',
].join('\n');
return execFileSync('bash', ['-c', script], {
encoding: 'utf8',
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' },
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', ...env },
});
}

Expand All @@ -93,6 +96,10 @@ describe('clone-ratspeak-stack.sh float policy', () => {
expect(cloneScript).toContain('export RS_RETICULUM_DIR=');
expect(cloneScript).toContain('export RS_LXMF_DIR=');
expect(cloneScript).toContain('refuse to float/pin');
expect(cloneScript).toContain('RS_STACK_DISCARD_DIRTY');
expect(cloneScript).toContain('discarding to float/pin');
expect(cloneScript).toContain('basename "${WORKSPACE_ROOT}"');
expect(cloneScript).toContain("== '.rsstack'");
expect(cloneScript).toContain('already at');
expect(cloneScript).toContain('skipping checkout');
expect(cloneScript).toContain('origin/${ref_or_empty}');
Expand Down Expand Up @@ -124,6 +131,76 @@ describe('clone-ratspeak-stack.sh float policy', () => {
expect(git(dest, 'rev-parse', 'HEAD')).toBe(tipSha);
});

it('ensure_repo discards dirty overlay state under .rsstack workspace to float', () => {
const { remote, tipSha } = createLocalRemote({ defaultBranch: 'main' });
const dest = join(makeTempDir('workspace-'), 'rsLXST');
runEnsureRepo({ remoteUrl: remote, destDir: dest });
expect(git(dest, 'rev-parse', 'HEAD')).toBe(tipSha);

// Advance remote tip after initial clone so float must move HEAD.
const seed = makeTempDir('rsLXST-advance-');
git(seed, 'clone', remote, '.');
git(seed, 'config', 'user.email', 'test@example.com');
git(seed, 'config', 'user.name', 'test');
writeFileSync(join(seed, 'NEXT'), 'next\n');
git(seed, 'add', 'NEXT');
git(seed, 'commit', '-m', 'next');
const newTip = git(seed, 'rev-parse', 'HEAD');
git(seed, 'push', 'origin', 'HEAD:main');
expect(newTip).not.toBe(tipSha);

writeFileSync(join(dest, 'OVERLAY'), 'dirty\n');
expect(git(dest, 'status', '--porcelain')).toContain('OVERLAY');

// Default WORKSPACE_ROOT from sourced script is repo .rsstack → discard dirty.
const out = runEnsureRepo({ remoteUrl: remote, destDir: dest, mergeStderr: true });
expect(out).toContain('discarding to float/pin');
expect(out).toContain(`SHA=${newTip}`);
expect(git(dest, 'rev-parse', 'HEAD')).toBe(newTip);
expect(git(dest, 'status', '--porcelain')).toBe('');
});

it('ensure_repo refuses dirty float outside .rsstack unless RS_STACK_DISCARD_DIRTY=1', () => {
const { remote, tipSha } = createLocalRemote({ defaultBranch: 'main' });
const dest = join(makeTempDir('siblings-'), 'rsLXST');
runEnsureRepo({ remoteUrl: remote, destDir: dest });
expect(git(dest, 'rev-parse', 'HEAD')).toBe(tipSha);

const seed = makeTempDir('rsLXST-advance-');
git(seed, 'clone', remote, '.');
git(seed, 'config', 'user.email', 'test@example.com');
git(seed, 'config', 'user.name', 'test');
writeFileSync(join(seed, 'NEXT'), 'next\n');
git(seed, 'add', 'NEXT');
git(seed, 'commit', '-m', 'next');
const newTip = git(seed, 'rev-parse', 'HEAD');
git(seed, 'push', 'origin', 'HEAD:main');
expect(newTip).not.toBe(tipSha);

writeFileSync(join(dest, 'WIP'), 'keep\n');

const externalWorkspace = makeTempDir('not-rsstack-');
expect(() =>
runEnsureRepo({
remoteUrl: remote,
destDir: dest,
env: { WORKSPACE_ROOT: externalWorkspace },
}),
).toThrow(/refuse to float\/pin/);
expect(git(dest, 'rev-parse', 'HEAD')).toBe(tipSha);
expect(git(dest, 'status', '--porcelain')).toContain('WIP');

const out = runEnsureRepo({
remoteUrl: remote,
destDir: dest,
mergeStderr: true,
env: { WORKSPACE_ROOT: externalWorkspace, RS_STACK_DISCARD_DIRTY: '1' },
});
expect(out).toContain('discarding to float/pin');
expect(out).toContain(`SHA=${newTip}`);
expect(git(dest, 'status', '--porcelain')).toBe('');
});

it('ensure_repo falls back to origin/master when main is absent', () => {
const { remote, tipSha } = createLocalRemote({ defaultBranch: 'master' });
const dest = join(makeTempDir('workspace-'), 'rsLXST');
Expand Down
Loading