| 1 | import * as Schema from "effect/Schema"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas.ts"; |
| 3 | import { ReviewDiffPreviewSource } from "./review.ts"; |
| 4 | |
| 5 | export const JjStackInput = Schema.Struct({ cwd: TrimmedNonEmptyString }); |
| 6 | export type JjStackInput = typeof JjStackInput.Type; |
| 7 | |
| 8 | export const JjTrailer = Schema.Struct({ key: Schema.String, value: Schema.String }); |
| 9 | export type JjTrailer = typeof JjTrailer.Type; |
| 10 | |
| 11 | export const JjBookmark = Schema.Struct({ |
| 12 | name: TrimmedNonEmptyString, |
| 13 | /** True when a tracked remote bookmark points at this same commit. */ |
| 14 | pushed: Schema.Boolean, |
| 15 | }); |
| 16 | export type JjBookmark = typeof JjBookmark.Type; |
| 17 | |
| 18 | export 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 | }); |
| 35 | export type JjChange = typeof JjChange.Type; |
| 36 | |
| 37 | export 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 | }); |
| 52 | export type JjStack = typeof JjStack.Type; |
| 53 | |
| 54 | export const JjDiffInput = Schema.Struct({ |
| 55 | cwd: TrimmedNonEmptyString, |
| 56 | fromCommitId: TrimmedNonEmptyString, |
| 57 | toCommitId: TrimmedNonEmptyString, |
| 58 | ignoreWhitespace: Schema.optionalKey(Schema.Boolean), |
| 59 | }); |
| 60 | export type JjDiffInput = typeof JjDiffInput.Type; |
| 61 | |
| 62 | export const JjDiffResult = ReviewDiffPreviewSource; |
| 63 | export type JjDiffResult = typeof JjDiffResult.Type; |
| 64 | |
| 65 | export 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 | }); |
| 72 | export type JjDescribeInput = typeof JjDescribeInput.Type; |
| 73 | |
| 74 | export const JjChangeInput = Schema.Struct({ |
| 75 | cwd: TrimmedNonEmptyString, |
| 76 | changeId: TrimmedNonEmptyString, |
| 77 | }); |
| 78 | export type JjChangeInput = typeof JjChangeInput.Type; |
| 79 | |
| 80 | export 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 | }); |
| 89 | export type JjPushInput = typeof JjPushInput.Type; |
| 90 | |
| 91 | export const JjPushResult = Schema.Struct({ |
| 92 | bookmark: TrimmedNonEmptyString, |
| 93 | remote: Schema.String, |
| 94 | }); |
| 95 | export type JjPushResult = typeof JjPushResult.Type; |
| 96 | |
| 97 | export 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. */ |
| 107 | export 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. */ |
| 117 | export 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. */ |
| 125 | export 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 | } |