import * as vscode from "vscode"; import type { ChangeStatus, RoomControl } from "../lib/editor-protocol"; import type { EditorSessionInfo, ForgeApi } from "./forge"; import type { GitforFileSystem } from "./fs"; const LABELS: Record = { A: "Added", M: "Modified", D: "Deleted" }; type CommitResult = { sha: string; branch: string; pullUrl: string | null; pullError: string | null; files: number }; /** * Source Control view over the room's pending changes (shared by everyone in the session). Commit * snapshots the room to the branch through the forge, which enforces protection and opens a PR when needed. */ export class GitforScm implements vscode.Disposable { private readonly scm: vscode.SourceControl; private readonly changes: vscode.SourceControlResourceGroup; private readonly conflicts: vscode.SourceControlResourceGroup; private readonly disposables: vscode.Disposable[] = []; constructor( private readonly api: ForgeApi, private readonly fs: GitforFileSystem, private session: EditorSessionInfo, ) { this.scm = vscode.scm.createSourceControl("gitfor", `gitfor: ${session.ref}`, fs.uri("").with({ path: fs.root })); this.scm.inputBox.placeholder = session.protected ? `Message (commits to a new branch and opens a pull request into ${session.ref})` : `Message (commit to ${session.ref})`; this.scm.acceptInputCommand = { command: "gitfor.commit", title: "Commit" }; this.conflicts = this.scm.createResourceGroup("conflicts", "Conflicts with upstream"); this.conflicts.hideWhenEmpty = true; this.changes = this.scm.createResourceGroup("changes", "Changes (shared)"); this.disposables.push( this.scm, vscode.workspace.registerTextDocumentContentProvider("gitfor-base", { provideTextDocumentContent: async (uri) => (await this.api.blob(uri.path.replace(/^\//, ""))).content ?? "", }), vscode.commands.registerCommand("gitfor.commit", () => this.commit()), vscode.commands.registerCommand("gitfor.commitToBranch", () => this.commit(true)), vscode.commands.registerCommand("gitfor.openChange", (path: string) => this.openChange(path)), ); } update(session: EditorSessionInfo): void { this.session = session; } onStatus(message: Extract): void { this.changes.resourceStates = message.changes.map((change) => ({ resourceUri: this.fs.uri(change.path), decorations: { tooltip: LABELS[change.status], strikeThrough: change.status === "D", faded: change.status === "D", }, command: { command: "gitfor.openChange", title: "Open changes", arguments: [change.path] }, })); this.conflicts.resourceStates = message.conflicts.map((path) => ({ resourceUri: this.fs.uri(path), decorations: { tooltip: "Changed upstream while edited here. Click to keep your version." }, command: { command: "gitfor.resolve", title: "Keep my version", arguments: [path] }, })); this.scm.count = message.changes.length; } private async openChange(path: string): Promise { const base = vscode.Uri.from({ scheme: "gitfor-base", path: `/${path}` }); await vscode.commands.executeCommand( "vscode.diff", base, this.fs.uri(path), `${path} (${this.session.ref} vs live)`, ); } private async commit(askBranch = false): Promise { const message = this.scm.inputBox.value.trim(); if (!message) { void vscode.window.showWarningMessage("Enter a commit message first."); return; } let newBranch: string | undefined; if (askBranch) { newBranch = await vscode.window.showInputBox({ prompt: "New branch name", placeHolder: `${this.session.user}-patch-1`, }); if (!newBranch) return; } try { const result = await vscode.window.withProgress( { location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => this.api.request("commit", { method: "POST", body: { message, fromRoom: true, newBranch } }), ); this.scm.inputBox.value = ""; this.fs.refresh(); const summary = `Committed ${result.files} file(s) as ${result.sha.slice(0, 7)} to ${result.branch}.`; if (result.pullUrl) { const choice = await vscode.window.showInformationMessage( `${summary} Opened a pull request.`, "Open pull request", ); if (choice) await vscode.env.openExternal(vscode.Uri.parse(result.pullUrl)); } else { void vscode.window.showInformationMessage( result.pullError ? `${summary} No pull request: ${result.pullError}` : summary, ); } } catch (error) { void vscode.window.showErrorMessage(error instanceof Error ? error.message : "Commit failed"); } } dispose(): void { for (const disposable of this.disposables) disposable.dispose(); } }