diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 5bbb4b03a9c..3e8c92349cc 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -30,6 +30,7 @@ export interface Ref { readonly type: RefType; readonly name?: string; readonly commit?: string; + readonly commitDetails?: Commit; readonly remote?: string; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1ab056ccafc..cddc183ba4c 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -8,8 +8,8 @@ import * as path from 'path'; import { Command, commands, Disposable, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; -import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; -import { Git, Ref, Stash } from './git'; +import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; +import { Git, Stash } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; @@ -109,6 +109,22 @@ class RefItem implements QuickPickItem { } class CheckoutItem extends RefItem { + override get description(): string { + const description: string[] = []; + + if (typeof this.ref.behind === 'number' && typeof this.ref.ahead === 'number') { + description.push(`${this.ref.behind}↓ ${this.ref.ahead}↑`); + } + if (this.ref.commitDetails?.commitDate) { + description.push(fromNow(this.ref.commitDetails.commitDate, true, true)); + } + + return description.length > 0 ? description.join(' | ') : this.shortCommit; + } + + constructor(override readonly ref: Branch) { + super(ref); + } async run(repository: Repository, opts?: { detached?: boolean }): Promise { if (!this.ref.name) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 2a178220959..e42ca1e00d8 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -13,7 +13,7 @@ import { EventEmitter } from 'events'; import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePath } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; -import { Ref as ApiRef, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions } from './api/git'; +import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -733,10 +733,6 @@ export interface RefQuery extends ApiRefQuery { readonly includeCommitDetails?: boolean; } -export interface Ref extends ApiRef { - readonly commitDetails?: Commit; -} - interface GitConfigSection { name: string; subSectionName?: string; @@ -1135,16 +1131,18 @@ function parseGitBlame(data: string): BlameInformation[] { const REFS_FORMAT = '%(refname)%00%(objectname)%00%(*objectname)'; const REFS_WITH_DETAILS_FORMAT = `${REFS_FORMAT}%00%(parent)%00%(*parent)%00%(authorname)%00%(*authorname)%00%(committerdate:unix)%00%(*committerdate:unix)%00%(subject)%00%(*subject)`; -function parseRefs(data: string): Ref[] { - const refRegex = /^(.*)\0([0-9a-f]{40})\0([0-9a-f]{40})?(?:\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)\0(.*))?$/gm; +function parseRefs(data: string): (Ref | Branch)[] { + const refRegex = /^(refs\/[^\0]+)\0([0-9a-f]{40})\0([0-9a-f]{40})?(?:\0(.*))?$/gm; const headRegex = /^refs\/heads\/([^ ]+)$/; const remoteHeadRegex = /^refs\/remotes\/([^/]+)\/([^ ]+)$/; const tagRegex = /^refs\/tags\/([^ ]+)$/; + const statusRegex = /\[(?:ahead ([0-9]+))?[,\s]*(?:behind ([0-9]+))?]|\[gone]/; let ref: string | undefined; let commitHash: string | undefined; let tagCommitHash: string | undefined; + let details: string | undefined; let commitParents: string | undefined; let tagCommitParents: string | undefined; let commitSubject: string | undefined; @@ -1153,8 +1151,9 @@ function parseRefs(data: string): Ref[] { let tagAuthorName: string | undefined; let committerDate: string | undefined; let tagCommitterDate: string | undefined; + let status: string | undefined; - const refs: Ref[] = []; + const refs: (Ref | Branch)[] = []; let match: RegExpExecArray | null; let refMatch: RegExpExecArray | null; @@ -1165,7 +1164,8 @@ function parseRefs(data: string): Ref[] { break; } - [, ref, commitHash, tagCommitHash, commitParents, tagCommitParents, authorName, tagAuthorName, committerDate, tagCommitterDate, commitSubject, tagCommitSubject] = match; + [, ref, commitHash, tagCommitHash, details] = match; + [commitParents, tagCommitParents, authorName, tagAuthorName, committerDate, tagCommitterDate, commitSubject, tagCommitSubject, status] = details?.split('\0') ?? []; const parents = tagCommitParents || commitParents; const subject = tagCommitSubject || commitSubject; @@ -1179,11 +1179,13 @@ function parseRefs(data: string): Ref[] { parents: parents.split(' '), authorName: author, commitDate: date ? new Date(Number(date) * 1000) : undefined, - refNames: [] - } satisfies Commit : undefined; + } satisfies ApiCommit : undefined; if (refMatch = headRegex.exec(ref)) { - refs.push({ name: refMatch[1], commit: commitHash, commitDetails, type: RefType.Head }); + const [, aheadCount, behindCount] = statusRegex.exec(status) ?? []; + const ahead = status ? aheadCount ? Number(aheadCount) : 0 : undefined; + const behind = status ? behindCount ? Number(behindCount) : 0 : undefined; + refs.push({ name: refMatch[1], commit: commitHash, commitDetails, ahead, behind, type: RefType.Head }); } else if (refMatch = remoteHeadRegex.exec(ref)) { const name = `${refMatch[1]}/${refMatch[2]}`; refs.push({ name, remote: refMatch[1], commit: commitHash, commitDetails, type: RefType.RemoteHead }); @@ -2623,7 +2625,7 @@ export class Repository { .map(([ref]): Branch => ({ name: ref, type: RefType.Head })); } - async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise { + async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<(Ref | Branch)[]> { if (cancellationToken && cancellationToken.isCancellationRequested) { throw new CancellationError(); } @@ -2638,7 +2640,14 @@ export class Repository { args.push('--sort', `-${query.sort}`); } - args.push('--format', query.includeCommitDetails ? REFS_WITH_DETAILS_FORMAT : REFS_FORMAT); + if (query.includeCommitDetails) { + const format = this._git.compareGitVersionTo('1.9.0') !== -1 + ? `${REFS_WITH_DETAILS_FORMAT}%00%(upstream:track)` + : REFS_WITH_DETAILS_FORMAT; + args.push('--format', format); + } else { + args.push('--format', REFS_FORMAT); + } if (query.pattern) { const patterns = Array.isArray(query.pattern) ? query.pattern : [query.pattern]; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index d9e37e07eff..784de38e037 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1617,7 +1617,7 @@ export class Repository implements Disposable { } } - async getRefs(query: RefQuery = {}, cancellationToken?: CancellationToken): Promise { + async getRefs(query: RefQuery = {}, cancellationToken?: CancellationToken): Promise<(Ref | Branch)[]> { const config = workspace.getConfiguration('git'); let defaultSort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder'); if (defaultSort !== 'alphabetically' && defaultSort !== 'committerdate') {