dw-mc 0.5.1 → 0.7.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/README.md +23 -19
- package/dist/bin.js +590 -147
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -547,9 +547,60 @@ const textStoreFor = Effect.fn("store.textStoreFor")(function* (namespace) {
|
|
|
547
547
|
* bargain is struck once rather than at each of them.
|
|
548
548
|
*/
|
|
549
549
|
const remembered = (read) => Effect.orElseSucceed(read, () => Option.none());
|
|
550
|
+
/**
|
|
551
|
+
* Every key the state directory holds, whichever namespace it sits in.
|
|
552
|
+
*
|
|
553
|
+
* A key/value store answers about a key it is given and never lists one, and
|
|
554
|
+
* forgetting a pull request needs the list: a review run is kept under the head
|
|
555
|
+
* it read, and nothing on hand names every head a pull request was reviewed at.
|
|
556
|
+
*/
|
|
557
|
+
var Keys = class extends Context.Service()("dw-mc/store/Keys") {};
|
|
558
|
+
/**
|
|
559
|
+
* The key a file in the state directory was written under, and none for a
|
|
560
|
+
* file no key could have been written as: the file store names every file it
|
|
561
|
+
* writes by percent-encoding its key, so a name that does not decode is
|
|
562
|
+
* something else put there.
|
|
563
|
+
*/
|
|
564
|
+
const keyOf$1 = (entry) => {
|
|
565
|
+
try {
|
|
566
|
+
return [decodeURIComponent(entry)];
|
|
567
|
+
} catch {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* The keys a file store over `directory` holds: one file each, named by the
|
|
573
|
+
* percent-encoded key.
|
|
574
|
+
*
|
|
575
|
+
* The clones and the checkouts sit beside them as directories, and none of
|
|
576
|
+
* them is a key.
|
|
577
|
+
*/
|
|
578
|
+
const keysOnDisk = (directory) => Layer.effect(Keys, Effect.gen(function* () {
|
|
579
|
+
const fs = yield* FileSystem.FileSystem;
|
|
580
|
+
const directories = /* @__PURE__ */ new Set([clonesIn, ...cuts]);
|
|
581
|
+
return { all: fs.readDirectory(directory).pipe(Effect.map((entries) => entries.filter((entry) => !directories.has(entry)).flatMap(keyOf$1)), Effect.mapError((cause) => new KeyValueStore.KeyValueStoreError({
|
|
582
|
+
method: "keys",
|
|
583
|
+
message: "Unable to list the keys",
|
|
584
|
+
cause
|
|
585
|
+
}))) };
|
|
586
|
+
}));
|
|
587
|
+
/** Every key the state directory holds. */
|
|
588
|
+
const allKeys = Effect.gen(function* () {
|
|
589
|
+
return yield* (yield* Keys).all;
|
|
590
|
+
});
|
|
550
591
|
/** The state directory on disk. */
|
|
551
|
-
const layer$1 = Layer.unwrap(Effect.map(stateDirectory, (directory) => KeyValueStore.layerFileSystem(directory)));
|
|
552
|
-
|
|
592
|
+
const layer$1 = Layer.unwrap(Effect.map(stateDirectory, (directory) => Layer.merge(KeyValueStore.layerFileSystem(directory), keysOnDisk(directory))));
|
|
593
|
+
Layer.effectContext(Effect.gen(function* () {
|
|
594
|
+
const inner = yield* KeyValueStore.KeyValueStore;
|
|
595
|
+
const held = /* @__PURE__ */ new Set();
|
|
596
|
+
const store = KeyValueStore.make({
|
|
597
|
+
...inner,
|
|
598
|
+
set: (key, value) => Effect.tap(inner.set(key, value), () => Effect.sync(() => held.add(key))),
|
|
599
|
+
remove: (key) => Effect.tap(inner.remove(key), () => Effect.sync(() => held.delete(key))),
|
|
600
|
+
clear: Effect.tap(inner.clear, () => Effect.sync(() => held.clear()))
|
|
601
|
+
});
|
|
602
|
+
return Context.make(KeyValueStore.KeyValueStore, store).pipe(Context.add(Keys, { all: Effect.sync(() => [...held]) }));
|
|
603
|
+
})).pipe(Layer.provide(KeyValueStore.layerMemory));
|
|
553
604
|
/** The three directories the tool cuts a checkout into, under the state directory. */
|
|
554
605
|
const cuts = [
|
|
555
606
|
"worktrees",
|
|
@@ -708,6 +759,92 @@ const tidy = Effect.fn("store.tidy")(function* (directory, upTo) {
|
|
|
708
759
|
}
|
|
709
760
|
});
|
|
710
761
|
//#endregion
|
|
762
|
+
//#region src/adapters/picker.ts
|
|
763
|
+
/**
|
|
764
|
+
* Turns quitting into an answer rather than a failure.
|
|
765
|
+
*
|
|
766
|
+
* Bailing out of a prompt gives `None`, so no caller has to catch an error to
|
|
767
|
+
* learn that I walked away. The prompt itself decides what a `Some` carries.
|
|
768
|
+
*/
|
|
769
|
+
const orNone = (prompt) => Effect.catchTag(prompt, "QuitError", () => Effect.succeedNone);
|
|
770
|
+
/**
|
|
771
|
+
* What a prompt looks like in this tool: the marker the rows already use, and
|
|
772
|
+
* the same colour for the choice I am standing on.
|
|
773
|
+
*
|
|
774
|
+
* It is set here rather than at each prompt, because this module is the only
|
|
775
|
+
* thing that opens one and four prompts that themed themselves would be four
|
|
776
|
+
* looks.
|
|
777
|
+
*/
|
|
778
|
+
const theme = (paint) => paint === plain ? {
|
|
779
|
+
prefix: "▸",
|
|
780
|
+
pointer: "●"
|
|
781
|
+
} : {
|
|
782
|
+
prefix: "▸",
|
|
783
|
+
pointer: "●",
|
|
784
|
+
primaryColor: "cyan",
|
|
785
|
+
mutedColor: "gray"
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* What the keyboard does, said under the question.
|
|
789
|
+
*
|
|
790
|
+
* It rides in the message rather than being printed above the prompt, so it
|
|
791
|
+
* leaves with the prompt: a hint that outlives the answer is scrollback I did
|
|
792
|
+
* not ask for.
|
|
793
|
+
*/
|
|
794
|
+
const moves = "↑↓ move · enter choose · q quit";
|
|
795
|
+
const asked$1 = (paint, message) => `${message}\n${paint.dim(moves)}`;
|
|
796
|
+
/** Asks which one of `choices` to act on. */
|
|
797
|
+
const pick = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.Select({
|
|
798
|
+
message: asked$1(paint, message),
|
|
799
|
+
choices,
|
|
800
|
+
theme: theme(paint)
|
|
801
|
+
}))));
|
|
802
|
+
/**
|
|
803
|
+
* Asks which of `choices` to act on, as many as I like.
|
|
804
|
+
*
|
|
805
|
+
* Nothing is selected to begin with, so what reaches the caller is what I
|
|
806
|
+
* picked rather than what I failed to unpick. Quitting is not the same as
|
|
807
|
+
* picking nothing: it gives `None`.
|
|
808
|
+
*/
|
|
809
|
+
const choose = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.MultiSelect({
|
|
810
|
+
message: `${message}\n${paint.dim("↑↓ move · space pick · enter confirm · q quit")}`,
|
|
811
|
+
choices,
|
|
812
|
+
theme: theme(paint)
|
|
813
|
+
}))));
|
|
814
|
+
/**
|
|
815
|
+
* Asks a yes-or-no question about something that cannot be taken back.
|
|
816
|
+
*
|
|
817
|
+
* It starts on no, and walking away is no as well: the answer this returns is
|
|
818
|
+
* the one I typed, and every other way out of the prompt leaves the thing
|
|
819
|
+
* undone. A confirmation that defaulted to yes would be one keystroke, which is
|
|
820
|
+
* exactly what it exists to stop being.
|
|
821
|
+
*/
|
|
822
|
+
const confirm = (message) => Effect.flatMap(Paint, (paint) => Effect.map(orNone(Effect.asSome(Prompt.Confirm({
|
|
823
|
+
message,
|
|
824
|
+
initial: false,
|
|
825
|
+
theme: theme(paint)
|
|
826
|
+
}))), Option.getOrElse(() => false)));
|
|
827
|
+
/**
|
|
828
|
+
* Asks for a line of prose, where having nothing to say is the ordinary answer.
|
|
829
|
+
*
|
|
830
|
+
* An empty line is no note, and that is not a failure: the prompt is optional
|
|
831
|
+
* by design. Quitting is the one thing it does not swallow. Ctrl-C part way
|
|
832
|
+
* through a list of notes means I want out of the whole command, and a prompt
|
|
833
|
+
* that turned it into "no note" would walk me through the rest of the list and
|
|
834
|
+
* then act on findings I was no longer sure about.
|
|
835
|
+
*/
|
|
836
|
+
const note = (message) => Effect.map(Prompt.String({ message }), (text) => text.trim() === "" ? Option.none() : Option.some(text.trim()));
|
|
837
|
+
/**
|
|
838
|
+
* How wide the screen is, or zero where there is no screen to measure.
|
|
839
|
+
*
|
|
840
|
+
* A prompt has to fit its row on one line: a row that wraps takes the list's
|
|
841
|
+
* alignment with it. Nothing is piping into a prompt, so zero means the writing
|
|
842
|
+
* is going somewhere that does not wrap either.
|
|
843
|
+
*/
|
|
844
|
+
const width = Effect.gen(function* () {
|
|
845
|
+
return yield* (yield* Terminal.Terminal).columns;
|
|
846
|
+
});
|
|
847
|
+
//#endregion
|
|
711
848
|
//#region src/adapters/heartbeat.ts
|
|
712
849
|
/** The frames of the spinner, in the order they turn. */
|
|
713
850
|
const frames = [
|
|
@@ -1373,92 +1510,6 @@ const prune = Effect.fn("git.prune")(function* (clone) {
|
|
|
1373
1510
|
]));
|
|
1374
1511
|
});
|
|
1375
1512
|
//#endregion
|
|
1376
|
-
//#region src/adapters/picker.ts
|
|
1377
|
-
/**
|
|
1378
|
-
* Turns quitting into an answer rather than a failure.
|
|
1379
|
-
*
|
|
1380
|
-
* Bailing out of a prompt gives `None`, so no caller has to catch an error to
|
|
1381
|
-
* learn that I walked away. The prompt itself decides what a `Some` carries.
|
|
1382
|
-
*/
|
|
1383
|
-
const orNone = (prompt) => Effect.catchTag(prompt, "QuitError", () => Effect.succeedNone);
|
|
1384
|
-
/**
|
|
1385
|
-
* What a prompt looks like in this tool: the marker the rows already use, and
|
|
1386
|
-
* the same colour for the choice I am standing on.
|
|
1387
|
-
*
|
|
1388
|
-
* It is set here rather than at each prompt, because this module is the only
|
|
1389
|
-
* thing that opens one and four prompts that themed themselves would be four
|
|
1390
|
-
* looks.
|
|
1391
|
-
*/
|
|
1392
|
-
const theme = (paint) => paint === plain ? {
|
|
1393
|
-
prefix: "▸",
|
|
1394
|
-
pointer: "●"
|
|
1395
|
-
} : {
|
|
1396
|
-
prefix: "▸",
|
|
1397
|
-
pointer: "●",
|
|
1398
|
-
primaryColor: "cyan",
|
|
1399
|
-
mutedColor: "gray"
|
|
1400
|
-
};
|
|
1401
|
-
/**
|
|
1402
|
-
* What the keyboard does, said under the question.
|
|
1403
|
-
*
|
|
1404
|
-
* It rides in the message rather than being printed above the prompt, so it
|
|
1405
|
-
* leaves with the prompt: a hint that outlives the answer is scrollback I did
|
|
1406
|
-
* not ask for.
|
|
1407
|
-
*/
|
|
1408
|
-
const moves = "↑↓ move · enter choose · q quit";
|
|
1409
|
-
const asked$1 = (paint, message) => `${message}\n${paint.dim(moves)}`;
|
|
1410
|
-
/** Asks which one of `choices` to act on. */
|
|
1411
|
-
const pick = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.Select({
|
|
1412
|
-
message: asked$1(paint, message),
|
|
1413
|
-
choices,
|
|
1414
|
-
theme: theme(paint)
|
|
1415
|
-
}))));
|
|
1416
|
-
/**
|
|
1417
|
-
* Asks which of `choices` to act on, as many as I like.
|
|
1418
|
-
*
|
|
1419
|
-
* Nothing is selected to begin with, so what reaches the caller is what I
|
|
1420
|
-
* picked rather than what I failed to unpick. Quitting is not the same as
|
|
1421
|
-
* picking nothing: it gives `None`.
|
|
1422
|
-
*/
|
|
1423
|
-
const choose = (message, choices) => Effect.flatMap(Paint, (paint) => orNone(Effect.asSome(Prompt.MultiSelect({
|
|
1424
|
-
message: `${message}\n${paint.dim("↑↓ move · space pick · enter confirm · q quit")}`,
|
|
1425
|
-
choices,
|
|
1426
|
-
theme: theme(paint)
|
|
1427
|
-
}))));
|
|
1428
|
-
/**
|
|
1429
|
-
* Asks a yes-or-no question about something that cannot be taken back.
|
|
1430
|
-
*
|
|
1431
|
-
* It starts on no, and walking away is no as well: the answer this returns is
|
|
1432
|
-
* the one I typed, and every other way out of the prompt leaves the thing
|
|
1433
|
-
* undone. A confirmation that defaulted to yes would be one keystroke, which is
|
|
1434
|
-
* exactly what it exists to stop being.
|
|
1435
|
-
*/
|
|
1436
|
-
const confirm = (message) => Effect.flatMap(Paint, (paint) => Effect.map(orNone(Effect.asSome(Prompt.Confirm({
|
|
1437
|
-
message,
|
|
1438
|
-
initial: false,
|
|
1439
|
-
theme: theme(paint)
|
|
1440
|
-
}))), Option.getOrElse(() => false)));
|
|
1441
|
-
/**
|
|
1442
|
-
* Asks for a line of prose, where having nothing to say is the ordinary answer.
|
|
1443
|
-
*
|
|
1444
|
-
* An empty line is no note, and that is not a failure: the prompt is optional
|
|
1445
|
-
* by design. Quitting is the one thing it does not swallow. Ctrl-C part way
|
|
1446
|
-
* through a list of notes means I want out of the whole command, and a prompt
|
|
1447
|
-
* that turned it into "no note" would walk me through the rest of the list and
|
|
1448
|
-
* then act on findings I was no longer sure about.
|
|
1449
|
-
*/
|
|
1450
|
-
const note = (message) => Effect.map(Prompt.String({ message }), (text) => text.trim() === "" ? Option.none() : Option.some(text.trim()));
|
|
1451
|
-
/**
|
|
1452
|
-
* How wide the screen is, or zero where there is no screen to measure.
|
|
1453
|
-
*
|
|
1454
|
-
* A prompt has to fit its row on one line: a row that wraps takes the list's
|
|
1455
|
-
* alignment with it. Nothing is piping into a prompt, so zero means the writing
|
|
1456
|
-
* is going somewhere that does not wrap either.
|
|
1457
|
-
*/
|
|
1458
|
-
const width = Effect.gen(function* () {
|
|
1459
|
-
return yield* (yield* Terminal.Terminal).columns;
|
|
1460
|
-
});
|
|
1461
|
-
//#endregion
|
|
1462
1513
|
//#region src/cli/table.ts
|
|
1463
1514
|
/**
|
|
1464
1515
|
* The rows of a table, padded so the columns line up and with the trailing
|
|
@@ -1515,7 +1566,9 @@ const standing = (inventory) => inventory.cuttings.flatMap((cutting) => {
|
|
|
1515
1566
|
/** The checkouts a review run cut, which no run that ended still needs. */
|
|
1516
1567
|
const orphaned = (inventory) => inventory.cuttings.filter((cutting) => cutting.cut === "worktrees");
|
|
1517
1568
|
const sum = (sizes) => ByteSize.bytes(sizes.reduce((total, size) => total + ByteSize.toBigInt(size), BigInt(0)));
|
|
1518
|
-
|
|
1569
|
+
/** What the glossary calls the session a standing checkout was cut for. */
|
|
1570
|
+
const sessionName = (session) => session === "fix" ? "fix" : "resolve";
|
|
1571
|
+
const reason = (sessions) => sessions.map((it) => `a ${sessionName(it.session)} session stands on ${it.repo}#${it.number}`).join(", ");
|
|
1519
1572
|
/**
|
|
1520
1573
|
* What a cleanup would take, weighed.
|
|
1521
1574
|
*
|
|
@@ -1545,7 +1598,7 @@ const plan = (inventory) => {
|
|
|
1545
1598
|
};
|
|
1546
1599
|
};
|
|
1547
1600
|
/** What the whole state directory weighs: the clones, the checkouts and the records. */
|
|
1548
|
-
const everything = (inventory) => sum([
|
|
1601
|
+
const everything$1 = (inventory) => sum([
|
|
1549
1602
|
...inventory.clones.map((it) => it.size),
|
|
1550
1603
|
...inventory.cuttings.map((it) => it.size),
|
|
1551
1604
|
inventory.records.size
|
|
@@ -1604,7 +1657,7 @@ const lines$3 = (it, state, path, paint) => {
|
|
|
1604
1657
|
* configuration file - and what I worked in - the worktree of a fix or resolve
|
|
1605
1658
|
* session, which stands on a branch of the tool's own and holds what I
|
|
1606
1659
|
* committed there. Forgetting a pull request's records is a different question
|
|
1607
|
-
* with a different answer (
|
|
1660
|
+
* with a different answer (`dw-mc forget`), and it is not asked here.
|
|
1608
1661
|
*
|
|
1609
1662
|
* A clone with a session standing on it stays with the session: a standing
|
|
1610
1663
|
* worktree keeps its history inside the clone, so a clone taken from under one
|
|
@@ -1772,6 +1825,10 @@ const PrView = Schema.fromJsonString(Schema.Struct({
|
|
|
1772
1825
|
reviewDecision: Schema.String,
|
|
1773
1826
|
statusCheckRollup: Schema.NullOr(Schema.Array(CheckEntry))
|
|
1774
1827
|
}));
|
|
1828
|
+
/**
|
|
1829
|
+
* The fields one `gh pr view` asks for, named so a test can spell the vector it
|
|
1830
|
+
* expects without copying the list and watching it drift.
|
|
1831
|
+
*/
|
|
1775
1832
|
const viewFields = "number,title,url,isDraft,headRefOid,headRefName,baseRefName,author,isCrossRepository,mergeable,reviewDecision,statusCheckRollup";
|
|
1776
1833
|
/**
|
|
1777
1834
|
* Everything about one pull request that arrives without paging through it:
|
|
@@ -1962,13 +2019,13 @@ const Actor = Schema.NullOr(Schema.Struct({
|
|
|
1962
2019
|
login: Schema.String,
|
|
1963
2020
|
__typename: Schema.String
|
|
1964
2021
|
}));
|
|
1965
|
-
const Said = Schema.Struct({
|
|
2022
|
+
const Said$1 = Schema.Struct({
|
|
1966
2023
|
author: Actor,
|
|
1967
2024
|
body: Schema.String,
|
|
1968
2025
|
createdAt: Schema.DateTimeUtcFromString
|
|
1969
2026
|
});
|
|
1970
2027
|
const Conversation = Schema.fromJsonString(Schema.Struct({ data: Schema.Struct({ repository: Schema.Struct({ pullRequest: Schema.Struct({
|
|
1971
|
-
comments: Schema.Struct({ nodes: Schema.Array(Said) }),
|
|
2028
|
+
comments: Schema.Struct({ nodes: Schema.Array(Said$1) }),
|
|
1972
2029
|
reviews: Schema.Struct({ nodes: Schema.Array(Schema.Struct({
|
|
1973
2030
|
author: Actor,
|
|
1974
2031
|
body: Schema.String,
|
|
@@ -1979,7 +2036,7 @@ const Conversation = Schema.fromJsonString(Schema.Struct({ data: Schema.Struct({
|
|
|
1979
2036
|
isOutdated: Schema.Boolean,
|
|
1980
2037
|
path: Schema.NullOr(Schema.String),
|
|
1981
2038
|
line: Schema.NullOr(Schema.Int),
|
|
1982
|
-
comments: Schema.Struct({ nodes: Schema.Array(Said) })
|
|
2039
|
+
comments: Schema.Struct({ nodes: Schema.Array(Said$1) })
|
|
1983
2040
|
})) })
|
|
1984
2041
|
}) }) }) }));
|
|
1985
2042
|
const remark = (said, at) => said.author === null || at === null || said.body.trim() === "" ? [] : [{
|
|
@@ -2151,12 +2208,15 @@ const Facts = Schema.Struct({
|
|
|
2151
2208
|
newestHumanCommentAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
2152
2209
|
myLastCommentAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
2153
2210
|
myLastCommitAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
2211
|
+
/** The newest comment my acknowledgement covers, or null where I have made none. */
|
|
2212
|
+
acknowledgedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
2154
2213
|
/** The head a review run has already covered, or null where none has. */
|
|
2155
2214
|
reviewRunHead: Schema.NullOr(Schema.String),
|
|
2156
2215
|
/** Findings on this head that withhold the stamp, at the bar `stamp.blocks_on` sets. */
|
|
2157
2216
|
blockingFindings: Schema.Int
|
|
2158
2217
|
});
|
|
2159
|
-
|
|
2218
|
+
/** The one place a tracked PR sits at a time, named for what it waits on. */
|
|
2219
|
+
const Bucket = Schema.Literals([
|
|
2160
2220
|
"needs-me",
|
|
2161
2221
|
"needs-review-run",
|
|
2162
2222
|
"waiting-on-others",
|
|
@@ -2189,6 +2249,16 @@ const unanswered = "a comment I have not answered";
|
|
|
2189
2249
|
*/
|
|
2190
2250
|
const blockedBy = (n) => `${n} blocking finding${n === 1 ? "" : "s"}`;
|
|
2191
2251
|
/**
|
|
2252
|
+
* How far my answer to the conversation reaches: the latest of my last comment,
|
|
2253
|
+
* my last commit and my acknowledgement.
|
|
2254
|
+
*
|
|
2255
|
+
* A comment is answered by a reply, by a push, or by my word that nothing in it
|
|
2256
|
+
* was mine to answer. It is named because it is read twice: here, where a newer
|
|
2257
|
+
* comment puts the PR in Needs me, and by `dw-mc comments`, which shows exactly
|
|
2258
|
+
* the comments newer than it.
|
|
2259
|
+
*/
|
|
2260
|
+
const answeredAt = (facts) => later(later(facts.myLastCommentAt, facts.myLastCommitAt), facts.acknowledgedAt);
|
|
2261
|
+
/**
|
|
2192
2262
|
* The first of the rules that makes a PR mine to move, or null when none
|
|
2193
2263
|
* does. The order is the order I would fix them in: a conflict makes every
|
|
2194
2264
|
* other signal on the PR stale, and a red build is worth more than a comment.
|
|
@@ -2199,7 +2269,7 @@ const needsMe = (facts) => {
|
|
|
2199
2269
|
if (facts.checks === "red" && facts.ciFlaky === null) return "CI is red";
|
|
2200
2270
|
if (facts.reviewDecision === "changes-requested") return "changes requested";
|
|
2201
2271
|
if (facts.blockingFindings > 0) return blockedBy(facts.blockingFindings);
|
|
2202
|
-
if (isAfter(facts.newestHumanCommentAt,
|
|
2272
|
+
if (isAfter(facts.newestHumanCommentAt, answeredAt(facts))) return unanswered;
|
|
2203
2273
|
return null;
|
|
2204
2274
|
};
|
|
2205
2275
|
/**
|
|
@@ -2536,7 +2606,7 @@ const skippedSince = (asked, docsOnly) => {
|
|
|
2536
2606
|
return changed === null || worthRerunning(changed, docsOnly) ? null : asked.last.head;
|
|
2537
2607
|
};
|
|
2538
2608
|
/** What a run was opened on, as the report says it. */
|
|
2539
|
-
const askedOf$
|
|
2609
|
+
const askedOf$2 = (run) => run.command === null ? "the tool's own prompt" : [run.command, run.effort].filter((part) => part !== null).join(" ");
|
|
2540
2610
|
/**
|
|
2541
2611
|
* The report as it is written down: what it is of, then what the run said.
|
|
2542
2612
|
*
|
|
@@ -2548,7 +2618,7 @@ const reportDocument = (run, title, prose) => [
|
|
|
2548
2618
|
`# ${run.repo}#${run.number} ${title}`,
|
|
2549
2619
|
"",
|
|
2550
2620
|
`- head: ${run.head}`,
|
|
2551
|
-
`- run: ${askedOf$
|
|
2621
|
+
`- run: ${askedOf$2(run)}`,
|
|
2552
2622
|
`- ran: ${DateTime.formatIso(run.ranAt)}`,
|
|
2553
2623
|
"",
|
|
2554
2624
|
prose.trim(),
|
|
@@ -2721,6 +2791,20 @@ const marker = {
|
|
|
2721
2791
|
"waiting-on-others": "○",
|
|
2722
2792
|
ready: "◆"
|
|
2723
2793
|
};
|
|
2794
|
+
/**
|
|
2795
|
+
* What sits in front of a row: `+` for a pull request never shown, `*` for one
|
|
2796
|
+
* that moved since it was last shown, and a blank for one that did neither.
|
|
2797
|
+
*
|
|
2798
|
+
* It is one character from the part of Unicode every font has, for the reason
|
|
2799
|
+
* the marker is, and it is not coloured: the bucket is what the colour says.
|
|
2800
|
+
*/
|
|
2801
|
+
const gutter = {
|
|
2802
|
+
new: "+",
|
|
2803
|
+
moved: "*",
|
|
2804
|
+
still: " "
|
|
2805
|
+
};
|
|
2806
|
+
/** How a row names its pull request. */
|
|
2807
|
+
const reference = (facts) => `${facts.repo}#${facts.number}`;
|
|
2724
2808
|
/** The colour a bucket is said in: red is mine, yellow is next, green is done, dim is not my turn. */
|
|
2725
2809
|
const tint = (paint, bucket) => ({
|
|
2726
2810
|
"needs-me": paint.red,
|
|
@@ -2733,6 +2817,8 @@ const rule = " │ ";
|
|
|
2733
2817
|
/**
|
|
2734
2818
|
* One row: which pull request, what it is, and what it waits on.
|
|
2735
2819
|
*
|
|
2820
|
+
* In front of it all is the gutter, which says what moved since I last looked.
|
|
2821
|
+
*
|
|
2736
2822
|
* A stamp is a mark beside the pull request rather than a column of its own, so
|
|
2737
2823
|
* a table where nothing is stamped is exactly the table it was before: the
|
|
2738
2824
|
* stamp is a thing I look for, not a thing I read every row of.
|
|
@@ -2750,25 +2836,51 @@ const rule = " │ ";
|
|
|
2750
2836
|
* straight, so its rows say it in more than one place and open the pull request
|
|
2751
2837
|
* besides.
|
|
2752
2838
|
*/
|
|
2753
|
-
const cells = (placed, stamped, room, paint, lead) => {
|
|
2839
|
+
const cells = (placed, stamped, since, room, paint, lead) => {
|
|
2754
2840
|
const { facts } = placed;
|
|
2755
2841
|
const { bucket } = placed.placement;
|
|
2756
2842
|
const say = tint(paint, bucket);
|
|
2757
|
-
const reference = `${facts.repo}#${facts.number}`;
|
|
2758
2843
|
const named = lead === "named";
|
|
2759
|
-
const pr = `${named ? reference : paint.link(reference, facts.url)}${facts.draft ? paint.dim(" (draft)") : ""}${stamped ? ` ${paint.green("✓")}` : ""}`;
|
|
2844
|
+
const pr = `${named ? reference(facts) : paint.link(reference(facts), facts.url)}${facts.draft ? paint.dim(" (draft)") : ""}${stamped ? ` ${paint.green("✓")}` : ""}`;
|
|
2845
|
+
const front = gutter[since._tag];
|
|
2760
2846
|
return named ? [
|
|
2761
|
-
say(`${marker[bucket]} ${heading[bucket]}`)
|
|
2847
|
+
`${front} ${say(`${marker[bucket]} ${heading[bucket]}`)}`,
|
|
2762
2848
|
pr,
|
|
2763
2849
|
truncate(facts.title, room),
|
|
2764
2850
|
placed.placement.reason
|
|
2765
2851
|
] : [
|
|
2766
|
-
`${say(marker[bucket])} ${pr}`,
|
|
2852
|
+
`${front} ${say(marker[bucket])} ${pr}`,
|
|
2767
2853
|
paint.dim(truncate(facts.title, room)),
|
|
2768
2854
|
say(placed.placement.reason)
|
|
2769
2855
|
];
|
|
2770
2856
|
};
|
|
2771
2857
|
//#endregion
|
|
2858
|
+
//#region src/domain/acknowledgement.ts
|
|
2859
|
+
/**
|
|
2860
|
+
* My word that I have read a pull request's conversation as far as one comment,
|
|
2861
|
+
* and that nothing in it is mine to answer.
|
|
2862
|
+
*
|
|
2863
|
+
* The comment's moment is the whole record, and no head is: everything else the
|
|
2864
|
+
* tool records is about code and lapses when the head changes, and this one is
|
|
2865
|
+
* about a conversation. A push is already counted as my answer to a comment, so
|
|
2866
|
+
* tying this to a head would only bring back a comment the push had nothing to
|
|
2867
|
+
* do with.
|
|
2868
|
+
*/
|
|
2869
|
+
const Acknowledgement = Schema.Struct({ at: Schema.DateTimeUtcFromString });
|
|
2870
|
+
/** The newest comment I have acknowledged on a pull request, or null where I have acknowledged none. */
|
|
2871
|
+
const acknowledgedAt = Effect.fn("acknowledgement.acknowledgedAt")(function* (repo, number) {
|
|
2872
|
+
const store = yield* storeFor("acknowledgements", Acknowledgement);
|
|
2873
|
+
const acknowledgement = yield* remembered(store.get(prKey(repo, number)));
|
|
2874
|
+
return Option.match(acknowledgement, {
|
|
2875
|
+
onNone: () => null,
|
|
2876
|
+
onSome: (it) => it.at
|
|
2877
|
+
});
|
|
2878
|
+
});
|
|
2879
|
+
/** Records that I have read a pull request's conversation as far as the comment written at `at`. */
|
|
2880
|
+
const acknowledge = Effect.fn("acknowledgement.acknowledge")(function* (repo, number, at) {
|
|
2881
|
+
yield* (yield* storeFor("acknowledgements", Acknowledgement)).set(prKey(repo, number), { at });
|
|
2882
|
+
});
|
|
2883
|
+
//#endregion
|
|
2772
2884
|
//#region src/domain/comments.ts
|
|
2773
2885
|
/**
|
|
2774
2886
|
* One thread's share of a strand, cut to what is worth reading.
|
|
@@ -2809,9 +2921,24 @@ const shown = (threads, options) => {
|
|
|
2809
2921
|
bots: kept.flatMap((it) => only(it, (bot) => bot, options.since, options.all))
|
|
2810
2922
|
};
|
|
2811
2923
|
};
|
|
2924
|
+
/**
|
|
2925
|
+
* The comment an acknowledgement of this conversation covers: the newest thing
|
|
2926
|
+
* a person said in it, whichever thread it is in.
|
|
2927
|
+
*
|
|
2928
|
+
* The whole conversation rather than what went on screen, because the bucket
|
|
2929
|
+
* rule counts the whole of it: a comment on a thread somebody resolved still
|
|
2930
|
+
* puts the pull request in Needs me, and an acknowledgement that stopped short
|
|
2931
|
+
* of it would settle nothing. A bot is left out for the reason the rule leaves
|
|
2932
|
+
* it out.
|
|
2933
|
+
*
|
|
2934
|
+
* It is a comment's own moment and never the clock's, so a comment written
|
|
2935
|
+
* after the conversation was read is one the acknowledgement does not cover.
|
|
2936
|
+
*/
|
|
2937
|
+
const acknowledging = (threads) => newest(threads.flatMap((thread) => thread.comments.filter((it) => !it.bot).map((it) => it.at)));
|
|
2812
2938
|
//#endregion
|
|
2813
2939
|
//#region src/cli/comments.ts
|
|
2814
|
-
const allFlag = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Print the whole conversation, including what is resolved, outdated and already answered"));
|
|
2940
|
+
const allFlag$1 = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Print the whole conversation, including what is resolved, outdated and already answered"));
|
|
2941
|
+
const ackFlag = Flag.Boolean("ack").pipe(Flag.withDefault(false), Flag.withDescription("Record that I have read the conversation and nothing in it is mine to answer"));
|
|
2815
2942
|
/** Where a thread hangs: a line of the diff, or the pull request itself. */
|
|
2816
2943
|
const where$1 = (thread) => thread.path === null ? "Conversation" : thread.line === null ? thread.path : `${thread.path}:${thread.line}`;
|
|
2817
2944
|
/**
|
|
@@ -2857,45 +2984,75 @@ const nothing$1 = (facts, all) => {
|
|
|
2857
2984
|
const placement = place(facts);
|
|
2858
2985
|
const rest = `dw-mc comments ${facts.number} --all prints the whole conversation.`;
|
|
2859
2986
|
return placement.bucket === "needs-me" && placement.reason === "a comment I have not answered" ? [
|
|
2860
|
-
"Nothing here is waiting on you: every thread is resolved, outdated, or older than your last comment or
|
|
2861
|
-
`${pr} sits in ${heading[placement.bucket]} all the same, and a reply
|
|
2987
|
+
"Nothing here is waiting on you: every thread is resolved, outdated, or older than your last comment, commit or acknowledgement.",
|
|
2988
|
+
`${pr} sits in ${heading[placement.bucket]} all the same, and a reply, a push or dw-mc comments ${facts.number} --ack is what settles it.`,
|
|
2862
2989
|
rest
|
|
2863
|
-
] : [`Nothing has been said on ${pr} since your last comment or
|
|
2990
|
+
] : [`Nothing has been said on ${pr} since your last comment, commit or acknowledgement.`, rest];
|
|
2864
2991
|
};
|
|
2865
2992
|
/**
|
|
2993
|
+
* Records an acknowledgement of the conversation read, and says what it covers
|
|
2994
|
+
* and where the pull request sits with it.
|
|
2995
|
+
*
|
|
2996
|
+
* Where it sits is worked out from the last sweep with the acknowledgement laid
|
|
2997
|
+
* over it, which is the same answer the next sweep gives unless somebody says
|
|
2998
|
+
* something new in between.
|
|
2999
|
+
*/
|
|
3000
|
+
const acknowledged = Effect.fn("comments.acknowledged")(function* (facts, threads) {
|
|
3001
|
+
const pr = `${facts.repo}#${facts.number}`;
|
|
3002
|
+
const at = acknowledging(threads);
|
|
3003
|
+
if (at === null) {
|
|
3004
|
+
yield* Console.log(`Nothing to acknowledge: nobody has said anything on ${pr}.`);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
yield* acknowledge(facts.repo, facts.number, at);
|
|
3008
|
+
const placement = place({
|
|
3009
|
+
...facts,
|
|
3010
|
+
acknowledgedAt: at
|
|
3011
|
+
});
|
|
3012
|
+
yield* Console.log(`Acknowledged everything said on ${pr} up to ${DateTime.formatIso(at)}.`);
|
|
3013
|
+
yield* Console.log(`${pr} sits in ${heading[placement.bucket]}: ${placement.reason}.`);
|
|
3014
|
+
});
|
|
3015
|
+
/**
|
|
2866
3016
|
* The conversation on one tracked pull request, and nothing else.
|
|
2867
3017
|
*
|
|
2868
3018
|
* What it shows by default is what the bucket rule measures: the comments newer
|
|
2869
|
-
* than the
|
|
2870
|
-
* put the pull request in Needs me. Reading it answers
|
|
2871
|
-
* asked.
|
|
3019
|
+
* than the latest of my last comment, my last commit and my acknowledgement,
|
|
3020
|
+
* which are the ones that put the pull request in Needs me. Reading it answers
|
|
3021
|
+
* the question the table asked.
|
|
2872
3022
|
*
|
|
2873
3023
|
* The cutoff is read off the last sweep rather than worked out again here, so
|
|
2874
3024
|
* the command shows exactly what `dw-mc status` counted rather than a second
|
|
2875
3025
|
* opinion about it.
|
|
2876
3026
|
*
|
|
2877
|
-
* It writes nothing
|
|
2878
|
-
*
|
|
3027
|
+
* It writes nothing to GitHub: no reply, no resolve, no reaction (ADR 0002).
|
|
3028
|
+
* `--ack` is the one thing it writes at all, and only on this machine: whether a
|
|
3029
|
+
* comment needs an answer is known after reading it, so reading cannot be what
|
|
3030
|
+
* decides it.
|
|
2879
3031
|
*/
|
|
2880
3032
|
const comments = Command.make("comments", {
|
|
2881
3033
|
pr: prArgument,
|
|
2882
|
-
all: allFlag
|
|
2883
|
-
|
|
3034
|
+
all: allFlag$1,
|
|
3035
|
+
ack: ackFlag
|
|
3036
|
+
}, Effect.fn("comments")(function* ({ ack, all, pr }) {
|
|
2884
3037
|
const { number, repo } = yield* forPr(pr);
|
|
2885
3038
|
const facts = yield* swept(repo, number);
|
|
2886
3039
|
const paint = yield* Paint;
|
|
2887
|
-
const
|
|
2888
|
-
|
|
3040
|
+
const threads = yield* reading(`${repo}#${number}`, prConversation(repo, number));
|
|
3041
|
+
const view = shown(threads, {
|
|
3042
|
+
since: answeredAt(facts),
|
|
2889
3043
|
all
|
|
2890
3044
|
});
|
|
2891
|
-
if (view.people.length === 0 && view.bots.length === 0)
|
|
2892
|
-
|
|
2893
|
-
|
|
3045
|
+
if (view.people.length === 0 && view.bots.length === 0) yield* Effect.forEach(nothing$1(facts, all), (line) => Console.log(line));
|
|
3046
|
+
else {
|
|
3047
|
+
yield* Console.log(paint.bold(`${repo}#${number}`) + ` ${paint.dim(facts.title)}`);
|
|
3048
|
+
yield* Console.log("");
|
|
3049
|
+
yield* Effect.forEach(lines$2(view, paint), (line) => Console.log(line));
|
|
2894
3050
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
}
|
|
3051
|
+
if (ack) {
|
|
3052
|
+
yield* Console.log("");
|
|
3053
|
+
yield* acknowledged(facts, threads);
|
|
3054
|
+
}
|
|
3055
|
+
}, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Print the conversation on one pull request and what is waiting on me in it, and with --ack record that I read it"));
|
|
2899
3056
|
//#endregion
|
|
2900
3057
|
//#region src/cli/findings.ts
|
|
2901
3058
|
/** The findings as the JSON the schema defines, rather than as this file spells it. */
|
|
@@ -3484,6 +3641,83 @@ const fix = Command.make("fix", {
|
|
|
3484
3641
|
yield* Console.log(`Once you have pushed, dw-mc review ${number} reviews the new head as a new run.`);
|
|
3485
3642
|
}, Effect.catchTag(userFacingAndSession, asUserError))).pipe(Command.withDescription("Pick findings from the current review run and open a fix session on them"));
|
|
3486
3643
|
//#endregion
|
|
3644
|
+
//#region src/domain/forget.ts
|
|
3645
|
+
/**
|
|
3646
|
+
* Whether a key in the state directory is about this pull request, whichever
|
|
3647
|
+
* namespace it sits in.
|
|
3648
|
+
*
|
|
3649
|
+
* Every namespace names a pull request by `prKey`, and the review runs add the
|
|
3650
|
+
* head after an `@`. Reading the key rather than a list of namespaces is what
|
|
3651
|
+
* lets a namespace added later be forgotten with the rest.
|
|
3652
|
+
*/
|
|
3653
|
+
const isAbout = (key, repo, number) => {
|
|
3654
|
+
const slash = key.indexOf("/");
|
|
3655
|
+
if (slash < 0) return false;
|
|
3656
|
+
const rest = key.slice(slash + 1);
|
|
3657
|
+
const pr = prKey(repo, number);
|
|
3658
|
+
return rest === pr || rest.startsWith(`${pr}@`);
|
|
3659
|
+
};
|
|
3660
|
+
/**
|
|
3661
|
+
* Removes everything the state directory keeps about one pull request, and
|
|
3662
|
+
* says how many keys that was.
|
|
3663
|
+
*
|
|
3664
|
+
* A pull request it keeps nothing about is forgotten already, so that is none
|
|
3665
|
+
* rather than a failure.
|
|
3666
|
+
*/
|
|
3667
|
+
const forget = Effect.fn("forget.forget")(function* (repo, number) {
|
|
3668
|
+
const store = yield* KeyValueStore.KeyValueStore;
|
|
3669
|
+
const about = (yield* allKeys).filter((key) => isAbout(key, repo, number));
|
|
3670
|
+
yield* Effect.forEach(about, (key) => store.remove(key), { discard: true });
|
|
3671
|
+
return about.length;
|
|
3672
|
+
});
|
|
3673
|
+
/**
|
|
3674
|
+
* The session worktrees that stand on one pull request.
|
|
3675
|
+
*
|
|
3676
|
+
* Forgetting takes none of them: each stands on a branch of the tool's own and
|
|
3677
|
+
* holds what I committed there, so what the tool does is say they are there.
|
|
3678
|
+
*/
|
|
3679
|
+
const standingOn = (inventory, repo, number) => standing(inventory).filter((it) => it.repo === repo && it.number === number);
|
|
3680
|
+
//#endregion
|
|
3681
|
+
//#region src/cli/forget.ts
|
|
3682
|
+
/**
|
|
3683
|
+
* Forgets one pull request and says what stays: the records go, the session
|
|
3684
|
+
* worktrees standing on it do not.
|
|
3685
|
+
*
|
|
3686
|
+
* `deleted` is the branch the pull request stood on, where the caller has just
|
|
3687
|
+
* deleted it. A session there tracks a branch that is gone, which is worth
|
|
3688
|
+
* saying, and what it holds is still mine, so saying it is all this does.
|
|
3689
|
+
*/
|
|
3690
|
+
const forgetting = Effect.fn("forgetting")(function* (repo, number, options = {}) {
|
|
3691
|
+
const deleted = options.deleted;
|
|
3692
|
+
const path = yield* Path.Path;
|
|
3693
|
+
const paint = yield* Paint;
|
|
3694
|
+
const where = `${repo}#${number}`;
|
|
3695
|
+
const forgot = yield* forget(repo, number);
|
|
3696
|
+
yield* Console.log(forgot === 0 ? `Nothing is kept about ${where}.` : `Forgot ${where}: ${count(forgot, "record")}.`);
|
|
3697
|
+
const found = yield* inventory;
|
|
3698
|
+
const sessions = standingOn(found, repo, number);
|
|
3699
|
+
if (sessions.length === 0) return;
|
|
3700
|
+
const rows = table(sessions.map((it) => [paint.dim(path.relative(found.directory, it.directory)), `a ${sessionName(it.session)} session's worktree, on ${sessionBranch(it.session, number)}` + (deleted === void 0 ? "" : `, which tracked ${deleted} - deleted with the merge`)]));
|
|
3701
|
+
yield* Console.log("");
|
|
3702
|
+
yield* Console.log("Stays");
|
|
3703
|
+
yield* Effect.forEach(rows, (row) => Console.log(` ${row}`));
|
|
3704
|
+
yield* Console.log("");
|
|
3705
|
+
yield* Console.log("What you committed there is yours, so nothing here takes it down.");
|
|
3706
|
+
});
|
|
3707
|
+
/**
|
|
3708
|
+
* Forgets a pull request that closed some other way than `dw-mc merge`.
|
|
3709
|
+
*
|
|
3710
|
+
* A sweep never does this on its own. A pull request missing from one search is
|
|
3711
|
+
* not one that is gone - a failed `gh search` would look the same - and a sweep
|
|
3712
|
+
* is built so that a failure costs a row rather than the table. Being done is
|
|
3713
|
+
* something I know and the tool does not, except when it merged the pull
|
|
3714
|
+
* request itself.
|
|
3715
|
+
*/
|
|
3716
|
+
const forgetCommand = Command.make("forget", { pr: prArgument }, Effect.fn("forget")(function* ({ pr }) {
|
|
3717
|
+
const { number, repo } = yield* forPr(pr);
|
|
3718
|
+
yield* forgetting(repo, number);
|
|
3719
|
+
})).pipe(Command.withDescription("Forget everything kept about a pull request that is done"));
|
|
3720
|
+
//#endregion
|
|
3487
3721
|
//#region src/cli/init.ts
|
|
3488
3722
|
const effortFlag$1 = Flag.Literals("effort", [
|
|
3489
3723
|
"low",
|
|
@@ -3920,6 +4154,10 @@ const decide$3 = (situation) => {
|
|
|
3920
4154
|
*
|
|
3921
4155
|
* Typing the command is the confirmation, so it takes no flag. The picker,
|
|
3922
4156
|
* where a keystroke is cheaper, asks before it dispatches.
|
|
4157
|
+
*
|
|
4158
|
+
* Its last step is forgetting the pull request, in every namespace. A session
|
|
4159
|
+
* worktree standing on it stays, and is named, because its branch tracked the
|
|
4160
|
+
* one this just deleted.
|
|
3923
4161
|
*/
|
|
3924
4162
|
const merge = Command.make("merge", { pr: prArgument }, Effect.fn("merge")(function* ({ pr }) {
|
|
3925
4163
|
const { number, repo, settings } = yield* forPr(pr);
|
|
@@ -3940,8 +4178,48 @@ const merge = Command.make("merge", { pr: prArgument }, Effect.fn("merge")(funct
|
|
|
3940
4178
|
yield* mergePr(repo, number);
|
|
3941
4179
|
yield* Console.log(`${repo}#${number} ${short(head)} squash-merged into ${view.baseRefName}, and ${view.headRefName} deleted`);
|
|
3942
4180
|
yield* Console.log(`The squash subject is the pull request title: ${view.title}`);
|
|
4181
|
+
yield* forgetting(repo, number, { deleted: view.headRefName }).pipe(Effect.catch((error) => Console.log(`\nCould not forget ${repo}#${number}: ${error.message}. Run dw-mc forget ${number} to try again.`)));
|
|
3943
4182
|
}, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Squash-merge a Ready, stamped pull request of mine and delete its branch"));
|
|
3944
4183
|
//#endregion
|
|
4184
|
+
//#region src/domain/coverage.ts
|
|
4185
|
+
const one = (repo, registered) => ({
|
|
4186
|
+
_tag: "covers",
|
|
4187
|
+
repos: [repo],
|
|
4188
|
+
leftOut: registered.length - 1
|
|
4189
|
+
});
|
|
4190
|
+
/**
|
|
4191
|
+
* The repositories a sweep covers.
|
|
4192
|
+
*
|
|
4193
|
+
* Standing in a registered repository is asking for that one, because that is
|
|
4194
|
+
* the table I want while I work there. Standing anywhere else asks for nothing
|
|
4195
|
+
* in particular, so it gets every registered repository, as it would have
|
|
4196
|
+
* before a sweep could be narrowed at all.
|
|
4197
|
+
*
|
|
4198
|
+
* `here` is the repository the working directory is in, and is only read when
|
|
4199
|
+
* neither flag decides.
|
|
4200
|
+
*/
|
|
4201
|
+
const covered = (asked, here, registered) => {
|
|
4202
|
+
if (asked.repo !== void 0 && asked.all) return {
|
|
4203
|
+
_tag: "refused",
|
|
4204
|
+
why: "--repo names one repository and --all asks for every one. Pass one of them."
|
|
4205
|
+
};
|
|
4206
|
+
if (asked.repo !== void 0) {
|
|
4207
|
+
if (registered.includes(asked.repo)) return one(asked.repo, registered);
|
|
4208
|
+
return {
|
|
4209
|
+
_tag: "refused",
|
|
4210
|
+
why: registered.length === 0 ? `${asked.repo} is not registered. Run dw-mc init inside it to register it.` : `${asked.repo} is not registered. Name one of ${registered.join(", ")}, or run dw-mc init inside it.`
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
if (!asked.all && here !== void 0 && registered.includes(here)) return one(here, registered);
|
|
4214
|
+
return {
|
|
4215
|
+
_tag: "covers",
|
|
4216
|
+
repos: registered,
|
|
4217
|
+
leftOut: 0
|
|
4218
|
+
};
|
|
4219
|
+
};
|
|
4220
|
+
/** Whether the working directory decides the coverage, which is the one case worth asking `gh` where it is. */
|
|
4221
|
+
const asksWhereIAm = (asked, registered) => asked.repo === void 0 && !asked.all && registered.length > 0;
|
|
4222
|
+
//#endregion
|
|
3945
4223
|
//#region src/domain/flaky.ts
|
|
3946
4224
|
/**
|
|
3947
4225
|
* The failures that are flaky wherever they appear: a machine, a network or a
|
|
@@ -4233,6 +4511,13 @@ const recordConflict = Effect.fn("rebase.recordConflict")(function* (repo, numbe
|
|
|
4233
4511
|
});
|
|
4234
4512
|
//#endregion
|
|
4235
4513
|
//#region src/cli/sweep.ts
|
|
4514
|
+
/** What the picker asks a sweep for: every registered repository, wherever I stand. */
|
|
4515
|
+
const everything = {
|
|
4516
|
+
repo: void 0,
|
|
4517
|
+
all: true
|
|
4518
|
+
};
|
|
4519
|
+
/** The repository I stand in, or none where the working directory is not one. */
|
|
4520
|
+
const whereIAm = currentRepo.pipe(Effect.asSome, Effect.catchTag("NoRepository", () => Effect.succeedNone), Effect.map(Option.getOrUndefined));
|
|
4236
4521
|
const writtenBy = (comments, login) => comments.filter((comment) => comment.login === login).map((comment) => comment.at);
|
|
4237
4522
|
const byHumansOtherThan = (comments, login) => comments.filter((comment) => !comment.bot && comment.login !== login).map((comment) => comment.at);
|
|
4238
4523
|
/**
|
|
@@ -4275,6 +4560,7 @@ const sweepPr = Effect.fn("sweep.pullRequest")(function* (store, me, found, sett
|
|
|
4275
4560
|
newestHumanCommentAt,
|
|
4276
4561
|
myLastCommentAt: newest(writtenBy(comments, me)),
|
|
4277
4562
|
myLastCommitAt,
|
|
4563
|
+
acknowledgedAt: yield* acknowledgedAt(found.repo, found.number),
|
|
4278
4564
|
...reviewed
|
|
4279
4565
|
};
|
|
4280
4566
|
yield* store.set(key, facts);
|
|
@@ -4306,7 +4592,8 @@ const saying$1 = (swept) => (since) => [
|
|
|
4306
4592
|
since
|
|
4307
4593
|
].join(" · ");
|
|
4308
4594
|
/**
|
|
4309
|
-
* One pass over
|
|
4595
|
+
* One pass over the tracked PRs of the repositories `asked` covers, and nothing
|
|
4596
|
+
* else: a sweep only ever reads.
|
|
4310
4597
|
*
|
|
4311
4598
|
* Every repository and every pull request is read on its own, so one of them
|
|
4312
4599
|
* failing costs me its rows and leaves the rest of the table standing. What
|
|
@@ -4316,11 +4603,15 @@ const saying$1 = (swept) => (since) => [
|
|
|
4316
4603
|
* that is worth saying is the caller's, which is why it is handed a count and
|
|
4317
4604
|
* not a sentence.
|
|
4318
4605
|
*/
|
|
4319
|
-
const sweep = Effect.fn("sweep")(function* (report) {
|
|
4606
|
+
const sweep = Effect.fn("sweep")(function* (asked, report) {
|
|
4320
4607
|
const file = Option.getOrElse(yield* read, () => ({}));
|
|
4321
|
-
const
|
|
4608
|
+
const registered = Object.keys(file.repos ?? {}).toSorted();
|
|
4609
|
+
const coverage = covered(asked, asksWhereIAm(asked, registered) ? yield* whereIAm : void 0, registered);
|
|
4610
|
+
if (coverage._tag === "refused") return yield* new CliError.UserError({ cause: coverage.why });
|
|
4611
|
+
const { repos, leftOut } = coverage;
|
|
4322
4612
|
if (repos.length === 0) return {
|
|
4323
4613
|
repos,
|
|
4614
|
+
leftOut,
|
|
4324
4615
|
facts: [],
|
|
4325
4616
|
troubles: []
|
|
4326
4617
|
};
|
|
@@ -4356,6 +4647,7 @@ const sweep = Effect.fn("sweep")(function* (report) {
|
|
|
4356
4647
|
}), { concurrency }));
|
|
4357
4648
|
return {
|
|
4358
4649
|
repos,
|
|
4650
|
+
leftOut,
|
|
4359
4651
|
facts: swept.got,
|
|
4360
4652
|
troubles: [...found.troubles, ...swept.troubles]
|
|
4361
4653
|
};
|
|
@@ -4367,7 +4659,25 @@ const sweep = Effect.fn("sweep")(function* (report) {
|
|
|
4367
4659
|
* three times over. It gives the heartbeat no aside, so a piped `dw-mc status`
|
|
4368
4660
|
* prints exactly what it printed before there was a heartbeat at all.
|
|
4369
4661
|
*/
|
|
4370
|
-
const sweeping = beating((since) => `sweeping · ${since}`, (says) => sweep((swept) => says(saying$1(swept))));
|
|
4662
|
+
const sweeping = (asked) => beating((since) => `sweeping · ${since}`, (says) => sweep(asked, (swept) => says(saying$1(swept))));
|
|
4663
|
+
/** `--repo`, for a command that sweeps: the one registered repository to cover. */
|
|
4664
|
+
const repoFlag = Flag.String("repo").pipe(Flag.withDescription("Cover this registered repository only, as owner/name, wherever I stand"), Flag.optional);
|
|
4665
|
+
/** `--all`, for a command that sweeps: every registered repository, even from inside one. */
|
|
4666
|
+
const allFlag = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("Cover every registered repository, and not only the one I stand in"));
|
|
4667
|
+
/** What the two flags ask a sweep to cover. */
|
|
4668
|
+
const askedOf$1 = (flags) => ({
|
|
4669
|
+
repo: Option.getOrUndefined(flags.repo),
|
|
4670
|
+
all: flags.all
|
|
4671
|
+
});
|
|
4672
|
+
/**
|
|
4673
|
+
* The word a narrowed report owes me about what it left out, so a table of one
|
|
4674
|
+
* repository is not read as the whole picture.
|
|
4675
|
+
*/
|
|
4676
|
+
const printLeftOut = Effect.fn("sweep.printLeftOut")(function* (report) {
|
|
4677
|
+
if (report.leftOut === 0) return;
|
|
4678
|
+
yield* Console.log("");
|
|
4679
|
+
yield* Console.log(`Only ${report.repos.join(", ")}. --all covers all ${report.repos.length + report.leftOut} registered repositories.`);
|
|
4680
|
+
});
|
|
4371
4681
|
/** What a sweep could not read, under a heading, so the table above it stands alone. */
|
|
4372
4682
|
const printTroubles = Effect.fn("sweep.printTroubles")(function* (troubles) {
|
|
4373
4683
|
if (troubles.length === 0) return;
|
|
@@ -4376,16 +4686,20 @@ const printTroubles = Effect.fn("sweep.printTroubles")(function* (troubles) {
|
|
|
4376
4686
|
for (const trouble of troubles) yield* Console.log(` ${trouble.where} ${trouble.detail}`);
|
|
4377
4687
|
});
|
|
4378
4688
|
/**
|
|
4379
|
-
* Refreshes what mission control knows about
|
|
4689
|
+
* Refreshes what mission control knows about the tracked PRs it covers.
|
|
4380
4690
|
*
|
|
4381
4691
|
* `dw-mc status` does this too, so this command is for the pass on its own:
|
|
4382
4692
|
* warming the state directory, or seeing what GitHub would not answer.
|
|
4383
4693
|
*/
|
|
4384
|
-
const sweepCommand = Command.make("sweep", {
|
|
4385
|
-
|
|
4694
|
+
const sweepCommand = Command.make("sweep", {
|
|
4695
|
+
repo: repoFlag,
|
|
4696
|
+
all: allFlag
|
|
4697
|
+
}, Effect.fn("sweep.command")(function* (flags) {
|
|
4698
|
+
const report = yield* sweeping(askedOf$1(flags));
|
|
4386
4699
|
yield* Console.log(report.repos.length === 0 ? "No repositories registered. Run dw-mc init inside a repository to register it." : `Swept ${count(report.facts.length, "pull request")} across ${repositories(report.repos.length)}`);
|
|
4700
|
+
yield* printLeftOut(report);
|
|
4387
4701
|
yield* printTroubles(report.troubles);
|
|
4388
|
-
}, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Refresh what mission control knows about
|
|
4702
|
+
}, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Refresh what mission control knows about the tracked pull requests of the repository I stand in, or of every one"));
|
|
4389
4703
|
//#endregion
|
|
4390
4704
|
//#region src/domain/pick.ts
|
|
4391
4705
|
/**
|
|
@@ -4522,6 +4836,113 @@ const recordRerun = Effect.fn("rerun.recordRerun")(function* (repo, number, head
|
|
|
4522
4836
|
yield* (yield* storeFor("reruns", Rerun)).set(prKey(repo, number), { head });
|
|
4523
4837
|
});
|
|
4524
4838
|
//#endregion
|
|
4839
|
+
//#region src/domain/watermark.ts
|
|
4840
|
+
/** What a row turns on, which is what a watermark keeps of it. */
|
|
4841
|
+
const Said = Schema.Struct({
|
|
4842
|
+
bucket: Bucket,
|
|
4843
|
+
mergeable: Mergeability,
|
|
4844
|
+
checks: ChecksState,
|
|
4845
|
+
/** Whether the flaky classifier excused a red CI. */
|
|
4846
|
+
excused: Schema.Boolean,
|
|
4847
|
+
reviewDecision: ReviewDecision,
|
|
4848
|
+
blockingFindings: Schema.Int,
|
|
4849
|
+
newestHumanCommentAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
4850
|
+
draft: Schema.Boolean
|
|
4851
|
+
});
|
|
4852
|
+
/**
|
|
4853
|
+
* The moment a tracked PR's row was last shown to me, and what the row said.
|
|
4854
|
+
*
|
|
4855
|
+
* It is kept on its own rather than on `Facts`, because a quiet PR carries its
|
|
4856
|
+
* old facts forward unchanged and a moment stored there would be about the
|
|
4857
|
+
* wrong thing. What it holds is only what a row turns on, so a fact the sweep
|
|
4858
|
+
* starts reading later does not make every row look new.
|
|
4859
|
+
*
|
|
4860
|
+
* Only a command that puts the row on screen writes one. A sweep on its own
|
|
4861
|
+
* shows nothing, and moving the watermark there would erase movement nobody saw.
|
|
4862
|
+
*/
|
|
4863
|
+
const Watermark = Schema.Struct({
|
|
4864
|
+
at: Schema.DateTimeUtcFromString,
|
|
4865
|
+
...Said.fields
|
|
4866
|
+
});
|
|
4867
|
+
const keyOf = ({ facts }) => prKey(facts.repo, facts.number);
|
|
4868
|
+
/** What a row says. */
|
|
4869
|
+
const said = ({ facts, placement }) => ({
|
|
4870
|
+
bucket: placement.bucket,
|
|
4871
|
+
mergeable: facts.mergeable,
|
|
4872
|
+
checks: facts.checks,
|
|
4873
|
+
excused: facts.ciFlaky !== null,
|
|
4874
|
+
reviewDecision: facts.reviewDecision,
|
|
4875
|
+
blockingFindings: facts.blockingFindings,
|
|
4876
|
+
newestHumanCommentAt: facts.newestHumanCommentAt,
|
|
4877
|
+
draft: facts.draft
|
|
4878
|
+
});
|
|
4879
|
+
/** What a row said at the moment it was shown. */
|
|
4880
|
+
const sighted = (placed, at) => ({
|
|
4881
|
+
at,
|
|
4882
|
+
...said(placed)
|
|
4883
|
+
});
|
|
4884
|
+
const ci = (it) => it.checks === "red" && it.excused ? "red, called flaky" : it.checks;
|
|
4885
|
+
const review$1 = {
|
|
4886
|
+
approved: "approved",
|
|
4887
|
+
"changes-requested": "changes requested",
|
|
4888
|
+
"review-required": "review required",
|
|
4889
|
+
none: "no review decision"
|
|
4890
|
+
};
|
|
4891
|
+
/**
|
|
4892
|
+
* The facts that moved between two sightings, said the way the row would say them.
|
|
4893
|
+
*
|
|
4894
|
+
* The head is left out on purpose: it changes on every push, and a list that
|
|
4895
|
+
* leads with it drowns what the push did. A mergeability GitHub has not worked
|
|
4896
|
+
* out yet is left out too, because it flickers to unknown and back on its own.
|
|
4897
|
+
*/
|
|
4898
|
+
const moved = (then, now) => [
|
|
4899
|
+
then.mergeable !== now.mergeable && then.mergeable !== "unknown" && now.mergeable !== "unknown" ? `${then.mergeable} → ${now.mergeable}` : null,
|
|
4900
|
+
ci(then) !== ci(now) ? `CI ${ci(then)} → ${ci(now)}` : null,
|
|
4901
|
+
then.reviewDecision !== now.reviewDecision ? `${review$1[then.reviewDecision]} → ${review$1[now.reviewDecision]}` : null,
|
|
4902
|
+
then.blockingFindings !== now.blockingFindings ? `${then.blockingFindings} → ${now.blockingFindings} blocking findings` : null,
|
|
4903
|
+
isAfter(now.newestHumanCommentAt, then.newestHumanCommentAt) ? "a new comment" : null,
|
|
4904
|
+
then.draft !== now.draft ? now.draft ? "back to draft" : "out of draft" : null
|
|
4905
|
+
].filter((it) => it !== null);
|
|
4906
|
+
/**
|
|
4907
|
+
* What happened to a row since `watermark`, the last time it was shown.
|
|
4908
|
+
*
|
|
4909
|
+
* Movement is a bucket transition plus the facts that moved with it. Either one
|
|
4910
|
+
* alone misses something: every field compared drowns in the head, and a bucket
|
|
4911
|
+
* on its own says nothing when CI goes green on a PR still mine for another
|
|
4912
|
+
* reason.
|
|
4913
|
+
*/
|
|
4914
|
+
const since = (watermark, placed) => {
|
|
4915
|
+
if (watermark === void 0) return { _tag: "new" };
|
|
4916
|
+
const now = said(placed);
|
|
4917
|
+
const what = moved(watermark, now);
|
|
4918
|
+
const from = watermark.bucket === now.bucket ? void 0 : watermark.bucket;
|
|
4919
|
+
return from === void 0 && what.length === 0 ? { _tag: "still" } : {
|
|
4920
|
+
_tag: "moved",
|
|
4921
|
+
from,
|
|
4922
|
+
what
|
|
4923
|
+
};
|
|
4924
|
+
};
|
|
4925
|
+
/**
|
|
4926
|
+
* What happened to each of these rows since it was last shown.
|
|
4927
|
+
*
|
|
4928
|
+
* It answers for any row, and a row it was not given is one it has no
|
|
4929
|
+
* watermark for, which is a row never shown.
|
|
4930
|
+
*/
|
|
4931
|
+
const sinceShown = Effect.fn("watermark.sinceShown")(function* (placed) {
|
|
4932
|
+
const store = yield* storeFor("watermarks", Watermark);
|
|
4933
|
+
const found = new Map(yield* Effect.forEach(placed, (it) => {
|
|
4934
|
+
const key = keyOf(it);
|
|
4935
|
+
return Effect.map(remembered(store.get(key)), (mark) => [key, since(Option.getOrUndefined(mark), it)]);
|
|
4936
|
+
}));
|
|
4937
|
+
return (it) => found.get(keyOf(it)) ?? { _tag: "new" };
|
|
4938
|
+
});
|
|
4939
|
+
/** Records that these rows were shown to me, now, saying what they say. */
|
|
4940
|
+
const markShown = Effect.fn("watermark.markShown")(function* (placed) {
|
|
4941
|
+
const store = yield* storeFor("watermarks", Watermark);
|
|
4942
|
+
const now = yield* DateTime.now;
|
|
4943
|
+
yield* Effect.forEach(placed, (it) => store.set(keyOf(it), sighted(it, now)), { discard: true });
|
|
4944
|
+
});
|
|
4945
|
+
//#endregion
|
|
4525
4946
|
//#region src/cli/pick.ts
|
|
4526
4947
|
/** A title cut this short says nothing, so a row that tight loses the column instead. */
|
|
4527
4948
|
const shortest = 12;
|
|
@@ -4547,9 +4968,10 @@ const screenRoom = (screen, paint) => screen === 0 ? Number.POSITIVE_INFINITY :
|
|
|
4547
4968
|
* The cells come from there rather than being built again here, so the list I
|
|
4548
4969
|
* pick from and the table I read are the same rows with the bucket moved onto
|
|
4549
4970
|
* each of them. A prompt has no headings to group under, so the bucket is named
|
|
4550
|
-
* on every row; the rows are still in the order the buckets are acted on.
|
|
4971
|
+
* on every row; the rows are still in the order the buckets are acted on. The
|
|
4972
|
+
* gutter in front says what moved since I last looked, as it does in the table.
|
|
4551
4973
|
*/
|
|
4552
|
-
const cellsOf = ({ placed, stamped }, room, paint) => cells(placed, stamped, room, paint, "named");
|
|
4974
|
+
const cellsOf = ({ placed, stamped }, since, room, paint) => cells(placed, stamped, since, room, paint, "named");
|
|
4553
4975
|
/**
|
|
4554
4976
|
* Every tracked PR as something to pick, aligned down the whole list.
|
|
4555
4977
|
*
|
|
@@ -4560,13 +4982,13 @@ const cellsOf = ({ placed, stamped }, room, paint) => cells(placed, stamped, roo
|
|
|
4560
4982
|
* tells me less than the room it took. A screen too narrow for all four columns
|
|
4561
4983
|
* loses the title's column rather than the reason's words.
|
|
4562
4984
|
*/
|
|
4563
|
-
const choicesOf = (standings, screen, paint) => {
|
|
4564
|
-
const measured = standings.map((it) => cellsOf(it, Number.POSITIVE_INFINITY, paint));
|
|
4985
|
+
const choicesOf = (standings, sinceOf, screen, paint) => {
|
|
4986
|
+
const measured = standings.map((it) => cellsOf(it, sinceOf(it.placed), Number.POSITIVE_INFINITY, paint));
|
|
4565
4987
|
const widest = (index) => Math.max(...measured.map((row) => visible(row[index] ?? "")));
|
|
4566
4988
|
const room = screenRoom(screen, paint) - (widest(0) + widest(1) + widest(3)) - 9;
|
|
4567
4989
|
const told = room >= shortest;
|
|
4568
4990
|
const rows = table(standings.map((it) => {
|
|
4569
|
-
const row = cellsOf(it, told ? room : 0, paint);
|
|
4991
|
+
const row = cellsOf(it, sinceOf(it.placed), told ? room : 0, paint);
|
|
4570
4992
|
return told ? row : [
|
|
4571
4993
|
row[0] ?? "",
|
|
4572
4994
|
row[1] ?? "",
|
|
@@ -4603,7 +5025,7 @@ const where = (facts) => `${facts.repo}#${facts.number}`;
|
|
|
4603
5025
|
* and a merge must never cost only that (ADR 0008).
|
|
4604
5026
|
*/
|
|
4605
5027
|
const picker = (dispatch) => Effect.fn("pick")(function* () {
|
|
4606
|
-
const report = yield* sweeping;
|
|
5028
|
+
const report = yield* sweeping(everything);
|
|
4607
5029
|
if (report.repos.length === 0) {
|
|
4608
5030
|
yield* Console.log("No repositories registered. Run dw-mc init inside a repository to register it.");
|
|
4609
5031
|
return;
|
|
@@ -4623,7 +5045,10 @@ const picker = (dispatch) => Effect.fn("pick")(function* () {
|
|
|
4623
5045
|
yield* Console.log("No open pull requests.");
|
|
4624
5046
|
return;
|
|
4625
5047
|
}
|
|
4626
|
-
const
|
|
5048
|
+
const shown = standings.map((it) => it.placed);
|
|
5049
|
+
const choices = choicesOf(standings, yield* sinceShown(shown), yield* width, yield* Paint);
|
|
5050
|
+
yield* markShown(shown);
|
|
5051
|
+
const chosen = yield* pick("Which pull request?", choices);
|
|
4627
5052
|
if (Option.isNone(chosen)) return;
|
|
4628
5053
|
const facts = chosen.value.placed.facts;
|
|
4629
5054
|
const offer = yield* pick(`What do I do with ${where(facts)}?`, actionChoices(actionsFor(chosen.value)));
|
|
@@ -5290,6 +5715,13 @@ const stampCommand = Command.make("stamp", {
|
|
|
5290
5715
|
}, Effect.catchTag(["ConfigMalformed"], asUserError))).pipe(Command.withDescription("Print my stamp on one pull request, or withdraw it by hand"));
|
|
5291
5716
|
//#endregion
|
|
5292
5717
|
//#region src/cli/status.ts
|
|
5718
|
+
/** What moved a row, said on its own line above the group it now sits in. */
|
|
5719
|
+
const movement = (placed, since) => {
|
|
5720
|
+
if (since._tag !== "moved") return [];
|
|
5721
|
+
const from = since.from === void 0 ? "" : ` from ${heading[since.from]}`;
|
|
5722
|
+
const what = since.what.length === 0 ? "" : `: ${since.what.join(", ")}`;
|
|
5723
|
+
return [` ↳ ${reference(placed.facts)}${from}${what}`];
|
|
5724
|
+
};
|
|
5293
5725
|
/**
|
|
5294
5726
|
* Every tracked PR under the bucket it sits in, in the order I act on them.
|
|
5295
5727
|
*
|
|
@@ -5297,9 +5729,12 @@ const stampCommand = Command.make("stamp", {
|
|
|
5297
5729
|
* the whole table rather than restarting under each heading, and they are ruled
|
|
5298
5730
|
* apart: three columns of prose run into one another without a rule, and the
|
|
5299
5731
|
* middle one is a commit subject that can end in anything.
|
|
5732
|
+
*
|
|
5733
|
+
* What moved since I last looked is marked in the gutter the rows are indented
|
|
5734
|
+
* by, so a mark costs no column and a row with none reads as it always did.
|
|
5300
5735
|
*/
|
|
5301
|
-
const lines = (grouped, stamped, paint) => {
|
|
5302
|
-
const rows = table(grouped.flatMap((it) => it.placed.map((placed) => cells(placed, stamped.has(prKey(placed.facts.repo, placed.facts.number)), 56, paint, "marker"))), rule);
|
|
5736
|
+
const lines = (grouped, stamped, sinceOf, paint) => {
|
|
5737
|
+
const rows = table(grouped.flatMap((it) => it.placed.map((placed) => cells(placed, stamped.has(prKey(placed.facts.repo, placed.facts.number)), sinceOf(placed), 56, paint, "marker"))), rule);
|
|
5303
5738
|
let taken = 0;
|
|
5304
5739
|
return grouped.flatMap((it, index) => {
|
|
5305
5740
|
const mine = rows.slice(taken, taken + it.placed.length);
|
|
@@ -5307,7 +5742,8 @@ const lines = (grouped, stamped, paint) => {
|
|
|
5307
5742
|
return [
|
|
5308
5743
|
...index === 0 ? [] : [""],
|
|
5309
5744
|
heading[it.bucket],
|
|
5310
|
-
...
|
|
5745
|
+
...it.placed.flatMap((placed) => movement(placed, sinceOf(placed))),
|
|
5746
|
+
...mine
|
|
5311
5747
|
];
|
|
5312
5748
|
});
|
|
5313
5749
|
};
|
|
@@ -5316,17 +5752,23 @@ const lines = (grouped, stamped, paint) => {
|
|
|
5316
5752
|
*
|
|
5317
5753
|
* It sweeps first, every time: a table I read is never one I forgot to refresh.
|
|
5318
5754
|
*/
|
|
5319
|
-
const status = Command.make("status", {
|
|
5320
|
-
|
|
5755
|
+
const status = Command.make("status", {
|
|
5756
|
+
repo: repoFlag,
|
|
5757
|
+
all: allFlag
|
|
5758
|
+
}, Effect.fn("status")(function* (flags) {
|
|
5759
|
+
const report = yield* sweeping(askedOf$1(flags));
|
|
5321
5760
|
if (report.repos.length === 0) {
|
|
5322
5761
|
yield* Console.log("No repositories registered. Run dw-mc init inside a repository to register it.");
|
|
5323
5762
|
return;
|
|
5324
5763
|
}
|
|
5325
5764
|
const grouped = group(report.facts);
|
|
5326
5765
|
if (grouped.length === 0) yield* Console.log("No open pull requests.");
|
|
5327
|
-
|
|
5766
|
+
const shown = grouped.flatMap((it) => it.placed);
|
|
5767
|
+
for (const line of lines(grouped, yield* stampedAmong(report.facts), yield* sinceShown(shown), yield* Paint)) yield* Console.log(line);
|
|
5768
|
+
yield* printLeftOut(report);
|
|
5328
5769
|
yield* printTroubles(report.troubles);
|
|
5329
|
-
|
|
5770
|
+
yield* markShown(shown);
|
|
5771
|
+
}, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Show which bucket every tracked pull request of the repository I stand in, or of every one, sits in, which ones I have stamped, and what moved since I last looked"));
|
|
5330
5772
|
//#endregion
|
|
5331
5773
|
//#region src/cli/uninstall.ts
|
|
5332
5774
|
const configFlag = Flag.Boolean("config").pipe(Flag.withDefault(false), Flag.withDescription("Take the configuration file too, and not only the state"));
|
|
@@ -5382,7 +5824,7 @@ const uninstall = Command.make("uninstall", {
|
|
|
5382
5824
|
}] : [])).pipe(Effect.map((found_) => found_.flat()));
|
|
5383
5825
|
yield* Effect.forEach(removes({
|
|
5384
5826
|
directory: found.directory,
|
|
5385
|
-
size: weight(everything(found))
|
|
5827
|
+
size: weight(everything$1(found))
|
|
5386
5828
|
}, alsoConfig && configured ? file : void 0, paint), (line) => Console.log(line));
|
|
5387
5829
|
if (holds.length > 0) {
|
|
5388
5830
|
yield* Effect.forEach(held(holds, paint), (line) => Console.log(line));
|
|
@@ -5409,7 +5851,7 @@ const uninstall = Command.make("uninstall", {
|
|
|
5409
5851
|
* Running from source leaves the constant undeclared rather than undefined, so
|
|
5410
5852
|
* the check has to be `typeof` and the fallback is what a test reads.
|
|
5411
5853
|
*/
|
|
5412
|
-
const version = "0.
|
|
5854
|
+
const version = "0.7.0";
|
|
5413
5855
|
/** Where the project lives, printed beside the version in the header. */
|
|
5414
5856
|
const projectUrl = "github.com/dominikwozniak/dw-mc";
|
|
5415
5857
|
const subcommands = [
|
|
@@ -5422,6 +5864,7 @@ const subcommands = [
|
|
|
5422
5864
|
rerun,
|
|
5423
5865
|
resolve,
|
|
5424
5866
|
merge,
|
|
5867
|
+
forgetCommand,
|
|
5425
5868
|
sweepCommand,
|
|
5426
5869
|
status,
|
|
5427
5870
|
stampCommand,
|