Git - show main worktree under the Worktrees node (#287564)

This commit is contained in:
Ladislau Szomoru
2026-01-13 16:38:32 +01:00
committed by GitHub
parent 6872394141
commit f5e25ad4ca
4 changed files with 44 additions and 15 deletions

View File

@@ -80,6 +80,7 @@ export interface Worktree {
readonly name: string;
readonly path: string;
readonly ref: string;
readonly main: boolean;
readonly detached: boolean;
}

View File

@@ -6,7 +6,7 @@
import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode';
import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util';
import { Repository } from './repository';
import { Commit, Ref, RefType } from './api/git';
import { Ref, RefType, Worktree } from './api/git';
import { OperationKind } from './operation';
/**
@@ -55,11 +55,14 @@ function sortRefByName(refA: Ref, refB: Ref): number {
return 0;
}
function sortByCommitDateDesc(a: { commitDetails?: Commit }, b: { commitDetails?: Commit }): number {
const aCommitDate = a.commitDetails?.commitDate?.getTime() ?? 0;
const bCommitDate = b.commitDetails?.commitDate?.getTime() ?? 0;
return bCommitDate - aCommitDate;
function sortByWorktreeTypeAndNameAsc(a: Worktree, b: Worktree): number {
if (a.main && !b.main) {
return -1;
} else if (!a.main && b.main) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
}
export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable {
@@ -164,7 +167,7 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
} else if (group === 'worktrees') {
const worktrees = await this.repository.getWorktreeDetails();
return worktrees.sort(sortByCommitDateDesc).map(w => ({
return worktrees.sort(sortByWorktreeTypeAndNameAsc).map(w => ({
id: w.path,
name: w.name,
description: coalesce([
@@ -172,10 +175,11 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
w.commitDetails?.hash.substring(0, shortCommitLength),
w.commitDetails?.message.split('\n')[0]
]).join(' \u2022 '),
icon: isCopilotWorktree(w.path)
? new ThemeIcon('chat-sparkle')
: new ThemeIcon('worktree'),
timestamp: w.commitDetails?.commitDate?.getTime(),
icon: w.main
? new ThemeIcon('repo')
: isCopilotWorktree(w.path)
? new ThemeIcon('chat-sparkle')
: new ThemeIcon('worktree')
}));
}
} catch (err) {

View File

@@ -29,6 +29,7 @@ export interface IDotGit {
readonly path: string;
readonly commonPath?: string;
readonly superProjectPath?: string;
readonly isBare: boolean;
}
export interface IFileStatus {
@@ -575,7 +576,12 @@ export class Git {
commonDotGitPath = path.normalize(commonDotGitPath);
}
const raw = await fs.readFile(path.join(commonDotGitPath ?? dotGitPath, 'config'), 'utf8');
const coreSections = GitConfigParser.parse(raw).find(s => s.name === 'core');
const isBare = coreSections?.properties['bare'] === 'true';
return {
isBare,
path: dotGitPath,
commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined,
superProjectPath: superProjectPath ? path.normalize(superProjectPath) : undefined
@@ -2954,10 +2960,27 @@ export class Repository {
private async getWorktreesFS(): Promise<Worktree[]> {
try {
// List all worktree folder names
const worktreesPath = path.join(this.dotGit.commonPath ?? this.dotGit.path, 'worktrees');
const mainRepositoryPath = this.dotGit.commonPath ?? this.dotGit.path;
const worktreesPath = path.join(mainRepositoryPath, 'worktrees');
const dirents = await fs.readdir(worktreesPath, { withFileTypes: true });
const result: Worktree[] = [];
if (!this.dotGit.isBare) {
// Add main worktree for a non-bare repository
const headPath = path.join(mainRepositoryPath, 'HEAD');
const headContent = (await fs.readFile(headPath, 'utf8')).trim();
const mainRepositoryWorktreeName = path.basename(path.dirname(mainRepositoryPath));
result.push({
name: mainRepositoryWorktreeName,
path: path.dirname(mainRepositoryPath),
ref: headContent.replace(/^ref: /, ''),
detached: !headContent.startsWith('ref: '),
main: true
} satisfies Worktree);
}
for (const dirent of dirents) {
if (!dirent.isDirectory()) {
continue;
@@ -2977,7 +3000,8 @@ export class Repository {
// Remove 'ref: ' prefix
ref: headContent.replace(/^ref: /, ''),
// Detached if HEAD does not start with 'ref: '
detached: !headContent.startsWith('ref: ')
detached: !headContent.startsWith('ref: '),
main: false
});
} catch (err) {
if (/ENOENT/.test(err.message)) {

View File

@@ -15,7 +15,7 @@ import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, F
import { AutoFetcher } from './autofetch';
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
import { debounce, memoize, sequentialize, throttle } from './decorators';
import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git';
import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, IDotGit, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git';
import { GitHistoryProvider } from './historyProvider';
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands';
@@ -866,7 +866,7 @@ export class Repository implements Disposable {
return this.repository.rootRealPath;
}
get dotGit(): { path: string; commonPath?: string } {
get dotGit(): IDotGit {
return this.repository.dotGit;
}