@coderook/cli 0.25.4 → 0.27.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.
@@ -47,9 +47,19 @@ exports.main = main;
47
47
  * `coderook://` URLs, which is what lets every editor's built-in git panel work
48
48
  * with CodeRook without any per-editor work.
49
49
  *
50
- * Both directions are implemented: `push` publishes commits as versions, and
51
- * `import` rebuilds a git history from versions so `git clone` and `git fetch`
52
- * work.
50
+ * Both directions are implemented: `push` publishes each commit as a save, and
51
+ * `import` rebuilds a git history from those saves so `git clone` and `git
52
+ * fetch` work.
53
+ *
54
+ * The vocabulary matters here, because the two products do not agree by
55
+ * accident. A pushed commit becomes a *commit* in CodeRook — a save, inside
56
+ * the project, that nobody outside can see. Tagging it names it, and a named
57
+ * save is a *version*, which is the only thing the public side offers. That
58
+ * is the same distinction git already draws between a commit and a tag, so
59
+ * the mapping is one-to-one rather than an approximation.
60
+ *
61
+ * A save also carries a description of its own, separate from its message.
62
+ * That travels as a git note on `refs/notes/coderook` in both directions.
53
63
  *
54
64
  * ## Why the `push` capability rather than `export`
55
65
  *
@@ -143,6 +153,26 @@ function git(args) {
143
153
  }
144
154
  return result.stdout;
145
155
  }
156
+ /**
157
+ * Where CodeRook keeps a save's description inside a git repository.
158
+ *
159
+ * `refs/notes/coderook` rather than git's default `refs/notes/commits`,
160
+ * because the default is shared ground — anything else attaching notes writes
161
+ * there too, and a fetch that overwrote somebody's own notes with ours would
162
+ * be taking something that was not offered.
163
+ */
164
+ const NOTES_REF = "refs/notes/coderook";
165
+ /**
166
+ * The description attached to a commit, if there is one.
167
+ *
168
+ * Empty rather than throwing for every ordinary reason it can be missing: no
169
+ * notes ref at all, no note on this commit, or a repository where notes have
170
+ * never been used. None of those is a problem worth stopping a push for.
171
+ */
172
+ function noteOn(sha) {
173
+ const result = (0, node_child_process_1.spawnSync)("git", ["notes", `--ref=${NOTES_REF}`, "show", sha], { encoding: "utf8", maxBuffer: 1024 * 1024 * 8 });
174
+ return result.status === 0 ? (result.stdout ?? "").trim() : "";
175
+ }
146
176
  /** Run git for its exit status alone. False rather than throwing. */
147
177
  function gitOk(args) {
148
178
  return ((0, node_child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: "ignore" }).status === 0);
@@ -665,6 +695,40 @@ async function doImport(refs, url) {
665
695
  say(` ${done}/${fresh.length} versions`);
666
696
  }
667
697
  }
698
+ /*
699
+ What each save says about itself, as git notes.
700
+
701
+ A CodeRook save carries a description separate from its message — the
702
+ thing the website shows as release info and `cbx notes` writes — and a
703
+ fetch used to drop all of it except on the few versions that were also
704
+ releases. Git's answer for text attached to a commit without changing
705
+ the commit is a note, so that is where it goes: `git log
706
+ --notes=coderook` shows them, and pushing them back returns them.
707
+
708
+ Written as one commit on its own ref after every version commit exists,
709
+ because a note has to name a commit that is already in the stream.
710
+ */
711
+ const described = fresh.filter((version) => (version.notes ?? "").trim());
712
+ if (described.length) {
713
+ send(`commit ${(0, git_history_js_1.importRef)(NOTES_REF)}`);
714
+ send(`committer CodeRook <noreply@coderook.com> ` +
715
+ `${(0, git_history_js_1.stamp)(new Date().toISOString())} +0000`);
716
+ const why = Buffer.from("What each save says about itself\n", "utf8");
717
+ send(`data ${why.length}`);
718
+ node_process_1.default.stdout.write(why);
719
+ node_process_1.default.stdout.write("\n");
720
+ for (const version of described) {
721
+ const mark = commitMark.get(version.id);
722
+ if (mark === undefined)
723
+ continue;
724
+ const body = Buffer.from(`${(version.notes ?? "").trim()}\n`, "utf8");
725
+ send(`N inline :${mark}`);
726
+ send(`data ${body.length}`);
727
+ node_process_1.default.stdout.write(body);
728
+ node_process_1.default.stdout.write("\n");
729
+ }
730
+ say(` ${described.length} description${described.length === 1 ? "" : "s"} as git notes`);
731
+ }
668
732
  /* Every ref git asked for, named outright now the commits exist. */
