dw-mc 0.6.0 → 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 +406 -45
- 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",
|
|
@@ -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
|
*
|
|
@@ -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
|
|
@@ -1966,13 +2019,13 @@ const Actor = Schema.NullOr(Schema.Struct({
|
|
|
1966
2019
|
login: Schema.String,
|
|
1967
2020
|
__typename: Schema.String
|
|
1968
2021
|
}));
|
|
1969
|
-
const Said = Schema.Struct({
|
|
2022
|
+
const Said$1 = Schema.Struct({
|
|
1970
2023
|
author: Actor,
|
|
1971
2024
|
body: Schema.String,
|
|
1972
2025
|
createdAt: Schema.DateTimeUtcFromString
|
|
1973
2026
|
});
|
|
1974
2027
|
const Conversation = Schema.fromJsonString(Schema.Struct({ data: Schema.Struct({ repository: Schema.Struct({ pullRequest: Schema.Struct({
|
|
1975
|
-
comments: Schema.Struct({ nodes: Schema.Array(Said) }),
|
|
2028
|
+
comments: Schema.Struct({ nodes: Schema.Array(Said$1) }),
|
|
1976
2029
|
reviews: Schema.Struct({ nodes: Schema.Array(Schema.Struct({
|
|
1977
2030
|
author: Actor,
|
|
1978
2031
|
body: Schema.String,
|
|
@@ -1983,7 +2036,7 @@ const Conversation = Schema.fromJsonString(Schema.Struct({ data: Schema.Struct({
|
|
|
1983
2036
|
isOutdated: Schema.Boolean,
|
|
1984
2037
|
path: Schema.NullOr(Schema.String),
|
|
1985
2038
|
line: Schema.NullOr(Schema.Int),
|
|
1986
|
-
comments: Schema.Struct({ nodes: Schema.Array(Said) })
|
|
2039
|
+
comments: Schema.Struct({ nodes: Schema.Array(Said$1) })
|
|
1987
2040
|
})) })
|
|
1988
2041
|
}) }) }) }));
|
|
1989
2042
|
const remark = (said, at) => said.author === null || at === null || said.body.trim() === "" ? [] : [{
|
|
@@ -2155,12 +2208,15 @@ const Facts = Schema.Struct({
|
|
|
2155
2208
|
newestHumanCommentAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
2156
2209
|
myLastCommentAt: Schema.NullOr(Schema.DateTimeUtcFromString),
|
|
2157
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),
|
|
2158
2213
|
/** The head a review run has already covered, or null where none has. */
|
|
2159
2214
|
reviewRunHead: Schema.NullOr(Schema.String),
|
|
2160
2215
|
/** Findings on this head that withhold the stamp, at the bar `stamp.blocks_on` sets. */
|
|
2161
2216
|
blockingFindings: Schema.Int
|
|
2162
2217
|
});
|
|
2163
|
-
|
|
2218
|
+
/** The one place a tracked PR sits at a time, named for what it waits on. */
|
|
2219
|
+
const Bucket = Schema.Literals([
|
|
2164
2220
|
"needs-me",
|
|
2165
2221
|
"needs-review-run",
|
|
2166
2222
|
"waiting-on-others",
|
|
@@ -2193,6 +2249,16 @@ const unanswered = "a comment I have not answered";
|
|
|
2193
2249
|
*/
|
|
2194
2250
|
const blockedBy = (n) => `${n} blocking finding${n === 1 ? "" : "s"}`;
|
|
2195
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
|
+
/**
|
|
2196
2262
|
* The first of the rules that makes a PR mine to move, or null when none
|
|
2197
2263
|
* does. The order is the order I would fix them in: a conflict makes every
|
|
2198
2264
|
* other signal on the PR stale, and a red build is worth more than a comment.
|
|
@@ -2203,7 +2269,7 @@ const needsMe = (facts) => {
|
|
|
2203
2269
|
if (facts.checks === "red" && facts.ciFlaky === null) return "CI is red";
|
|
2204
2270
|
if (facts.reviewDecision === "changes-requested") return "changes requested";
|
|
2205
2271
|
if (facts.blockingFindings > 0) return blockedBy(facts.blockingFindings);
|
|
2206
|
-
if (isAfter(facts.newestHumanCommentAt,
|
|
2272
|
+
if (isAfter(facts.newestHumanCommentAt, answeredAt(facts))) return unanswered;
|
|
2207
2273
|
return null;
|
|
2208
2274
|
};
|
|
2209
2275
|
/**
|
|
@@ -2725,6 +2791,20 @@ const marker = {
|
|
|
2725
2791
|
"waiting-on-others": "○",
|
|
2726
2792
|
ready: "◆"
|
|
2727
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}`;
|
|
2728
2808
|
/** The colour a bucket is said in: red is mine, yellow is next, green is done, dim is not my turn. */
|
|
2729
2809
|
const tint = (paint, bucket) => ({
|
|
2730
2810
|
"needs-me": paint.red,
|
|
@@ -2737,6 +2817,8 @@ const rule = " │ ";
|
|
|
2737
2817
|
/**
|
|
2738
2818
|
* One row: which pull request, what it is, and what it waits on.
|
|
2739
2819
|
*
|
|
2820
|
+
* In front of it all is the gutter, which says what moved since I last looked.
|
|
2821
|
+
*
|
|
2740
2822
|
* A stamp is a mark beside the pull request rather than a column of its own, so
|
|
2741
2823
|
* a table where nothing is stamped is exactly the table it was before: the
|
|
2742
2824
|
* stamp is a thing I look for, not a thing I read every row of.
|
|
@@ -2754,25 +2836,51 @@ const rule = " │ ";
|
|
|
2754
2836
|
* straight, so its rows say it in more than one place and open the pull request
|
|
2755
2837
|
* besides.
|
|
2756
2838
|
*/
|
|
2757
|
-
const cells = (placed, stamped, room, paint, lead) => {
|
|
2839
|
+
const cells = (placed, stamped, since, room, paint, lead) => {
|
|
2758
2840
|
const { facts } = placed;
|
|
2759
2841
|
const { bucket } = placed.placement;
|
|
2760
2842
|
const say = tint(paint, bucket);
|
|
2761
|
-
const reference = `${facts.repo}#${facts.number}`;
|
|
2762
2843
|
const named = lead === "named";
|
|
2763
|
-
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];
|
|
2764
2846
|
return named ? [
|
|
2765
|
-
say(`${marker[bucket]} ${heading[bucket]}`)
|
|
2847
|
+
`${front} ${say(`${marker[bucket]} ${heading[bucket]}`)}`,
|
|
2766
2848
|
pr,
|
|
2767
2849
|
truncate(facts.title, room),
|
|
2768
2850
|
placed.placement.reason
|
|
2769
2851
|
] : [
|
|
2770
|
-
`${say(marker[bucket])} ${pr}`,
|
|
2852
|
+
`${front} ${say(marker[bucket])} ${pr}`,
|
|
2771
2853
|
paint.dim(truncate(facts.title, room)),
|
|
2772
2854
|
say(placed.placement.reason)
|
|
2773
2855
|
];
|
|
2774
2856
|
};
|
|
2775
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
|
|
2776
2884
|
//#region src/domain/comments.ts
|
|
2777
2885
|
/**
|
|
2778
2886
|
* One thread's share of a strand, cut to what is worth reading.
|
|
@@ -2813,9 +2921,24 @@ const shown = (threads, options) => {
|
|
|
2813
2921
|
bots: kept.flatMap((it) => only(it, (bot) => bot, options.since, options.all))
|
|
2814
2922
|
};
|
|
2815
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)));
|
|
2816
2938
|
//#endregion
|
|
2817
2939
|
//#region src/cli/comments.ts
|
|
2818
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"));
|
|
2819
2942
|
/** Where a thread hangs: a line of the diff, or the pull request itself. */
|
|
2820
2943
|
const where$1 = (thread) => thread.path === null ? "Conversation" : thread.line === null ? thread.path : `${thread.path}:${thread.line}`;
|
|
2821
2944
|
/**
|
|
@@ -2861,45 +2984,75 @@ const nothing$1 = (facts, all) => {
|
|
|
2861
2984
|
const placement = place(facts);
|
|
2862
2985
|
const rest = `dw-mc comments ${facts.number} --all prints the whole conversation.`;
|
|
2863
2986
|
return placement.bucket === "needs-me" && placement.reason === "a comment I have not answered" ? [
|
|
2864
|
-
"Nothing here is waiting on you: every thread is resolved, outdated, or older than your last comment or
|
|
2865
|
-
`${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.`,
|
|
2866
2989
|
rest
|
|
2867
|
-
] : [`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];
|
|
2868
2991
|
};
|
|
2869
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
|
+
/**
|
|
2870
3016
|
* The conversation on one tracked pull request, and nothing else.
|
|
2871
3017
|
*
|
|
2872
3018
|
* What it shows by default is what the bucket rule measures: the comments newer
|
|
2873
|
-
* than the
|
|
2874
|
-
* put the pull request in Needs me. Reading it answers
|
|
2875
|
-
* 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.
|
|
2876
3022
|
*
|
|
2877
3023
|
* The cutoff is read off the last sweep rather than worked out again here, so
|
|
2878
3024
|
* the command shows exactly what `dw-mc status` counted rather than a second
|
|
2879
3025
|
* opinion about it.
|
|
2880
3026
|
*
|
|
2881
|
-
* It writes nothing
|
|
2882
|
-
*
|
|
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.
|
|
2883
3031
|
*/
|
|
2884
3032
|
const comments = Command.make("comments", {
|
|
2885
3033
|
pr: prArgument,
|
|
2886
|
-
all: allFlag$1
|
|
2887
|
-
|
|
3034
|
+
all: allFlag$1,
|
|
3035
|
+
ack: ackFlag
|
|
3036
|
+
}, Effect.fn("comments")(function* ({ ack, all, pr }) {
|
|
2888
3037
|
const { number, repo } = yield* forPr(pr);
|
|
2889
3038
|
const facts = yield* swept(repo, number);
|
|
2890
3039
|
const paint = yield* Paint;
|
|
2891
|
-
const
|
|
2892
|
-
|
|
3040
|
+
const threads = yield* reading(`${repo}#${number}`, prConversation(repo, number));
|
|
3041
|
+
const view = shown(threads, {
|
|
3042
|
+
since: answeredAt(facts),
|
|
2893
3043
|
all
|
|
2894
3044
|
});
|
|
2895
|
-
if (view.people.length === 0 && view.bots.length === 0)
|
|
2896
|
-
|
|
2897
|
-
|
|
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));
|
|
2898
3050
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
}
|
|
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"));
|
|
2903
3056
|
//#endregion
|
|
2904
3057
|
//#region src/cli/findings.ts
|
|
2905
3058
|
/** The findings as the JSON the schema defines, rather than as this file spells it. */
|
|
@@ -3488,6 +3641,83 @@ const fix = Command.make("fix", {
|
|
|
3488
3641
|
yield* Console.log(`Once you have pushed, dw-mc review ${number} reviews the new head as a new run.`);
|
|
3489
3642
|
}, Effect.catchTag(userFacingAndSession, asUserError))).pipe(Command.withDescription("Pick findings from the current review run and open a fix session on them"));
|
|
3490
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
|
|
3491
3721
|
//#region src/cli/init.ts
|
|
3492
3722
|
const effortFlag$1 = Flag.Literals("effort", [
|
|
3493
3723
|
"low",
|
|
@@ -3924,6 +4154,10 @@ const decide$3 = (situation) => {
|
|
|
3924
4154
|
*
|
|
3925
4155
|
* Typing the command is the confirmation, so it takes no flag. The picker,
|
|
3926
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.
|
|
3927
4161
|
*/
|
|
3928
4162
|
const merge = Command.make("merge", { pr: prArgument }, Effect.fn("merge")(function* ({ pr }) {
|
|
3929
4163
|
const { number, repo, settings } = yield* forPr(pr);
|
|
@@ -3944,6 +4178,7 @@ const merge = Command.make("merge", { pr: prArgument }, Effect.fn("merge")(funct
|
|
|
3944
4178
|
yield* mergePr(repo, number);
|
|
3945
4179
|
yield* Console.log(`${repo}#${number} ${short(head)} squash-merged into ${view.baseRefName}, and ${view.headRefName} deleted`);
|
|
3946
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.`)));
|
|
3947
4182
|
}, Effect.catchTag(userFacing, asUserError))).pipe(Command.withDescription("Squash-merge a Ready, stamped pull request of mine and delete its branch"));
|
|
3948
4183
|
//#endregion
|
|
3949
4184
|
//#region src/domain/coverage.ts
|
|
@@ -4325,6 +4560,7 @@ const sweepPr = Effect.fn("sweep.pullRequest")(function* (store, me, found, sett
|
|
|
4325
4560
|
newestHumanCommentAt,
|
|
4326
4561
|
myLastCommentAt: newest(writtenBy(comments, me)),
|
|
4327
4562
|
myLastCommitAt,
|
|
4563
|
+
acknowledgedAt: yield* acknowledgedAt(found.repo, found.number),
|
|
4328
4564
|
...reviewed
|
|
4329
4565
|
};
|
|
4330
4566
|
yield* store.set(key, facts);
|
|
@@ -4600,6 +4836,113 @@ const recordRerun = Effect.fn("rerun.recordRerun")(function* (repo, number, head
|
|
|
4600
4836
|
yield* (yield* storeFor("reruns", Rerun)).set(prKey(repo, number), { head });
|
|
4601
4837
|
});
|
|
4602
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
|
|
4603
4946
|
//#region src/cli/pick.ts
|
|
4604
4947
|
/** A title cut this short says nothing, so a row that tight loses the column instead. */
|
|
4605
4948
|
const shortest = 12;
|
|
@@ -4625,9 +4968,10 @@ const screenRoom = (screen, paint) => screen === 0 ? Number.POSITIVE_INFINITY :
|
|
|
4625
4968
|
* The cells come from there rather than being built again here, so the list I
|
|
4626
4969
|
* pick from and the table I read are the same rows with the bucket moved onto
|
|
4627
4970
|
* each of them. A prompt has no headings to group under, so the bucket is named
|
|
4628
|
-
* 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.
|
|
4629
4973
|
*/
|
|
4630
|
-
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");
|
|
4631
4975
|
/**
|
|
4632
4976
|
* Every tracked PR as something to pick, aligned down the whole list.
|
|
4633
4977
|
*
|
|
@@ -4638,13 +4982,13 @@ const cellsOf = ({ placed, stamped }, room, paint) => cells(placed, stamped, roo
|
|
|
4638
4982
|
* tells me less than the room it took. A screen too narrow for all four columns
|
|
4639
4983
|
* loses the title's column rather than the reason's words.
|
|
4640
4984
|
*/
|
|
4641
|
-
const choicesOf = (standings, screen, paint) => {
|
|
4642
|
-
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));
|
|
4643
4987
|
const widest = (index) => Math.max(...measured.map((row) => visible(row[index] ?? "")));
|
|
4644
4988
|
const room = screenRoom(screen, paint) - (widest(0) + widest(1) + widest(3)) - 9;
|
|
4645
4989
|
const told = room >= shortest;
|
|
4646
4990
|
const rows = table(standings.map((it) => {
|
|
4647
|
-
const row = cellsOf(it, told ? room : 0, paint);
|
|
4991
|
+
const row = cellsOf(it, sinceOf(it.placed), told ? room : 0, paint);
|
|
4648
4992
|
return told ? row : [
|
|
4649
4993
|
row[0] ?? "",
|
|
4650
4994
|
row[1] ?? "",
|
|
@@ -4701,7 +5045,10 @@ const picker = (dispatch) => Effect.fn("pick")(function* () {
|
|
|
4701
5045
|
yield* Console.log("No open pull requests.");
|
|
4702
5046
|
return;
|
|
4703
5047
|
}
|
|
4704
|
-
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);
|
|
4705
5052
|
if (Option.isNone(chosen)) return;
|
|
4706
5053
|
const facts = chosen.value.placed.facts;
|
|
4707
5054
|
const offer = yield* pick(`What do I do with ${where(facts)}?`, actionChoices(actionsFor(chosen.value)));
|
|
@@ -5368,6 +5715,13 @@ const stampCommand = Command.make("stamp", {
|
|
|
5368
5715
|
}, Effect.catchTag(["ConfigMalformed"], asUserError))).pipe(Command.withDescription("Print my stamp on one pull request, or withdraw it by hand"));
|
|
5369
5716
|
//#endregion
|
|
5370
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
|
+
};
|
|
5371
5725
|
/**
|
|
5372
5726
|
* Every tracked PR under the bucket it sits in, in the order I act on them.
|
|
5373
5727
|
*
|
|
@@ -5375,9 +5729,12 @@ const stampCommand = Command.make("stamp", {
|
|
|
5375
5729
|
* the whole table rather than restarting under each heading, and they are ruled
|
|
5376
5730
|
* apart: three columns of prose run into one another without a rule, and the
|
|
5377
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.
|
|
5378
5735
|
*/
|
|
5379
|
-
const lines = (grouped, stamped, paint) => {
|
|
5380
|
-
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);
|
|
5381
5738
|
let taken = 0;
|
|
5382
5739
|
return grouped.flatMap((it, index) => {
|
|
5383
5740
|
const mine = rows.slice(taken, taken + it.placed.length);
|
|
@@ -5385,7 +5742,8 @@ const lines = (grouped, stamped, paint) => {
|
|
|
5385
5742
|
return [
|
|
5386
5743
|
...index === 0 ? [] : [""],
|
|
5387
5744
|
heading[it.bucket],
|
|
5388
|
-
...
|
|
5745
|
+
...it.placed.flatMap((placed) => movement(placed, sinceOf(placed))),
|
|
5746
|
+
...mine
|
|
5389
5747
|
];
|
|
5390
5748
|
});
|
|
5391
5749
|
};
|
|
@@ -5405,10 +5763,12 @@ const status = Command.make("status", {
|
|
|
5405
5763
|
}
|
|
5406
5764
|
const grouped = group(report.facts);
|
|
5407
5765
|
if (grouped.length === 0) yield* Console.log("No open pull requests.");
|
|
5408
|
-
|
|
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);
|
|
5409
5768
|
yield* printLeftOut(report);
|
|
5410
5769
|
yield* printTroubles(report.troubles);
|
|
5411
|
-
|
|
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"));
|
|
5412
5772
|
//#endregion
|
|
5413
5773
|
//#region src/cli/uninstall.ts
|
|
5414
5774
|
const configFlag = Flag.Boolean("config").pipe(Flag.withDefault(false), Flag.withDescription("Take the configuration file too, and not only the state"));
|
|
@@ -5491,7 +5851,7 @@ const uninstall = Command.make("uninstall", {
|
|
|
5491
5851
|
* Running from source leaves the constant undeclared rather than undefined, so
|
|
5492
5852
|
* the check has to be `typeof` and the fallback is what a test reads.
|
|
5493
5853
|
*/
|
|
5494
|
-
const version = "0.
|
|
5854
|
+
const version = "0.7.0";
|
|
5495
5855
|
/** Where the project lives, printed beside the version in the header. */
|
|
5496
5856
|
const projectUrl = "github.com/dominikwozniak/dw-mc";
|
|
5497
5857
|
const subcommands = [
|
|
@@ -5504,6 +5864,7 @@ const subcommands = [
|
|
|
5504
5864
|
rerun,
|
|
5505
5865
|
resolve,
|
|
5506
5866
|
merge,
|
|
5867
|
+
forgetCommand,
|
|
5507
5868
|
sweepCommand,
|
|
5508
5869
|
status,
|
|
5509
5870
|
stampCommand,
|