mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Show recent commit history in the explorer (#2312)
* feat(app): show recent commit history in explorer * fix(app): keep recent commit history accurate * fix(app): include merged commits in recent history * perf(app): bound recent commit classification * fix(app): refresh recent commits on Git updates
This commit is contained in:
@@ -2,7 +2,11 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CheckoutStatusUpdate } from "@getpaseo/protocol/messages";
|
||||
import { checkoutPrStatusQueryKey, checkoutStatusQueryKey } from "@/git/query-keys";
|
||||
import {
|
||||
checkoutCommitsQueryKey,
|
||||
checkoutPrStatusQueryKey,
|
||||
checkoutStatusQueryKey,
|
||||
} from "@/git/query-keys";
|
||||
import {
|
||||
prPanePipelineQueryKey,
|
||||
prPaneTimelineQueryKey,
|
||||
@@ -132,6 +136,25 @@ describe("applyCheckoutStatusUpdateFromEvent", () => {
|
||||
expect(queryClient.getQueryData(checkoutStatusQueryKey(serverId, cwd))).toEqual(pushed);
|
||||
});
|
||||
|
||||
it("invalidates recent commits when checkout status is pushed", () => {
|
||||
const queryClient = createQueryClient();
|
||||
queryClient.setQueryData(checkoutCommitsQueryKey(serverId, cwd), { commits: [] });
|
||||
queryClient.setQueryData(checkoutCommitsQueryKey(serverId, "/repo2"), { commits: [] });
|
||||
|
||||
applyCheckoutStatusUpdateFromEvent({
|
||||
queryClient,
|
||||
serverId,
|
||||
message: checkoutStatusUpdate(checkoutStatus()),
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryState(checkoutCommitsQueryKey(serverId, cwd))?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
queryClient.getQueryState(checkoutCommitsQueryKey(serverId, "/repo2"))?.isInvalidated,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("writes the PR status cache when prStatus is present, and skips it otherwise", () => {
|
||||
const queryClient = createQueryClient();
|
||||
const pushedPr = prStatus({ requestId: "pr-1" });
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import type { CheckoutStatusResponse, CheckoutStatusUpdate } from "@getpaseo/protocol/messages";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import {
|
||||
checkoutCommitsQueryKey,
|
||||
checkoutPrStatusQueryKey,
|
||||
checkoutStatusQueryKey,
|
||||
invalidatePrPaneTimelineForCheckout,
|
||||
@@ -49,6 +50,9 @@ export function applyCheckoutStatusUpdateFromEvent({
|
||||
: undefined;
|
||||
const cachePayload = prStatus ? { ...payload, prStatus } : payload;
|
||||
queryClient.setQueryData(checkoutStatusQueryKey(serverId, payload.cwd), cachePayload);
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: checkoutCommitsQueryKey(serverId, payload.cwd),
|
||||
});
|
||||
expireStaleDiffModeOverrides({
|
||||
serverId,
|
||||
cwd: payload.cwd,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { QueryClient } from "@tanstack/react-query";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
checkoutDiffQueryKey,
|
||||
checkoutCommitsQueryKey,
|
||||
checkoutPrStatusQueryKey,
|
||||
checkoutStatusQueryKey,
|
||||
invalidateCheckoutGitQueriesForClient,
|
||||
@@ -24,6 +25,8 @@ describe("checkout query keys", () => {
|
||||
files: [],
|
||||
});
|
||||
queryClient.setQueryData(checkoutPrStatusQueryKey(serverId, cwd), { status: { number: 12 } });
|
||||
queryClient.setQueryData(checkoutCommitsQueryKey(serverId, cwd), { commits: [] });
|
||||
queryClient.setQueryData(checkoutCommitsQueryKey(serverId, "/tmp/other"), { commits: [] });
|
||||
queryClient.setQueryData(prPaneTimelineQueryKey({ serverId, cwd, prNumber: 12 }), {
|
||||
items: [],
|
||||
});
|
||||
@@ -62,6 +65,12 @@ describe("checkout query keys", () => {
|
||||
expect(queryClient.getQueryState(checkoutPrStatusQueryKey(serverId, cwd))?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(queryClient.getQueryState(checkoutCommitsQueryKey(serverId, cwd))?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
queryClient.getQueryState(checkoutCommitsQueryKey(serverId, "/tmp/other"))?.isInvalidated,
|
||||
).toBe(false);
|
||||
expect(
|
||||
queryClient.getQueryState(prPaneTimelineQueryKey({ serverId, cwd, prNumber: 12 }))
|
||||
?.isInvalidated,
|
||||
@@ -102,6 +111,8 @@ describe("checkout query keys", () => {
|
||||
queryClient.setQueryData(checkoutStatusQueryKey(serverId, cwd), { isGit: true });
|
||||
queryClient.setQueryData(checkoutStatusQueryKey(serverId, otherCwd), { isGit: true });
|
||||
queryClient.setQueryData(checkoutPrStatusQueryKey(serverId, cwd), { status: { number: 12 } });
|
||||
queryClient.setQueryData(checkoutCommitsQueryKey(serverId, cwd), { commits: [] });
|
||||
queryClient.setQueryData(checkoutCommitsQueryKey(otherServerId, cwd), { commits: [] });
|
||||
queryClient.setQueryData(prPaneTimelineQueryKey({ serverId, cwd, prNumber: 12 }), {
|
||||
items: [],
|
||||
});
|
||||
@@ -128,6 +139,12 @@ describe("checkout query keys", () => {
|
||||
expect(queryClient.getQueryState(checkoutPrStatusQueryKey(serverId, cwd))?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(queryClient.getQueryState(checkoutCommitsQueryKey(serverId, cwd))?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
queryClient.getQueryState(checkoutCommitsQueryKey(otherServerId, cwd))?.isInvalidated,
|
||||
).toBe(false);
|
||||
expect(
|
||||
queryClient.getQueryState(prPaneTimelineQueryKey({ serverId, cwd, prNumber: 12 }))
|
||||
?.isInvalidated,
|
||||
|
||||
@@ -62,6 +62,9 @@ export async function invalidateCheckoutGitQueriesForClient(
|
||||
queryClient.invalidateQueries({
|
||||
predicate: checkoutQueryPredicate("checkoutPrStatus", identity),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: checkoutCommitsQueryKey(identity.serverId, identity.cwd),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
predicate: checkoutQueryPredicate(prPaneTimelineQueryKind, identity),
|
||||
}),
|
||||
@@ -81,6 +84,7 @@ export async function invalidateCheckoutGitQueriesForServer(
|
||||
const kinds = [
|
||||
"checkoutStatus",
|
||||
"checkoutPrStatus",
|
||||
"checkoutCommits",
|
||||
prPaneTimelineQueryKind,
|
||||
prPanePipelineQueryKind,
|
||||
];
|
||||
|
||||
@@ -788,12 +788,12 @@ export const ar: TranslationResources = {
|
||||
deletedFile: "تم الحذف",
|
||||
commits: {
|
||||
title: "الإيداعات",
|
||||
countLabel: "{{count}} إيداعات قبل الأساس",
|
||||
countLabel: "{{count}} من الإيداعات الأخيرة",
|
||||
fileDiffEmpty: "لا توجد تغييرات لعرضها",
|
||||
fileDiffError: "تعذّر تحميل فروق الملف",
|
||||
loading: "جارٍ تحميل الإيداعات…",
|
||||
loadError: "تعذّر تحميل الإيداعات",
|
||||
empty: "لا توجد إيداعات قبل الأساس",
|
||||
empty: "لا توجد إيداعات بعد",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -798,12 +798,12 @@ export const en = {
|
||||
deletedFile: "Deleted",
|
||||
commits: {
|
||||
title: "Commits",
|
||||
countLabel: "{{count}} commits ahead of base",
|
||||
countLabel: "{{count}} recent commits",
|
||||
fileDiffEmpty: "No changes to display",
|
||||
fileDiffError: "Failed to load file diff",
|
||||
loading: "Loading commits…",
|
||||
loadError: "Failed to load commits",
|
||||
empty: "No commits ahead of base",
|
||||
empty: "No commits yet",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -819,12 +819,12 @@ export const es: TranslationResources = {
|
||||
deletedFile: "Eliminado",
|
||||
commits: {
|
||||
title: "Commits",
|
||||
countLabel: "{{count}} commits por delante de la base",
|
||||
countLabel: "{{count}} commits recientes",
|
||||
fileDiffEmpty: "No hay cambios para mostrar",
|
||||
fileDiffError: "Error al cargar el diff del archivo",
|
||||
loading: "Cargando commits…",
|
||||
loadError: "Error al cargar los commits",
|
||||
empty: "No hay commits por delante de la base",
|
||||
empty: "Aún no hay commits",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -818,12 +818,12 @@ export const fr: TranslationResources = {
|
||||
deletedFile: "Supprimé",
|
||||
commits: {
|
||||
title: "Commits",
|
||||
countLabel: "{{count}} commits en avance sur la base",
|
||||
countLabel: "{{count}} commits récents",
|
||||
fileDiffEmpty: "Aucune modification à afficher",
|
||||
fileDiffError: "Échec du chargement du diff du fichier",
|
||||
loading: "Chargement des commits…",
|
||||
loadError: "Échec du chargement des commits",
|
||||
empty: "Aucun commit en avance sur la base",
|
||||
empty: "Aucun commit pour le moment",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -799,12 +799,12 @@ export const ja: TranslationResources = {
|
||||
deletedFile: "削除済み",
|
||||
commits: {
|
||||
title: "コミット",
|
||||
countLabel: "ベースより先のコミット数: {{count}}",
|
||||
countLabel: "最近のコミット数: {{count}}",
|
||||
fileDiffEmpty: "表示する変更はありません",
|
||||
fileDiffError: "ファイル差分の読み込みに失敗しました",
|
||||
loading: "コミットを読み込み中…",
|
||||
loadError: "コミットの読み込みに失敗しました",
|
||||
empty: "ベースより先のコミットはありません",
|
||||
empty: "コミットはまだありません",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -810,12 +810,12 @@ export const ptBR: TranslationResources = {
|
||||
deletedFile: "Excluído",
|
||||
commits: {
|
||||
title: "Commits",
|
||||
countLabel: "{{count}} commits à frente da base",
|
||||
countLabel: "{{count}} commits recentes",
|
||||
fileDiffEmpty: "Nenhuma alteração para exibir",
|
||||
fileDiffError: "Falha ao carregar diff do arquivo",
|
||||
loading: "Carregando commits…",
|
||||
loadError: "Falha ao carregar commits",
|
||||
empty: "Nenhum commit à frente da base",
|
||||
empty: "Ainda não há commits",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -810,12 +810,12 @@ export const ru: TranslationResources = {
|
||||
deletedFile: "Удалено",
|
||||
commits: {
|
||||
title: "Коммиты",
|
||||
countLabel: "{{count}} коммитов впереди базы",
|
||||
countLabel: "{{count}} последних коммитов",
|
||||
fileDiffEmpty: "Нет изменений для отображения",
|
||||
fileDiffError: "Не удалось загрузить различия файла",
|
||||
loading: "Загрузка коммитов…",
|
||||
loadError: "Не удалось загрузить коммиты",
|
||||
empty: "Нет коммитов впереди базы",
|
||||
empty: "Коммитов пока нет",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -780,12 +780,12 @@ export const zhCN: TranslationResources = {
|
||||
deletedFile: "已删除",
|
||||
commits: {
|
||||
title: "提交",
|
||||
countLabel: "领先基线 {{count}} 个提交",
|
||||
countLabel: "最近 {{count}} 个提交",
|
||||
fileDiffEmpty: "没有可显示的更改",
|
||||
fileDiffError: "加载文件差异失败",
|
||||
loading: "正在加载提交…",
|
||||
loadError: "加载提交失败",
|
||||
empty: "没有领先基线的提交",
|
||||
empty: "暂无提交",
|
||||
},
|
||||
},
|
||||
openInEditor: {
|
||||
|
||||
@@ -88,4 +88,22 @@ describe("getCommitFileDiff", () => {
|
||||
|
||||
expect(file).toBeNull();
|
||||
});
|
||||
|
||||
it("returns a merge commit diff against its first parent", async () => {
|
||||
const repoDir = initRepo();
|
||||
commitFile(repoDir, "README.md", "base\n", "initial");
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "feature.txt", "feature\n", "add feature");
|
||||
git(["checkout", "main"], repoDir);
|
||||
commitFile(repoDir, "main.txt", "main\n", "advance main");
|
||||
git(["merge", "--no-ff", "feature", "-m", "merge feature"], repoDir);
|
||||
const sha = headSha(repoDir);
|
||||
|
||||
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: "feature.txt" });
|
||||
|
||||
expect(file?.path).toBe("feature.txt");
|
||||
expect(file?.isNew).toBe(true);
|
||||
expect(file?.additions).toBe(1);
|
||||
expect(file?.deletions).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ function addBareRemote(repoDir: string, tempDir: string): string {
|
||||
}
|
||||
|
||||
describe("listCheckoutCommits", () => {
|
||||
it("lists commits ahead of base newest-first with on-remote flags and file stats", async () => {
|
||||
it("lists recent commits newest-first with on-remote flags and file stats", async () => {
|
||||
const { repoDir, tempDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "foo.txt", "a\nb\nc\n", "Add foo");
|
||||
@@ -65,12 +65,14 @@ describe("listCheckoutCommits", () => {
|
||||
const { baseRef, commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(baseRef).toBe("main");
|
||||
expect(commits).toHaveLength(2);
|
||||
expect(commits).toHaveLength(3);
|
||||
expect(commits[0]?.subject).toBe("Add bar");
|
||||
expect(commits[1]?.subject).toBe("Add foo");
|
||||
expect(commits[2]?.subject).toBe("initial");
|
||||
|
||||
expect(commits[0]?.isOnRemote).toBe(false);
|
||||
expect(commits[1]?.isOnRemote).toBe(true);
|
||||
expect(commits[2]?.isOnRemote).toBe(true);
|
||||
|
||||
expect(commits[0]?.files).toEqual([
|
||||
{ path: "bar.txt", additions: 1, deletions: 0, status: "added" },
|
||||
@@ -85,11 +87,11 @@ describe("listCheckoutCommits", () => {
|
||||
expect(Number.isNaN(new Date(commits[0]?.authorDate ?? "").getTime())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns [] when there are no commits ahead of base", async () => {
|
||||
it("shows recent history on the base branch", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
const { baseRef, commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
expect(baseRef).toBeNull();
|
||||
expect(commits).toEqual([]);
|
||||
expect(commits.map((entry) => entry.subject)).toEqual(["initial"]);
|
||||
});
|
||||
|
||||
it("marks all commits local-only when there is no remote", async () => {
|
||||
@@ -101,10 +103,61 @@ describe("listCheckoutCommits", () => {
|
||||
const { baseRef, commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(baseRef).toBe("main");
|
||||
expect(commits).toHaveLength(2);
|
||||
expect(commits).toHaveLength(3);
|
||||
expect(commits.every((c) => c.isOnRemote === false)).toBe(true);
|
||||
});
|
||||
|
||||
it("recognizes base history on a remote before the feature branch is pushed", async () => {
|
||||
const { repoDir, tempDir } = initRepoOnMain();
|
||||
addBareRemote(repoDir, tempDir);
|
||||
git(["push", "-u", "origin", "main"], repoDir);
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "feature.txt", "local\n", "Local feature");
|
||||
|
||||
const { commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(commits.map(({ subject, isOnRemote }) => ({ subject, isOnRemote }))).toEqual([
|
||||
{ subject: "Local feature", isOnRemote: false },
|
||||
{ subject: "initial", isOnRemote: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("limits history and unpushed classification to the 20 most recent commits", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
for (let index = 1; index <= 24; index += 1) {
|
||||
commitFile(repoDir, "history.txt", `${index}\n`, `Commit ${index}`);
|
||||
}
|
||||
|
||||
const { commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(commits).toHaveLength(20);
|
||||
expect(commits.every((entry) => entry.isOnRemote === false)).toBe(true);
|
||||
expect(commits.map((entry) => entry.subject)).toEqual(
|
||||
Array.from({ length: 20 }, (_, index) => `Commit ${24 - index}`),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows merged branch commits and compares the merge against its first parent", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
commitFile(repoDir, "feature.txt", "feature\n", "Add feature");
|
||||
git(["checkout", "main"], repoDir);
|
||||
commitFile(repoDir, "main.txt", "main\n", "Advance main");
|
||||
git(["merge", "--no-ff", "feature", "-m", "Merge feature"], repoDir);
|
||||
|
||||
const { commits } = await listCheckoutCommits({ cwd: repoDir });
|
||||
|
||||
expect(commits.map((entry) => entry.subject)).toEqual([
|
||||
"Merge feature",
|
||||
"Advance main",
|
||||
"Add feature",
|
||||
"initial",
|
||||
]);
|
||||
expect(commits[0]?.files).toEqual([
|
||||
{ path: "feature.txt", additions: 1, deletions: 0, status: "added" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("classifies renamed files with status renamed and correct destination path", async () => {
|
||||
const { repoDir } = initRepoOnMain();
|
||||
git(["checkout", "-b", "feature"], repoDir);
|
||||
|
||||
@@ -2001,9 +2001,8 @@ export async function getCheckoutStatus(
|
||||
};
|
||||
}
|
||||
|
||||
// Cap on how many ahead-of-base commits we enumerate. Branches with more than
|
||||
// this many unmerged commits are truncated to the newest MAX_CHECKOUT_COMMITS.
|
||||
const MAX_CHECKOUT_COMMITS = 200;
|
||||
// The explorer is a recent-history view, not a full repository log.
|
||||
const MAX_CHECKOUT_COMMITS = 20;
|
||||
// Bytes git emits between fields/records. We split parsed output on these.
|
||||
const COMMIT_FIELD_SEPARATOR = "\x00";
|
||||
const COMMIT_RECORD_SEPARATOR = "\x1e";
|
||||
@@ -2145,41 +2144,16 @@ function parseCheckoutCommitRecords(stdout: string): ParsedCheckoutCommit[] {
|
||||
return commits;
|
||||
}
|
||||
|
||||
async function resolveCheckoutCommitUpstreamRef(
|
||||
cwd: string,
|
||||
currentBranch: string,
|
||||
context?: CheckoutContext,
|
||||
): Promise<string | null> {
|
||||
// Prefer the branch's configured `@{u}`. If it's configured but the
|
||||
// remote-tracking ref isn't present locally (e.g. configured upstream that was
|
||||
// never fetched), fall back to `origin/<branch>` when that ref does exist, so a
|
||||
// standard `origin` push is still recognized. Both missing => no remote.
|
||||
const configured = await getConfiguredUpstreamRef(cwd, currentBranch, context);
|
||||
if (configured && (await doesGitRefExist(cwd, `refs/remotes/${configured}`, context))) {
|
||||
return configured;
|
||||
}
|
||||
if (await doesGitRefExist(cwd, `refs/remotes/origin/${currentBranch}`, context)) {
|
||||
return `origin/${currentBranch}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Returns the set of SHAs on the current branch that are NOT on the upstream
|
||||
// (local-only/unpushed), or `null` when no upstream exists (no remote at all).
|
||||
async function getLocalOnlyCommitShas(
|
||||
cwd: string,
|
||||
currentBranch: string,
|
||||
context?: CheckoutContext,
|
||||
): Promise<Set<string> | null> {
|
||||
const upstreamRef = await resolveCheckoutCommitUpstreamRef(cwd, currentBranch, context);
|
||||
if (!upstreamRef) {
|
||||
return null;
|
||||
}
|
||||
const { stdout } = await runGitCommand(["rev-list", `${upstreamRef}..HEAD`], {
|
||||
cwd,
|
||||
envOverlay: READ_ONLY_GIT_ENV,
|
||||
logger: context?.logger,
|
||||
});
|
||||
// Returns commits reachable from HEAD that are not reachable from any remote ref.
|
||||
async function getUnpushedCommitShas(cwd: string, context?: CheckoutContext): Promise<Set<string>> {
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", `--max-count=${MAX_CHECKOUT_COMMITS}`, "HEAD", "--not", "--remotes"],
|
||||
{
|
||||
cwd,
|
||||
envOverlay: READ_ONLY_GIT_ENV,
|
||||
logger: context?.logger,
|
||||
},
|
||||
);
|
||||
return new Set(
|
||||
stdout
|
||||
.split("\n")
|
||||
@@ -2189,13 +2163,8 @@ async function getLocalOnlyCommitShas(
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the current branch's commits that are ahead of its base branch, newest
|
||||
* first, each flagged local-only vs on-remote with per-commit file +/- stats.
|
||||
*
|
||||
* Base ref is resolved exactly like {@link getCheckoutStatus}: the stored/default
|
||||
* base branch, mapped to the best comparison ref (origin/<base> when present,
|
||||
* else local <base>). Returns `[]` when the base cannot be resolved, the current
|
||||
* ref is the base itself, or there are no commits ahead.
|
||||
* Lists the current branch's 20 most recent commits, newest first, each flagged
|
||||
* local-only vs on-remote with per-commit file +/- stats.
|
||||
*/
|
||||
export interface CheckoutCommitsResult {
|
||||
baseRef: string | null;
|
||||
@@ -2213,21 +2182,14 @@ export async function listCheckoutCommits({
|
||||
}
|
||||
|
||||
const { resolvedBaseRef } = await resolveBaseRefForCwd(cwd);
|
||||
if (!resolvedBaseRef) {
|
||||
return { baseRef: null, commits: [] };
|
||||
}
|
||||
|
||||
const normalizedBaseRef = normalizeLocalBranchRefName(resolvedBaseRef);
|
||||
if (!normalizedBaseRef || normalizedBaseRef === currentBranch) {
|
||||
return { baseRef: null, commits: [] };
|
||||
}
|
||||
|
||||
let comparisonBaseRef: string;
|
||||
try {
|
||||
comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, resolvedBaseRef);
|
||||
} catch {
|
||||
// Base branch is not present locally or on origin — nothing to compare against.
|
||||
return { baseRef: null, commits: [] };
|
||||
const normalizedBaseRef = resolvedBaseRef ? normalizeLocalBranchRefName(resolvedBaseRef) : null;
|
||||
let comparisonBaseRef: string | null = null;
|
||||
if (resolvedBaseRef && normalizedBaseRef && normalizedBaseRef !== currentBranch) {
|
||||
try {
|
||||
comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, resolvedBaseRef);
|
||||
} catch {
|
||||
// History does not depend on the configured base being available.
|
||||
}
|
||||
}
|
||||
|
||||
// Single pass: `--raw` carries the status letter, `--numstat` the +/- counts.
|
||||
@@ -2235,8 +2197,8 @@ export async function listCheckoutCommits({
|
||||
const logResult = await runGitCommand(
|
||||
[
|
||||
"log",
|
||||
`${comparisonBaseRef}..HEAD`,
|
||||
"--no-merges",
|
||||
"HEAD",
|
||||
"--diff-merges=first-parent",
|
||||
`--max-count=${MAX_CHECKOUT_COMMITS}`,
|
||||
`--format=${COMMIT_LOG_FORMAT}`,
|
||||
"--raw",
|
||||
@@ -2251,7 +2213,7 @@ export async function listCheckoutCommits({
|
||||
return { baseRef: comparisonBaseRef, commits: [] };
|
||||
}
|
||||
|
||||
const localOnlyShas = await getLocalOnlyCommitShas(cwd, currentBranch);
|
||||
const unpushedShas = await getUnpushedCommitShas(cwd);
|
||||
|
||||
const commits = records.map((record) => ({
|
||||
sha: record.sha,
|
||||
@@ -2259,7 +2221,7 @@ export async function listCheckoutCommits({
|
||||
subject: record.subject,
|
||||
authorName: record.authorName,
|
||||
authorDate: record.authorDate,
|
||||
isOnRemote: localOnlyShas === null ? false : !localOnlyShas.has(record.sha),
|
||||
isOnRemote: !unpushedShas.has(record.sha),
|
||||
files: record.files,
|
||||
}));
|
||||
|
||||
@@ -2271,13 +2233,12 @@ export async function listCheckoutCommits({
|
||||
* parses it into the same {@link ParsedDiffFile} shape the diff subscription
|
||||
* emits (so the client can reuse its existing renderer).
|
||||
*
|
||||
* Runs `git show <sha> --format= -- <path>` with the sha and path passed as
|
||||
* separate process args (never interpolated into a shell string), and `--format=`
|
||||
* suppresses the commit header so the output is a pure unified diff. The text is
|
||||
* parsed and highlighted by {@link parseAndHighlightDiff} — the exact parser the
|
||||
* diff subscription uses. Returns `null` when the file is absent from the commit
|
||||
* or the change is binary-only (no textual hunks). Throws on git failure (e.g. an
|
||||
* unknown sha), which the caller maps to a typed checkout error.
|
||||
* Compares merge commits to their first parent, matching the linear history shown
|
||||
* in the explorer. The text is parsed and highlighted by
|
||||
* {@link parseAndHighlightDiff} — the exact parser the diff subscription uses.
|
||||
* Returns `null` when the file is absent from the commit or the change is
|
||||
* binary-only (no textual hunks). Throws on git failure (e.g. an unknown sha),
|
||||
* which the caller maps to a typed checkout error.
|
||||
*/
|
||||
export async function getCommitFileDiff({
|
||||
cwd,
|
||||
@@ -2288,10 +2249,13 @@ export async function getCommitFileDiff({
|
||||
sha: string;
|
||||
path: string;
|
||||
}): Promise<ParsedDiffFile | null> {
|
||||
const { stdout } = await runGitCommand(["show", sha, "--format=", "--", path], {
|
||||
cwd,
|
||||
envOverlay: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
const { stdout } = await runGitCommand(
|
||||
["show", sha, "--format=", "--diff-merges=first-parent", "--", path],
|
||||
{
|
||||
cwd,
|
||||
envOverlay: READ_ONLY_GIT_ENV,
|
||||
},
|
||||
);
|
||||
|
||||
if (stdout.trim().length === 0) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user