669
733
  settleRefs();
670
734
  /*
@@ -856,7 +920,7 @@ async function doPush(requests, url) {
856
920
  const existing = await releases(repositoryId).catch(() => []);
857
921
  const already = existing.find((release) => release.versionId === versionId && release.name !== tag);
858
922
  if (already) {
859
- say(` note: v${already.sequence} was already released as "${already.name}".
923
+ say(` note: v${already.sequence} was already released as "${already.name}".
860
924
  ` +
861
925
  ` A version carries one name, so it is now "${tag}".`);
862
926
  }
@@ -1098,10 +1162,10 @@ async function doPush(requests, url) {
1098
1162
  happened and go looking for a way to undo it.
1099
1163
  */
1100
1164
  send(`error ${request.dst} the connection failed part way through`);
1101
- say(`
1102
- ${published} of ${commits.length} commits were published.
1165
+ say(`
1166
+ ${published} of ${commits.length} commits were published.
1103
1167
  ` +
1104
- ` Push again — commits already published are not sent twice.
1168
+ ` Push again — commits already published are not sent twice.
1105
1169
  `);
1106
1170
  throw error;
1107
1171
  }
@@ -1113,8 +1177,8 @@ async function doPush(requests, url) {
1113
1177
  anyway.
1114
1178
  */
1115
1179
  send(`error ${request.dst} this commit carries a credential`);
1116
- say(`
1117
- ${failure.message}
1180
+ say(`
1181
+ ${failure.message}
1118
1182
  `);
1119
1183
  /*
1120
1184
  Deleting the file in a later commit does not help, and saying
@@ -1124,14 +1188,14 @@ async function doPush(requests, url) {
1124
1188
  The history has to lose it.
1125
1189
  */
1126
1190
  say(` A later commit that deletes it is not enough — every commit` +
1127
- ` being pushed
1191
+ ` being pushed
1128
1192
  becomes a version, and the one that added` +
1129
- ` the key still carries it.
1193
+ ` the key still carries it.
1130
1194
  ` +
1131
1195
  ` Rewrite it out (git rebase -i, or git commit --amend if it` +
1132
- ` is the last one),
1196
+ ` is the last one),
1133
1197
  or publish this deliberately with` +
1134
- ` cbx submit --allow-secrets.
1198
+ ` cbx submit --allow-secrets.
1135
1199
  `);
1136
1200
  /*
1137
1201
  Reported already, and in more detail than the wrapper can. The
@@ -1141,8 +1205,8 @@ async function doPush(requests, url) {
1141
1205
  }
1142
1206
  if (failure?.kind === "conflict") {
1143
1207
  send(`error ${request.dst} somebody published to "${branch}" while this push was running`);
1144
- say(`
1145
- git fetch, then push again.
1208
+ say(`
1209
+ git fetch, then push again.
1146
1210
  `);
1147
1211
  throw error;
1148
1212
  }
@@ -1154,6 +1218,27 @@ async function doPush(requests, url) {
1154
1218
  }
1155
1219
  currentRepositoryId = result.repositoryId;
1156
1220
  baseVersionId = result.versionId;
1221
+ /*
1222
+ A note on the commit becomes what the version says about itself.
1223
+
1224
+ CodeRook lets a save be described before anybody decides to publish
1225
+ it, and git has exactly one place for text attached to a commit
1226
+ without altering it. Sent after the publish rather than with it,
1227
+ because a note is not part of what was saved and a failure to
1228
+ record one must not fail the push.
1229
+ */
1230
+ const note = noteOn(sha);
1231
+ if (note) {
1232
+ try {
1233
+ const { changeVersion } = await Promise.resolve().then(() => __importStar(require("./api.js")));
1234
+ await changeVersion(result.repositoryId, result.versionId, {
1235
+ notes: note,
1236
+ });
1237
+ }
1238
+ catch {
1239
+ say(` note: the description on ${sha.slice(0, 8)} could not be saved`);
1240
+ }
1241
+ }
1157
1242
  /*
1158
1243
  The version and the commit it came from are the same thing in this
1159
1244
  repository from here on. Recorded so a later fetch does not rebuild
@@ -131,20 +131,83 @@ async function commandVersions(parsed) {
131
131
  console.log(dim("Nothing has been saved to this project yet."));
132
132
  return 0;
133
133
  }
134
+ /*
135
+ The same two lanes the website shows.
136
+
137
+ A version is a commit somebody named, so the lanes are a split of one
138
+ history rather than two histories — asking for commits gives the saves
139
+ still waiting on that decision, and asking for versions gives the ones
140
+ the public side can see. Neither contains the other; with no flag you get
141
+ both, which is what this always did.
142
+ */
143
+ const onlyVersions = parsed.flags.get("versions") === true;
144
+ const onlyCommits = parsed.flags.get("commits") === true;
145
+ if (onlyVersions && onlyCommits) {
146
+ console.error(red("Pick one: --versions or --commits, or neither for both."));
147
+ return 1;
148
+ }
149
+ const lane = saved.filter((version) => {
150
+ if (onlyVersions)
151
+ return Boolean(version.name);
152
+ if (onlyCommits)
153
+ return !version.name;
154
+ return true;
155
+ });
156
+ if (!lane.length) {
157
+ console.log(dim(onlyVersions
158
+ ? "Nothing has been made a version yet."
159
+ : "Every save here has been made a version."));
160
+ return 0;
161
+ }
162
+ const withNotes = parsed.flags.get("notes") === true;
134
163
  const asked = Number(parsed.flags.get("limit") ?? 20);
135
164
  const limit = Number.isFinite(asked) && asked > 0 ? asked : 20;
136
165
  console.log(bold(project.name));
137
- for (const version of saved.slice(0, limit)) {
166
+ for (const version of lane.slice(0, limit)) {
138
167
  const when = version.createdAt
139
168
  ? new Date(version.createdAt).toLocaleString()
140
169
  : "";
170
+ /*
171
+ What the project has made of this version, said before the numbers.
172
+
173
+ A name, a pin and a set of labels are how somebody finds the version
174
+ they meant among forty; the file count is how they check it once they
175
+ have. Putting the arrangement first is the difference between a log and
176
+ a list they organised.
177
+ */
178
+ const marks = [
179
+ /* Which one the project is standing on, which is not the same as which
180
+ one is at the top once somebody has pinned something. */
181
+ version.head ? green("current") : "",
182
+ version.pinned ? accent("pinned") : "",
183
+ version.name ? bold(version.name) : "",
184
+ version.state === "held" ? red("held") : "",
185
+ version.state === "declined" ? dim("declined") : "",
186
+ version.removedAt ? dim("taken down") : "",
187
+ /*
188
+ Said only of a version, because only a version is offered to anybody.
189
+ A commit nobody has promoted is not hidden — it was never on offer.
190
+ */
191
+ version.name && version.visibility === "private" ? dim("hidden") : "",
192
+ ...version.labels.map((label) => accent(label.name)),
193
+ ].filter(Boolean);
141
194
  console.log(` ${accent(`v${version.sequence}`).padEnd(16)} ${when.padEnd(22)}` +
142
- `${String(version.fileCount).padStart(5)} files ${bytes(version.storedSize)}`);
195
+ `${String(version.fileCount).padStart(5)} files ${bytes(version.storedSize)}` +
196
+ (marks.length ? ` ${marks.join(" ")}` : ""));
143
197
  if (version.message)
144
198
  console.log(` ${dim(version.message)}`);
199
+ /*
200
+ Indented under the save it belongs to rather than printed flat, so a
201
+ note of several paragraphs still reads as part of one row.
202
+ */
203
+ if (withNotes && version.notes?.trim()) {
204
+ for (const line of version.notes.trim().split(/\r?\n/)) {
205
+ console.log(` ${line}`);
206
+ }
207
+ }
145
208
  }
146
- if (saved.length > limit) {
147
- console.log(dim(` … ${saved.length - limit} older. Use --limit to see more.`));
209
+ if (lane.length > limit) {
210
+ console.log(dim(` … ${lane.length - limit} older. Use --limit to see more.`));
148
211
  }
149
212
  return 0;
150
213
  }
@@ -105,7 +105,8 @@ async function commandRelease(parsed) {
105
105
  const wanted = parsed.flags.get("version");
106
106
  const target = typeof wanted === "string"
107
107
  ? all.find((version) => String(version.sequence) === wanted.replace(/^v/i, ""))
108
- : all[0];
108
+ /* The newest by number rather than by position in the list. */
109
+ : all.reduce((newest, one) => (one.sequence > newest.sequence ? one : newest));
109
110
  if (!target) {
110
111
  console.error(red(`This project has no version ${String(wanted)}.`));
111
112
  return 1;