mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 00:07:29 +00:00
feat(vcs): fill the commit message with the repository's commit template (#1340)
An empty commit message field now starts with the file `commit.template` names (any git config level; `~/` and repository-relative paths resolve as git resolves them), without its `#` comment lines, since commits keep the message as written. It is filled when the repository opens or changes and when a commit clears the field; a typed message is never replaced, and a prepared merge / cherry-pick / revert message still takes precedence. Fixes #1285
This commit is contained in:
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const vcsLog = vi.fn();
|
||||
const fsReadText = vi.fn();
|
||||
const vcsConfigGet = vi.fn(async (_repo: unknown, _key: string) => ({ status: "ok", data: null as string | null }));
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
@@ -10,7 +11,7 @@ vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
commands: {
|
||||
...real.commands,
|
||||
vcsLog: (repo: unknown, query: unknown) => vcsLog(repo, query),
|
||||
vcsConfigGet: async () => ({ status: "ok", data: null }),
|
||||
vcsConfigGet: (repo: unknown, key: string) => vcsConfigGet(repo, key),
|
||||
fsReadText: (path: string) => fsReadText(path),
|
||||
},
|
||||
};
|
||||
@@ -18,7 +19,13 @@ vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
|
||||
import { gitTabs, shownTab, stashTabOf, stashTabs } from "../gitTabs";
|
||||
import { pushDialogNeeded } from "../commitUi";
|
||||
import { installPreparedMessage, keepMessageAfterCommit, messageAfterCommit, useCommit } from "../commitStore";
|
||||
import {
|
||||
installPreparedMessage,
|
||||
keepMessageAfterCommit,
|
||||
messageAfterCommit,
|
||||
templatePath,
|
||||
useCommit,
|
||||
} from "../commitStore";
|
||||
import { closeLogTab, emptyFilters, openLogTab, selectGitTab, setLogFilters, useGitLog } from "../logStore";
|
||||
import { useVcs } from "../store";
|
||||
|
||||
@@ -95,6 +102,43 @@ describe("commit message after a commit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("commit template", () => {
|
||||
beforeEach(() => {
|
||||
fsReadText.mockReset();
|
||||
vcsConfigGet.mockImplementation(async (_repo, key) => ({
|
||||
status: "ok",
|
||||
data: key === "commit.template" ? ".gitmessage" : null,
|
||||
}));
|
||||
useCommit.setState({ message: "" });
|
||||
});
|
||||
|
||||
it("resolves the configured path as git does", () => {
|
||||
expect(templatePath("~/.gitmessage", "/p", "/home/me/")).toBe("/home/me/.gitmessage");
|
||||
expect(templatePath("/etc/msg", "/p", "")).toBe("/etc/msg");
|
||||
expect(templatePath("C:\\msg.txt", "/p", "")).toBe("C:\\msg.txt");
|
||||
expect(templatePath(".gitmessage", "/p", "")).toBe("/p/.gitmessage");
|
||||
});
|
||||
|
||||
it("fills the empty message field, and again after a commit clears it", async () => {
|
||||
fsReadText.mockResolvedValue({ status: "ok", data: "feat: \n\n# Why:\nRefs: \n" });
|
||||
useVcs.setState({ root: "/p", branch: null });
|
||||
const dispose = installPreparedMessage();
|
||||
await vi.waitFor(() => expect(useCommit.getState().message).toBe("feat: \n\nRefs:"));
|
||||
expect(fsReadText).toHaveBeenCalledWith("/p/.gitmessage");
|
||||
|
||||
keepMessageAfterCommit("feat: x", true);
|
||||
await vi.waitFor(() => expect(useCommit.getState().message).toBe("feat: \n\nRefs:"));
|
||||
// A kept message stays; a typed one is never replaced.
|
||||
keepMessageAfterCommit("feat: x", false);
|
||||
useCommit.setState({ message: "typed" });
|
||||
useVcs.setState({ root: "/q" });
|
||||
useCommit.setState({ message: "typed" });
|
||||
await Promise.resolve();
|
||||
expect(useCommit.getState().message).toBe("typed");
|
||||
dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("log tabs of their own", () => {
|
||||
beforeEach(() => {
|
||||
vcsLog.mockReset().mockResolvedValue({ status: "ok", data: [] });
|
||||
|
||||
@@ -180,6 +180,7 @@ export function keepMessageAfterCommit(committed: string, clear: boolean) {
|
||||
const message = messageAfterCommit(committed, clear);
|
||||
keptMessage = message || null;
|
||||
useCommit.setState({ message });
|
||||
if (!message) void loadTemplate();
|
||||
}
|
||||
|
||||
/** The field holds nothing the user typed: empty, or what the last commit left there. */
|
||||
@@ -200,9 +201,35 @@ export function preparedMessageText(raw: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** `commit.template` resolved as git does: `~/` in the home directory, a relative path in the repository. */
|
||||
export function templatePath(value: string, root: string, home: string): string {
|
||||
if (value === "~" || value.startsWith("~/")) return home.replace(/[/\\]$/, "") + value.slice(1);
|
||||
if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)) return value;
|
||||
return `${root}/${value}`;
|
||||
}
|
||||
|
||||
/** Fills an empty message field with the repository's commit template (`commit.template`), without its `#` lines. */
|
||||
async function loadTemplate() {
|
||||
const root = useVcs.getState().root;
|
||||
if (!root || useCommit.getState().message.trim()) return;
|
||||
try {
|
||||
const value = await call(commands.vcsConfigGet(root, "commit.template"));
|
||||
if (!value) return;
|
||||
const home = value.startsWith("~") ? await (await import("@tauri-apps/api/path")).homeDir() : "";
|
||||
const text = preparedMessageText(await call(commands.fsReadText(templatePath(value, root, home))));
|
||||
if (!text || useVcs.getState().root !== root || useCommit.getState().message.trim()) return;
|
||||
// Not typed: a prepared merge message may replace it.
|
||||
keptMessage = text;
|
||||
useCommit.setState({ message: text });
|
||||
} catch {
|
||||
// No readable template: the field stays empty.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like IDEA, pre-fills the commit message (unless something was typed) with the
|
||||
* message git prepared for a merge / cherry-pick / revert that stopped (e.g. on conflicts).
|
||||
* message git prepared for a merge / cherry-pick / revert that stopped (e.g. on conflicts),
|
||||
* otherwise with the commit template.
|
||||
*/
|
||||
export function installPreparedMessage(): () => void {
|
||||
const load = async () => {
|
||||
@@ -217,6 +244,7 @@ export function installPreparedMessage(): () => void {
|
||||
};
|
||||
const op = (s = useVcs.getState()) => s.branch?.operation ?? null;
|
||||
if (PREPARED_MESSAGE_OPS.has(op() ?? "")) void load();
|
||||
else void loadTemplate();
|
||||
return useVcs.subscribe((s, prev) => {
|
||||
if (s.root !== prev.root) {
|
||||
// Another repository: nothing typed or ticked for the previous one carries over (Amend least of all).
|
||||
@@ -233,6 +261,7 @@ export function installPreparedMessage(): () => void {
|
||||
selectedShelf: null,
|
||||
});
|
||||
if (PREPARED_MESSAGE_OPS.has(op(s) ?? "")) void load();
|
||||
else void loadTemplate();
|
||||
return;
|
||||
}
|
||||
const now = op(s);
|
||||
|
||||
Reference in New Issue
Block a user