@zgeoff/atc 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client/build-workspace-destination.ts +38 -0
- package/src/client/pick-refusal-step.ts +29 -0
- package/src/client/resolve-workspace-root.ts +49 -0
- package/src/client/spawn-picker.ts +1655 -182
- package/src/shared/collect-workspaces-config.ts +25 -10
- package/src/shared/config.ts +17 -2
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
interface DestinationParts {
|
|
2
|
+
// The root the checkout lands under, absolute on its target.
|
|
3
|
+
readonly root: string;
|
|
4
|
+
|
|
5
|
+
// The repository URL the checkout comes from, as the daemon resolved it.
|
|
6
|
+
readonly url: string;
|
|
7
|
+
|
|
8
|
+
// The branch or tag the commit was chosen by, or null for a bare commit.
|
|
9
|
+
readonly ref: string | null;
|
|
10
|
+
readonly sha: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The default directory a git workspace lands in: `<repo>-<ref>-<short
|
|
15
|
+
* sha>` under the root, with the ref left out for a bare commit. The repo
|
|
16
|
+
* is the URL's last path segment without `.git`, and each part keeps only
|
|
17
|
+
* `[A-Za-z0-9._-]`, with `/` turned into `-`, so the name is one path
|
|
18
|
+
* segment on any target.
|
|
19
|
+
*/
|
|
20
|
+
export function buildWorkspaceDestination(parts: DestinationParts): string {
|
|
21
|
+
const repo =
|
|
22
|
+
parts.url
|
|
23
|
+
.replace(/\/+$/u, '')
|
|
24
|
+
.split(/[/:]/u)
|
|
25
|
+
.at(-1)
|
|
26
|
+
?.replace(/\.git$/u, '') ?? '';
|
|
27
|
+
|
|
28
|
+
const name = [repo, ...(parts.ref === null ? [] : [parts.ref]), parts.sha.slice(0, 7)]
|
|
29
|
+
.map((part) => toSafeSegment(part))
|
|
30
|
+
.filter((part) => part !== '')
|
|
31
|
+
.join('-');
|
|
32
|
+
|
|
33
|
+
return `${parts.root.replace(/\/+$/u, '')}/${name}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toSafeSegment(part: string): string {
|
|
37
|
+
return part.replaceAll('/', '-').replaceAll(/[^\w.-]/gu, '');
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The steps of the git spawn flow a refused spawn can return to.
|
|
2
|
+
type RefusalStep = 'confirm' | 'destination' | 'ref' | 'source';
|
|
3
|
+
|
|
4
|
+
const REFUSAL_STEPS: ReadonlyMap<string, RefusalStep> = new Map<string, RefusalStep>([
|
|
5
|
+
['workspace_exists', 'destination'],
|
|
6
|
+
['ref_not_found', 'ref'],
|
|
7
|
+
['invalid_git_url', 'source'],
|
|
8
|
+
['credential_in_url', 'source'],
|
|
9
|
+
['credential_missing', 'source'],
|
|
10
|
+
['clone_failed', 'source'],
|
|
11
|
+
['has_submodules', 'confirm'],
|
|
12
|
+
['lfs_unsupported', 'confirm'],
|
|
13
|
+
['unreadable_tree', 'confirm'],
|
|
14
|
+
['sanitize_failed', 'confirm'],
|
|
15
|
+
['tar_failed', 'confirm'],
|
|
16
|
+
['transfer_failed', 'confirm'],
|
|
17
|
+
['workspace_mismatch', 'confirm'],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The step of the git spawn flow that can fix a workspace spawn's
|
|
22
|
+
* refusal: the destination for one that already exists, the ref for one
|
|
23
|
+
* the upstream lacks, the source step for a URL or clone failure, and the
|
|
24
|
+
* confirm screen for a refusal of the commit or the target's copy of it.
|
|
25
|
+
* Null for any other refusal, which stays on the step that sent the spawn.
|
|
26
|
+
*/
|
|
27
|
+
export function pickRefusalStep(code: string): RefusalStep | null {
|
|
28
|
+
return REFUSAL_STEPS.get(code) ?? null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WorkspacesConfig } from '../shared/collect-workspaces-config';
|
|
2
|
+
import { resolveHomeDir } from '../shared/resolve-home-dir';
|
|
3
|
+
|
|
4
|
+
type WorkspaceRoot =
|
|
5
|
+
| { readonly ok: true; readonly root: string }
|
|
6
|
+
| { readonly ok: false; readonly message: string };
|
|
7
|
+
|
|
8
|
+
interface RootTarget {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
|
|
11
|
+
// Whether the target is the daemon's own machine, where `~` is this
|
|
12
|
+
// user's home.
|
|
13
|
+
readonly inPlace: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Where checkouts land on the daemon's own machine without a configured root.
|
|
17
|
+
const DEFAULT_ROOT = '~/.local/share/atc/workspaces';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The directory git workspaces land under on a target: the target's own
|
|
21
|
+
* root, else the global root, else the default. On the daemon's own
|
|
22
|
+
* machine a leading `~` expands to the home directory. On any other target
|
|
23
|
+
* the root must be absolute, since `~` there is a home this client cannot
|
|
24
|
+
* see, so a root that is not is refused with the config key to set.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveWorkspaceRoot(config: WorkspacesConfig, target: RootTarget): WorkspaceRoot {
|
|
27
|
+
const configured = config.targetRoots.get(target.id) ?? config.root;
|
|
28
|
+
|
|
29
|
+
if (target.inPlace) {
|
|
30
|
+
const root = expandHome(configured ?? DEFAULT_ROOT);
|
|
31
|
+
|
|
32
|
+
return root.startsWith('/')
|
|
33
|
+
? { ok: true, root }
|
|
34
|
+
: { ok: false, message: `workspaces root '${root}' must be an absolute path` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (configured === null || !configured.startsWith('/')) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
message: `set workspaces.targets.${target.id} in config.json to an absolute path on that target`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ok: true, root: configured };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function expandHome(dir: string): string {
|
|
48
|
+
return dir === '~' || dir.startsWith('~/') ? `${resolveHomeDir()}${dir.slice(1)}` : dir;
|
|
49
|
+
}
|