1import * as Schema from "effect/Schema";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
3import { ReviewDiffPreviewSource } from "./review.ts";
4
5export const JjStackInput = Schema.Struct({ cwd: TrimmedNonEmptyString });
6export type JjStackInput = typeof JjStackInput.Type;
7
8export const JjTrailer = Schema.Struct({ key: Schema.String, value: Schema.String });
9export type JjTrailer = typeof JjTrailer.Type;
10
11export const JjBookmark = Schema.Struct({
12 name: TrimmedNonEmptyString,
13 /** True when a tracked remote bookmark points at this same commit. */
14 pushed: Schema.Boolean,
15});
16export type JjBookmark = typeof JjBookmark.Type;
17
18export const JjChange = Schema.Struct({
19 changeId: TrimmedNonEmptyString,
20 /** Shortest unique change-id prefix, at least four characters. */
21 shortId: TrimmedNonEmptyString,
22 commitId: TrimmedNonEmptyString,
23 parentCommitIds: Schema.Array(TrimmedNonEmptyString),
24 description: Schema.String,
25 trailers: Schema.Array(JjTrailer),
26 bookmarks: Schema.Array(JjBookmark),
27 immutable: Schema.Boolean,
28 conflict: Schema.Boolean,
29 empty: Schema.Boolean,
30 workingCopy: Schema.Boolean,
31 additions: Schema.Number,
32 deletions: Schema.Number,
33 authoredAt: Schema.String,
34});
35export type JjChange = typeof JjChange.Type;
36
37export const JjStack = Schema.Struct({
38 workspaceRoot: TrimmedNonEmptyString,
39 /** `owner/name` of the GitHub push remote, when there is one. */
40 repository: Schema.NullOr(Schema.String),
41 trunk: Schema.NullOr(
42 Schema.Struct({
43 name: Schema.String,
44 commitId: TrimmedNonEmptyString,
45 /** Trunk commits the stack's ancestry lacks. */
46 behind: Schema.Number,
47 }),
48 ),
49 /** Newest first. Empty changes are omitted, except the working copy. */
50 changes: Schema.Array(JjChange),
51});
52export type JjStack = typeof JjStack.Type;
53
54export const JjDiffInput = Schema.Struct({
55 cwd: TrimmedNonEmptyString,
56 fromCommitId: TrimmedNonEmptyString,
57 toCommitId: TrimmedNonEmptyString,
58 ignoreWhitespace: Schema.optionalKey(Schema.Boolean),
59});
60export type JjDiffInput = typeof JjDiffInput.Type;
61
62export const JjDiffResult = ReviewDiffPreviewSource;
63export type JjDiffResult = typeof JjDiffResult.Type;
64
65export const JjDescribeInput = Schema.Struct({
66 cwd: TrimmedNonEmptyString,
67 changeId: TrimmedNonEmptyString,
68 /** The commit the editor loaded; a concurrent rewrite with a different message is refused. */
69 expectedCommitId: TrimmedNonEmptyString,
70 message: Schema.String,
71});
72export type JjDescribeInput = typeof JjDescribeInput.Type;
73
74export const JjChangeInput = Schema.Struct({
75 cwd: TrimmedNonEmptyString,
76 changeId: TrimmedNonEmptyString,
77});
78export type JjChangeInput = typeof JjChangeInput.Type;
79
80export const JjPushInput = Schema.Struct({
81 cwd: TrimmedNonEmptyString,
82 /** The newest change the pushed bookmark should point at. */
83 changeId: TrimmedNonEmptyString,
84 /** Move and push this bookmark; omitted creates one from the repository's push-bookmark template. */
85 bookmark: Schema.optionalKey(TrimmedNonEmptyString),
86 /** The change whose description names a new bookmark, usually the PR's first change. */
87 nameFrom: Schema.optionalKey(TrimmedNonEmptyString),
88});
89export type JjPushInput = typeof JjPushInput.Type;
90
91export const JjPushResult = Schema.Struct({
92 bookmark: TrimmedNonEmptyString,
93 remote: Schema.String,
94});
95export type JjPushResult = typeof JjPushResult.Type;
96
97export class JjError extends Schema.TaggedError<JjError>()("JjError", {
98 operation: Schema.String,
99 detail: Schema.String,
100}) {
101 override get message(): string {
102 return `jj ${this.operation} failed: ${this.detail}`;
103 }
104}
105
106/** Another writer changed the description after the editor loaded it. */
107export class JjDescriptionConflictError extends Schema.TaggedError<JjDescriptionConflictError>()(
108 "JjDescriptionConflictError",
109 { changeId: Schema.String, description: Schema.String, commitId: Schema.String },
110) {
111 override get message(): string {
112 return "This change's description was edited elsewhere.";
113 }
114}
115
116/** Splits jj's `title\n\nbody` into what the editor shows: title on line 1, body from line 2. */
117export function commitMessageToEditor(description: string): string {
118 const trimmed = description.replace(/\n+$/, "");
119 const newline = trimmed.indexOf("\n");
120 if (newline === -1) return trimmed;
121 return `${trimmed.slice(0, newline)}\n${trimmed.slice(newline + 1).replace(/^\n/, "")}`;
122}
123
124/** Inverse of `commitMessageToEditor`: restores the blank line between title and body. */
125export function editorToCommitMessage(text: string): string {
126 const trimmed = text.replace(/\s+$/, "");
127 const newline = trimmed.indexOf("\n");
128 if (newline === -1) return trimmed;
129 const body = trimmed.slice(newline + 1).replace(/^\n+/, "");
130 return body ? `${trimmed.slice(0, newline)}\n\n${body}` : trimmed.slice(0, newline);
131}