mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
Review fixes for several repositories in one project: - A path equal to a nested repository's root (a submodule's entry, an untracked clone) belongs to the enclosing repository, in the backend and the Commit view. It was routed to the nested repository as ".", so Rollback ran `git restore` and `git clean` over the whole submodule and Commit staged everything in it. - Repository-scoped commands take the repository root exactly and fail when it is gone instead of running in an enclosing repository; path-based ones resolve the file's repository (Project::with_repo_of). - The rescan on a repository appearing or going watches `.git/HEAD`: the watcher never reports a `.git` path itself, so the old check never fired. - Status skips a repository that cannot be read; refresh falls back to the primary repository when the selected one is gone instead of failing every time. - Commit and Compare-with-Local diffs resolve repository-relative paths against the selected repository's root, not the project root. - Update Project and Fetch go on after a failing repository and report the failures together; Update skips repositories with a detached HEAD or no upstream. - The Push dialog leaves out a repository that cannot be read. - Shelve and patches fail clearly for changes of a nested repository instead of leaving them out silently (they still support the main repository only). - Fix the repository discovery test on Windows (path separators); Chinese text for the new strings.
161 lines
5.8 KiB
TypeScript
161 lines
5.8 KiB
TypeScript
/** Diff editor inputs and helpers to open them. */
|
|
import type { ChangedFile } from "@/bindings";
|
|
import { call, commands } from "@/platform/ipc";
|
|
import { editorService, type EditorInput } from "@/workbench/contributions";
|
|
import { baseName, effectiveKind } from "./model";
|
|
import { readWorkingText } from "./ops";
|
|
import { renamedFromPath } from "./changes";
|
|
import { currentRepoRoot, statusOf } from "./store";
|
|
|
|
export type DiffSide =
|
|
/** File content at a git revision (`HEAD`, `:0` = index, a commit id, `<id>^`). */
|
|
| { type: "rev"; rev: string; path?: string; label?: string }
|
|
/** The working file (unsaved editor buffer included). */
|
|
| { type: "worktree"; label?: string }
|
|
/** Another file (Compare With…, Compare Files); editable like the working file. */
|
|
| { type: "file"; path: string; label?: string }
|
|
| { type: "text"; text: string; label: string }
|
|
/** A Local History revision. */
|
|
| { type: "history"; id: string; label: string };
|
|
|
|
/** A note attached below a line of one side (pull request review comments). */
|
|
export interface DiffAnnotation {
|
|
side: "left" | "right";
|
|
/** 0-based line of that side. */
|
|
line: number;
|
|
author: string;
|
|
body: string;
|
|
/** ISO date. */
|
|
date?: string;
|
|
}
|
|
|
|
export interface DiffData {
|
|
/** Absolute path of the file (the right side's path). */
|
|
path: string;
|
|
left: DiffSide;
|
|
right: DiffSide;
|
|
/** Reveal this 0-based line of the right side. */
|
|
line?: number;
|
|
/** Tab title instead of the default "name (left ↔ right)". */
|
|
title?: string;
|
|
/** Notes shown under lines (unified mode shows the right side's only). */
|
|
annotations?: DiffAnnotation[];
|
|
}
|
|
|
|
export const DIFF_KIND = "diff";
|
|
|
|
export function sideKey(s: DiffSide): string {
|
|
switch (s.type) {
|
|
case "rev":
|
|
return `rev:${s.rev}:${s.path ?? ""}`;
|
|
case "worktree":
|
|
return "wt";
|
|
case "file":
|
|
return `file:${s.path}`;
|
|
case "text":
|
|
return `text:${s.label}:${textHash(s.text)}`;
|
|
case "history":
|
|
return `hist:${s.id}`;
|
|
}
|
|
}
|
|
|
|
/** Short content hash so different texts under one label get their own tab. */
|
|
function textHash(text: string): string {
|
|
let h = 0x811c9dc5;
|
|
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 0x01000193);
|
|
return (h >>> 0).toString(36);
|
|
}
|
|
|
|
export function sideLabel(s: DiffSide): string {
|
|
switch (s.type) {
|
|
case "rev":
|
|
if (s.label) return s.label;
|
|
if (s.rev === "HEAD") return "HEAD";
|
|
if (s.rev === ":0" || s.rev === ":") return "Staged (index)";
|
|
return s.rev.length >= 40 ? s.rev.slice(0, 8) : s.rev;
|
|
case "worktree":
|
|
return s.label ?? "Your version";
|
|
case "file":
|
|
return s.label ?? s.path;
|
|
case "text":
|
|
case "history":
|
|
return s.label;
|
|
}
|
|
}
|
|
|
|
export function diffInput(data: DiffData): EditorInput {
|
|
return { kind: DIFF_KIND, key: `diff:${data.path}:${sideKey(data.left)}:${sideKey(data.right)}`, path: data.path, data };
|
|
}
|
|
|
|
export function diffTitle(data: DiffData): string {
|
|
if (data.title) return data.title;
|
|
if (data.left.type === "file") return `${baseName(data.left.path)} ↔ ${baseName(data.path)}`;
|
|
return `${baseName(data.path)} (${sideLabel(data.left)} ↔ ${sideLabel(data.right)})`;
|
|
}
|
|
|
|
export async function openDiff(data: DiffData, opts: { preview?: boolean } = {}) {
|
|
await editorService().openInput(diffInput(data), { preview: opts.preview, focus: true });
|
|
}
|
|
|
|
/** Default local-changes diff for a working file: HEAD (or empty) vs the working copy. */
|
|
export function workingDiffData(path: string, line?: number): DiffData {
|
|
const st = statusOf(path);
|
|
const kind = st ? effectiveKind(st) : null;
|
|
const left: DiffSide =
|
|
kind === "untracked" || kind === "added"
|
|
? { type: "text", text: "", label: "(does not exist)" }
|
|
: st?.renamedFrom
|
|
? { type: "rev", rev: "HEAD", path: renamedFromPath(st) ?? undefined, label: `HEAD (${st.renamedFrom})` }
|
|
: { type: "rev", rev: "HEAD" };
|
|
const right: DiffSide =
|
|
kind === "deleted" ? { type: "text", text: "", label: "(deleted)" } : { type: "worktree" };
|
|
return { path, left, right, line };
|
|
}
|
|
|
|
/** A path of the selected repository's log (repository-relative) made absolute. */
|
|
function joinRoot(rel: string): string {
|
|
const root = currentRepoRoot();
|
|
return root ? `${root}/${rel}` : rel;
|
|
}
|
|
|
|
/** Diff of one file changed by `commit` (first parent ↔ commit). */
|
|
export function commitFileDiffData(commit: { id: string; shortId: string; parents: string[] }, file: ChangedFile): DiffData {
|
|
const path = joinRoot(file.path);
|
|
const parent = commit.parents[0];
|
|
const left: DiffSide =
|
|
!parent || file.kind === "added"
|
|
? { type: "text", text: "", label: "(does not exist)" }
|
|
: {
|
|
type: "rev",
|
|
rev: parent,
|
|
path: file.oldPath ? joinRoot(file.oldPath) : undefined,
|
|
label: parent.slice(0, 8),
|
|
};
|
|
const right: DiffSide =
|
|
file.kind === "deleted"
|
|
? { type: "text", text: "", label: "(deleted)" }
|
|
: { type: "rev", rev: commit.id, label: commit.shortId };
|
|
return { path, left, right };
|
|
}
|
|
|
|
export async function loadSide(path: string, side: DiffSide): Promise<string> {
|
|
switch (side.type) {
|
|
case "rev":
|
|
return (await call(commands.vcsFileAt(side.path ?? path, side.rev))) ?? "";
|
|
case "worktree":
|
|
return (await readWorkingText(path)) ?? "";
|
|
case "file":
|
|
return (await readWorkingText(side.path)) ?? "";
|
|
case "text":
|
|
return side.text;
|
|
case "history":
|
|
return call(commands.historyContent(side.id));
|
|
}
|
|
}
|
|
|
|
export const isWorktree = (s: DiffSide) => s.type === "worktree";
|
|
/** Path of a side backed by a file on disk (editable in the diff), else null. */
|
|
export const sideFile = (s: DiffSide, path: string): string | null =>
|
|
s.type === "worktree" ? path : s.type === "file" ? s.path : null;
|
|
export const isIndex = (s: DiffSide) => s.type === "rev" && (s.rev === ":0" || s.rev === ":");
|