feat(vcs): Add a modal commit mode and fill in Git log, branch popup and settings (#144)

- Settings › Commit › "Use non-modal commit interface": when off, Local
  Changes, Shelf and Stash become tabs of the Git tool window and ⌘K opens
  a Commit dialog; switches live.
- Git tool window: Worktrees tab; Show History and Show Git Log for a
  branch open their own closable tabs instead of changing the Log filters.
- Log: toolbar and context menu for a commit's changed files, vertical
  toolbar, Tags node and hiding for the branches pane, Collapse / Expand
  Linear Branches as separate actions, and a Version Control › Log page.
- Branch popup: collapsible groups, branches grouped by prefix, an options
  menu behind the gear, incoming / outgoing icons and "Pull into … Using
  Rebase / Merge"; New Branch on ⌥⌘N outside the editor.
- Git main menu regrouped with Uncommitted Changes and Current File
  submenus; rebase / merge controls enabled only while one is in progress.
- Settings: Version Control lists its pages; new options for clearing the
  initial commit message, showing Update options, confirming dropped
  commits, revealing created patches, large-file and file-name warnings,
  the Push dialog after Commit and Push, combined stashes and shelves, and
  removing applied files from the shelf.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-28 21:49:43 +08:00
1 джерело 66c36a7361
коміт eb67595e72
54 files changed
+3425 -871

No files matched your search

+29 -4
Переглянути файл
@@ -140,6 +140,30 @@ export function forgetAmendDraft() {
amendData = null;
}
/** The message a commit left in the field (not typed since). */
let keptMessage: string | null = null;
/**
* The message field after a commit, as IDEA's: the committed message stays for the
* next commit unless Settings › Version Control › Commit › "Clear initial commit
* message" is on.
*/
export function messageAfterCommit(committed: string, clear: boolean): string {
return clear ? "" : committed.trim();
}
export function keepMessageAfterCommit(committed: string, clear: boolean) {
const message = messageAfterCommit(committed, clear);
keptMessage = message || null;
useCommit.setState({ message });
}
/** The field holds nothing the user typed: empty, or what the last commit left there. */
function messageReplaceable(): boolean {
const m = useCommit.getState().message.trim();
return !m || m === keptMessage;
}
/** Operations whose commit message git prepares in `.git/MERGE_MSG`. */
const PREPARED_MESSAGE_OPS = new Set(["merge", "cherry-pick", "revert"]);
@@ -153,16 +177,16 @@ export function preparedMessageText(raw: string): string {
}
/**
* Like IDEA, pre-fills an empty commit message with the message git prepared
* for a merge / cherry-pick / revert that stopped (e.g. on conflicts).
* 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).
*/
export function installPreparedMessage(): () => void {
const load = async () => {
const root = useVcs.getState().root;
if (!root || useCommit.getState().message.trim()) return;
if (!root || !messageReplaceable()) return;
try {
const text = preparedMessageText(await call(commands.fsReadText(`${root}/.git/MERGE_MSG`)));
if (text && !useCommit.getState().message.trim()) useCommit.setState({ message: text });
if (text && messageReplaceable()) useCommit.setState({ message: text });
} catch {
// No prepared message (or `.git` is a worktree file): leave the field empty.
}
@@ -173,6 +197,7 @@ export function installPreparedMessage(): () => void {
if (s.root !== prev.root) {
// Another repository: nothing typed or ticked for the previous one carries over (Amend least of all).
amendData = null;
keptMessage = null;
useCommit.setState({
message: "",
amend: false,