docs: explain repository sync boundaries
This commit is contained in:
parent
b21a1387a5
commit
04e6ba60e8
3 changed files with 61 additions and 2 deletions
25
README.md
25
README.md
|
|
@ -291,6 +291,31 @@ stoke repo clone -o heavy-duty -r stoke ~/src/stoke --depth 1
|
|||
|
||||
The stored token is handed to git ephemerally through environment-based config (`GIT_CONFIG_*`): it never appears in the remote URL, on the command line, or in the cloned repository's `.git/config`. Git's output is streamed directly and its exit status is forwarded, so failures behave exactly like a plain `git clone`.
|
||||
|
||||
### `stoke repo sync`
|
||||
|
||||
Fast-forward an undiverged imported repository from an upstream Git URL.
|
||||
|
||||
```text
|
||||
Options:
|
||||
-o, --owner <owner> repository owner (required)
|
||||
-r, --repo <repo> repository name (required)
|
||||
--from <upstream-url> upstream Git URL (required)
|
||||
--branch <branch> branch to synchronize (default: the forge repository's default branch)
|
||||
--tags also create new upstream tags
|
||||
--dry-run report branch and tag moves without pushing
|
||||
```
|
||||
|
||||
```bash
|
||||
stoke repo sync -o heavy-duty -r box \
|
||||
--from https://github.com/heavy-duty/box.git
|
||||
stoke repo sync -o heavy-duty -r box \
|
||||
--from https://github.com/heavy-duty/box.git --tags --dry-run
|
||||
```
|
||||
|
||||
The command fetches both branch tips into an ephemeral bare repository and pushes only when the forge tip is an ancestor of the upstream tip. It refuses a diverged tree with both commit SHAs and never offers a force option. With `--tags`, new upstream tags are created; an existing forge tag that points elsewhere is reported and left untouched, and the command exits non-zero after applying any other safe moves. The stored Forgejo token uses the same environment-only Git authentication as `repo clone` and is never written to an argument, remote, or Git config.
|
||||
|
||||
This verb deliberately does not merge diverged trees, configure Forgejo pull-mirrors, or copy releases. Follow ceremony's live `docs/UPSTREAM-SYNC.md` procedure for a diverged tree; import a scheduled read-only repository as a pull-mirror; compose release mirroring from `release create` and `release upload`.
|
||||
|
||||
### `stoke repo create`
|
||||
|
||||
Create a new repository for the authenticated user or an organization.
|
||||
|
|
|
|||
1
changelog.d/23.md
Normal file
1
changelog.d/23.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Added `repo sync` for credential-safe, fast-forward-only branch and tag updates with dry-run and divergence protection. (#23).
|
||||
|
|
@ -7,6 +7,7 @@ const path = require('node:path');
|
|||
|
||||
const CLI = path.join(__dirname, '..', 'src', 'cli.js');
|
||||
const TOKEN = 'stoke-secret-token-for-sync-tests';
|
||||
const REAL_GIT = execFileSync('which', ['git'], { encoding: 'utf8' }).trim();
|
||||
|
||||
function git(args, cwd) {
|
||||
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
|
||||
|
|
@ -64,7 +65,7 @@ function refSha(repository, ref) {
|
|||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
}
|
||||
|
||||
function runSync(fx, extra = [], { branch = 'main' } = {}) {
|
||||
function runSync(fx, extra = [], { branch = 'main', env = {} } = {}) {
|
||||
const args = [
|
||||
CLI,
|
||||
'repo',
|
||||
|
|
@ -77,7 +78,7 @@ function runSync(fx, extra = [], { branch = 'main' } = {}) {
|
|||
args.push(...extra);
|
||||
return spawnSync(process.execPath, args, {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, STOKE_CONFIG_FILE: fx.config },
|
||||
env: { ...process.env, STOKE_CONFIG_FILE: fx.config, ...env },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -179,3 +180,35 @@ test('repo sync --dry-run reports branch and tag moves without writing', () => {
|
|||
fx.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('repo sync keeps the token out of Git argv, output, remotes, and config', () => {
|
||||
const fx = fixture();
|
||||
try {
|
||||
const wrapperDirectory = path.join(fx.root, 'bin');
|
||||
const argvLog = path.join(fx.root, 'git-argv.log');
|
||||
const wrapper = path.join(wrapperDirectory, 'git');
|
||||
fs.mkdirSync(wrapperDirectory);
|
||||
fs.writeFileSync(wrapper, `#!/bin/sh\nprintf '%s\\n' "$@" >> "$STOKE_TEST_GIT_ARGV"\nexec "${REAL_GIT}" "$@"\n`);
|
||||
fs.chmodSync(wrapper, 0o755);
|
||||
|
||||
const result = runSync(fx, [], {
|
||||
env: {
|
||||
PATH: `${wrapperDirectory}:${process.env.PATH}`,
|
||||
STOKE_TEST_GIT_ARGV: argvLog,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
for (const text of [
|
||||
result.stdout,
|
||||
result.stderr,
|
||||
fs.readFileSync(argvLog, 'utf8'),
|
||||
fs.readFileSync(path.join(fx.forgeRepo, 'config'), 'utf8'),
|
||||
fs.readFileSync(path.join(fx.upstreamRepo, 'config'), 'utf8'),
|
||||
]) {
|
||||
assert.ok(!text.includes(TOKEN), 'token leaked from the environment-only auth path');
|
||||
}
|
||||
} finally {
|
||||
fx.cleanup();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue