diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 285d76ee55b..18b49fcb268 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -80,6 +80,7 @@ export interface Worktree { readonly name: string; readonly path: string; readonly ref: string; + readonly main: boolean; readonly detached: boolean; } diff --git a/extensions/git/src/artifactProvider.ts b/extensions/git/src/artifactProvider.ts index f99e262b9c4..f63899efa3e 100644 --- a/extensions/git/src/artifactProvider.ts +++ b/extensions/git/src/artifactProvider.ts @@ -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) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 012710ca9f7..5b2ca69d087 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -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 { 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)) { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 7ca1c886194..8480e6d3617 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -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; }