@coderook/cli 0.22.2 → 0.24.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.
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ /**
3
+ * The decisions the git remote helper makes, with nothing plugged in.
4
+ *
5
+ * Ordering a version graph, reading and writing the commit marker, parsing a
6
+ * remote URL, quoting a path for fast-import: none of it needs a network, a
7
+ * token, an upload engine or a git binary, and all of it is where a mistake is
8
+ * silent rather than loud. Kept apart so a test can exercise it directly —
9
+ * importing the helper itself drags in the uploader and the downloader, which
10
+ * is why this file exists at all.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.TRAILER = exports.EMPTY_TREE = void 0;
14
+ exports.parseRemoteUrl = parseRemoteUrl;
15
+ exports.commitFromMessage = commitFromMessage;
16
+ exports.messageWithCommit = messageWithCommit;
17
+ exports.messageWithoutMarker = messageWithoutMarker;
18
+ exports.tagOf = tagOf;
19
+ exports.branchOf = branchOf;
20
+ exports.versionsInOrder = versionsInOrder;
21
+ exports.stamp = stamp;
22
+ exports.importRef = importRef;
23
+ exports.quotePath = quotePath;
24
+ /**
25
+ * Git's constant hash for the empty tree.
26
+ *
27
+ * Diffing a root commit against this gives the same "everything is added"
28
+ * answer as a normal parent diff, so the first commit is not a special case
29
+ * with its own code path to get wrong.
30
+ */
31
+ exports.EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32
+ /**
33
+ * The line that ties a CodeRook version back to the git commit it came from.
34
+ *
35
+ * Written into the version message because it has to survive somewhere both
36
+ * machines can see. A file under `.git/` would be invisible to a colleague and
37
+ * to this same person on another laptop, and the consequence of losing the
38
+ * mapping is not a missing feature — it is re-pushing a history the project
39
+ * already holds, as a second copy. `git-svn` and `git-p4` put their marker in
40
+ * the message for the same reason.
41
+ */
42
+ exports.TRAILER = "CodeRook-Git-Commit:";
43
+ /** `coderook://project`, `coderook://owner/project`, `coderook::project`. */
44
+ function parseRemoteUrl(url) {
45
+ let rest = url.trim();
46
+ for (const prefix of ["coderook://", "coderook::", "coderook:"]) {
47
+ if (rest.startsWith(prefix)) {
48
+ rest = rest.slice(prefix.length);
49
+ break;
50
+ }
51
+ }
52
+ rest = rest.replace(/^\/+/, "").replace(/\/+$/, "");
53
+ // An owner segment is accepted and ignored: a token already says who this
54
+ // is, and silently pushing to a different account than the URL named would
55
+ // be a worse outcome than not supporting the form at all.
56
+ const parts = rest.split("/").filter(Boolean);
57
+ const slug = parts[parts.length - 1] ?? "";
58
+ if (!slug)
59
+ throw new Error(`Not a CodeRook remote URL: ${url}`);
60
+ return { slug };
61
+ }
62
+ /**
63
+ * Pull the git commit id back out of a version message, if it carries one.
64
+ *
65
+ * The *last* marker wins. A message can hold more than one after a round trip
66
+ * — clone a project, push it back, and the message carries the marker it was
67
+ * imported with plus the one this publish adds. Taking the first would name
68
+ * the commit from the original repository rather than the one just published,
69
+ * and every incremental push after that would compute its range from a commit
70
+ * this repository has never heard of.
71
+ */
72
+ function commitFromMessage(message) {
73
+ const all = [
74
+ ...message.matchAll(new RegExp(`${exports.TRAILER}\\s*([0-9a-f]{40})\\b`, "g")),
75
+ ];
76
+ return all.length ? all[all.length - 1][1] : null;
77
+ }
78
+ /** Attach the marker to a commit message without disturbing what it says. */
79
+ function messageWithCommit(message, sha) {
80
+ const body = message.replace(/\s+$/, "");
81
+ return `${body}\n\n${exports.TRAILER} ${sha}`;
82
+ }
83
+ /**
84
+ * The message as a person wrote it, with our bookkeeping taken back out.
85
+ *
86
+ * Used when rebuilding git commits: the marker names the commit a version came
87
+ * *from*, which is not the commit being created here, so leaving it in would
88
+ * stamp every imported commit with a false identity.
89
+ */
90
+ function messageWithoutMarker(message) {
91
+ return message
92
+ .replace(new RegExp(`^\\s*${exports.TRAILER}\\s*[0-9a-f]{40}\\s*$`, "gm"), "")
93
+ .replace(/\n{3,}/g, "\n\n")
94
+ .trim();
95
+ }
96
+ /** `refs/tags/v1.0` -> `v1.0`. Anything else is not a tag. */
97
+ function tagOf(ref) {
98
+ return ref.startsWith("refs/tags/") ? ref.slice("refs/tags/".length) : null;
99
+ }
100
+ /** `refs/heads/main` -> `main`. Anything else is not a branch. */
101
+ function branchOf(ref) {
102
+ return ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : null;
103
+ }
104
+ /**
105
+ * Every version reachable from these heads, oldest first.
106
+ *
107
+ * A plain sort by sequence would look right and be wrong: sequences are handed
108
+ * out per project as versions are made, so two lines interleave in a way that
109
+ * has nothing to do with ancestry. Walking `parentVersionIds` and emitting a
110
+ * version only once all of its parents have been emitted is what makes a
111
+ * commit's parents exist by the time git is told about them — fast-import
112
+ * refuses a mark it has not seen, so getting this wrong fails loudly, which is
113
+ * the one mercy of it.
114
+ */
115
+ function versionsInOrder(heads, all) {
116
+ const byId = new Map(all.map((version) => [version.id, version]));
117
+ // Everything the heads can reach. Iterative rather than recursive: a long
118
+ // project is thousands of versions deep and a stack is not.
119
+ const reachable = new Set();
120
+ const pending = [...heads];
121
+ while (pending.length) {
122
+ const id = pending.pop();
123
+ if (!id || reachable.has(id) || !byId.has(id))
124
+ continue;
125
+ reachable.add(id);
126
+ for (const parent of byId.get(id).parentVersionIds)
127
+ pending.push(parent);
128
+ }
129
+ const emitted = new Set();
130
+ const order = [];
131
+ /*
132
+ Depth-first with an explicit stack, visiting parents before the child. The
133
+ `expanded` flag is what separates "I have queued this one's parents" from
134
+ "its parents are done", without which a diamond history emits the join
135
+ twice.
136
+ */
137
+ const stack = heads
138
+ .filter((id) => reachable.has(id))
139
+ .map((id) => ({ id, expanded: false }));
140
+ while (stack.length) {
141
+ const frame = stack.pop();
142
+ if (emitted.has(frame.id))
143
+ continue;
144
+ const version = byId.get(frame.id);
145
+ if (!version)
146
+ continue;
147
+ if (frame.expanded) {
148
+ emitted.add(frame.id);
149
+ order.push(version);
150
+ continue;
151
+ }
152
+ stack.push({ id: frame.id, expanded: true });
153
+ for (const parent of version.parentVersionIds) {
154
+ if (reachable.has(parent) && !emitted.has(parent)) {
155
+ stack.push({ id: parent, expanded: false });
156
+ }
157
+ }
158
+ }
159
+ return order;
160
+ }
161
+ /** Seconds since the epoch, for a fast-import person stamp. */
162
+ function stamp(when) {
163
+ const at = Date.parse(when);
164
+ return Number.isFinite(at) ? Math.floor(at / 1000) : Math.floor(Date.now() / 1000);
165
+ }
166
+ /**
167
+ * Where an imported branch is written.
168
+ *
169
+ * Into the helper's own namespace rather than `refs/heads/*`, matching the
170
+ * `refspec` advertised in `capabilities`. Writing straight to `refs/heads/*`
171
+ * would have this remote overwrite local branches of the same name on every
172
+ * fetch.
173
+ */
174
+ function importRef(ref) {
175
+ const branch = branchOf(ref);
176
+ return branch ? `refs/coderook/${branch}` : ref;
177
+ }
178
+ /** fast-import wants a path C-quoted when it holds a quote or a newline. */
179
+ function quotePath(filePath) {
180
+ const normalised = filePath.replaceAll("\\", "/");
181
+ if (!/["\n\r]/.test(normalised))
182
+ return normalised;
183
+ return `"${normalised
184
+ .replaceAll("\\", "\\\\")
185
+ .replaceAll('"', '\\"')
186
+ .replaceAll("\n", "\\n")
187
+ .replaceAll("\r", "\\r")}"`;
188
+ }