diff --git a/package.json b/package.json index 44e2c7d..85aa072 100644 --- a/package.json +++ b/package.json @@ -1308,11 +1308,15 @@ "default": "name:desc", "enum": [ "name:desc", - "name:asc" + "name:asc", + "date:desc", + "date:asc" ], "enumDescriptions": [ "Sorts tags by name in descending order", - "Sorts tags by name in ascending order" + "Sorts tags by name in ascending order", + "Sorts tags by date in descending order", + "Sorts tags by date in ascending order" ], "markdownDescription": "Specifies how tags are sorted in quick pick menus and views", "scope": "window" diff --git a/src/commands/git/cherry-pick.ts b/src/commands/git/cherry-pick.ts index 340da63..acf0142 100644 --- a/src/commands/git/cherry-pick.ts +++ b/src/commands/git/cherry-pick.ts @@ -132,7 +132,7 @@ export class CherryPickGitCommand extends QuickCommandBase { )}(select or enter a reference)`, matchOnDescription: true, matchOnDetail: true, - items: await getBranchesAndOrTags(state.repo, true, { + items: await getBranchesAndOrTags(state.repo, ['branches', 'tags'], { filterBranches: b => b.id !== destId }), onValidateValue: getValidateGitReferenceFn(state.repo) diff --git a/src/commands/git/merge.ts b/src/commands/git/merge.ts index 9462ac0..1afc7eb 100644 --- a/src/commands/git/merge.ts +++ b/src/commands/git/merge.ts @@ -125,7 +125,7 @@ export class MergeGitCommand extends QuickCommandBase { )}(select or enter a reference)`, matchOnDescription: true, matchOnDetail: true, - items: await getBranchesAndOrTags(state.repo, true, { + items: await getBranchesAndOrTags(state.repo, ['branches', 'tags'], { filterBranches: b => b.id !== destId, picked: state.reference && state.reference.ref }), diff --git a/src/commands/git/rebase.ts b/src/commands/git/rebase.ts index 8c357d3..db20c97 100644 --- a/src/commands/git/rebase.ts +++ b/src/commands/git/rebase.ts @@ -145,7 +145,7 @@ export class RebaseGitCommand extends QuickCommandBase { } onto${GlyphChars.Space.repeat(3)}(select or enter a reference)`, matchOnDescription: true, matchOnDetail: true, - items: await getBranchesAndOrTags(state.repo, true, { + items: await getBranchesAndOrTags(state.repo, ['branches', 'tags'], { picked: state.reference && state.reference.ref }), additionalButtons: [pickBranchOrCommitButton], diff --git a/src/commands/git/switch.ts b/src/commands/git/switch.ts index ab560eb..92bc69c 100644 --- a/src/commands/git/switch.ts +++ b/src/commands/git/switch.ts @@ -138,7 +138,7 @@ export class SwitchGitCommand extends QuickCommandBase { const items = await getBranchesAndOrTags( state.repos, - showTags, + showTags ? ['branches', 'tags'] : ['branches'], state.repos.length === 1 ? undefined : { filterBranches: b => !b.remote } ); @@ -171,7 +171,7 @@ export class SwitchGitCommand extends QuickCommandBase { quickpick.items = await getBranchesAndOrTags( state.repos!, - showTags, + showTags ? ['branches', 'tags'] : ['branches'], state.repos!.length === 1 ? undefined : { filterBranches: b => !b.remote } ); diff --git a/src/commands/quickCommand.helpers.ts b/src/commands/quickCommand.helpers.ts index ec5c398..b934cfb 100644 --- a/src/commands/quickCommand.helpers.ts +++ b/src/commands/quickCommand.helpers.ts @@ -9,21 +9,19 @@ export async function getBranches( repos: Repository | Repository[], options: { filterBranches?: (b: GitBranch) => boolean; picked?: string | string[] } = {} ): Promise { - return getBranchesAndOrTags(repos, false, options) as Promise; + return getBranchesAndOrTags(repos, ['branches'], options) as Promise; } export async function getTags( repos: Repository | Repository[], options: { filterTags?: (t: GitTag) => boolean; picked?: string | string[] } = {} ): Promise { - return getBranchesAndOrTags(repos, true, { ...options, filterBranches: () => false }) as Promise< - TagQuickPickItem[] - >; + return getBranchesAndOrTags(repos, ['tags'], options) as Promise; } export async function getBranchesAndOrTags( repos: Repository | Repository[], - includeTags: boolean, + include: ('tags' | 'branches')[], { filterBranches, filterTags, @@ -34,7 +32,7 @@ export async function getBranchesAndOrTags( picked?: string | string[]; } = {} ): Promise<(BranchQuickPickItem | TagQuickPickItem)[]> { - let branches: GitBranch[]; + let branches: GitBranch[] | undefined; let tags: GitTag[] | undefined; let singleRepo = false; @@ -42,32 +40,36 @@ export async function getBranchesAndOrTags( singleRepo = true; const repo = repos instanceof Repository ? repos : repos[0]; - [branches, tags] = await Promise.all([ - repo.getBranches({ filter: filterBranches, sort: true }), - includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined + [branches, tags] = await Promise.all([ + include.includes('branches') ? repo.getBranches({ filter: filterBranches, sort: true }) : undefined, + include.includes('tags') ? repo.getTags({ filter: filterTags, sort: true }) : undefined ]); } else { - const [branchesByRepo, tagsByRepo] = await Promise.all([ - Promise.all(repos.map(r => r.getBranches({ filter: filterBranches, sort: true }))), - includeTags - ? Promise.all(repos.map(r => r.getTags({ filter: filterTags, includeRefs: true, sort: true }))) + const [branchesByRepo, tagsByRepo] = await Promise.all([ + include.includes('branches') + ? Promise.all(repos.map(r => r.getBranches({ filter: filterBranches, sort: true }))) + : undefined, + include.includes('tags') + ? Promise.all(repos.map(r => r.getTags({ filter: filterTags, sort: true }))) : undefined ]); - branches = GitBranch.sort( - Arrays.intersection(...branchesByRepo, ((b1: GitBranch, b2: GitBranch) => b1.name === b2.name) as any) - ); + if (include.includes('branches')) { + branches = GitBranch.sort( + Arrays.intersection(...branchesByRepo!, ((b1: GitBranch, b2: GitBranch) => b1.name === b2.name) as any) + ); + } - if (includeTags) { + if (include.includes('tags')) { tags = GitTag.sort( Arrays.intersection(...tagsByRepo!, ((t1: GitTag, t2: GitTag) => t1.name === t2.name) as any) ); } } - if (!includeTags) { + if (include.includes('branches') && !include.includes('tags')) { return Promise.all( - branches.map(b => + branches!.map(b => BranchQuickPickItem.create( b, picked != null && (typeof picked === 'string' ? b.ref === picked : picked.includes(b.ref)), @@ -82,8 +84,23 @@ export async function getBranchesAndOrTags( ); } + if (include.includes('tags') && !include.includes('branches')) { + return Promise.all( + tags!.map(t => + TagQuickPickItem.create( + t, + picked != null && (typeof picked === 'string' ? t.ref === picked : picked.includes(t.ref)), + { + message: singleRepo, + ref: singleRepo + } + ) + ) + ); + } + return Promise.all([ - ...branches + ...branches! .filter(b => !b.remote) .map(b => BranchQuickPickItem.create( @@ -101,12 +118,13 @@ export async function getBranchesAndOrTags( t, picked != null && (typeof picked === 'string' ? t.ref === picked : picked.includes(t.ref)), { + message: singleRepo, ref: singleRepo, type: true } ) ), - ...branches + ...branches! .filter(b => b.remote) .map(b => BranchQuickPickItem.create( diff --git a/src/config.ts b/src/config.ts index d97d287..349a21d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -201,7 +201,9 @@ export enum StatusBarCommand { export enum TagSorting { NameDesc = 'name:desc', - NameAsc = 'name:asc' + NameAsc = 'name:asc', + DateDesc = 'date:desc', + DateAsc = 'date:asc' } export enum ViewBranchesLayout { diff --git a/src/extension.ts b/src/extension.ts index bc1e7a1..ae1cd0c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -150,10 +150,10 @@ async function migrateSettings(context: ExtensionContext, previousVersion: strin } function notifyOnUnsupportedGitVersion(version: string) { - if (GitService.compareGitVersion('2.2.0') !== -1) return; + if (GitService.compareGitVersion('2.7.2') !== -1) return; - // If git is less than v2.2.0 - void Messages.showGitVersionUnsupportedErrorMessage(version); + // If git is less than v2.7.2 + void Messages.showGitVersionUnsupportedErrorMessage(version, '2.7.2'); } async function showWelcomeOrWhatsNew(version: string, previousVersion: string | undefined) { diff --git a/src/git/git.ts b/src/git/git.ts index a0c13ad..fab35bb 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -8,7 +8,7 @@ import { Logger } from '../logger'; import { Objects, Strings } from '../system'; import { findGitPath, GitLocation } from './locator'; import { run, RunOptions } from './shell'; -import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser } from './parsers/parsers'; +import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from './parsers/parsers'; import { GitFileStatus } from './models/file'; export * from './models/models'; @@ -1156,6 +1156,6 @@ export namespace Git { } export function tag(repoPath: string) { - return git({ cwd: repoPath }, 'tag', '-l', '-n1'); + return git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); } } diff --git a/src/git/gitService.ts b/src/git/gitService.ts index db96065..d04020d 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -177,7 +177,6 @@ export class GitService implements Disposable { private readonly _branchesCache = new Map(); private readonly _tagsCache = new Map(); - private readonly _tagsWithRefsCache = new Map(); private readonly _trackedCache = new Map>(); private readonly _userMapCache = new Map(); @@ -198,7 +197,6 @@ export class GitService implements Disposable { this._repositoryTree.forEach(r => r.dispose()); this._branchesCache.clear(); this._tagsCache.clear(); - this._tagsWithRefsCache.clear(); this._trackedCache.clear(); this._userMapCache.clear(); @@ -230,7 +228,6 @@ export class GitService implements Disposable { this._branchesCache.delete(repo.path); this._tagsCache.delete(repo.path); - this._tagsWithRefsCache.clear(); this._trackedCache.clear(); if (e.changed(RepositoryChange.Config)) { @@ -1176,10 +1173,7 @@ export class GitService implements Disposable { @log() async getBranchesAndTagsTipsFn(repoPath: string | undefined, currentName?: string) { - const [branches, tags] = await Promise.all([ - this.getBranches(repoPath), - this.getTags(repoPath, { includeRefs: true }) - ]); + const [branches, tags] = await Promise.all([this.getBranches(repoPath), this.getTags(repoPath)]); const branchesAndTagsBySha = Arrays.groupByFilterMap( (branches as { name: string; sha: string }[]).concat(tags as { name: string; sha: string }[]), @@ -2475,27 +2469,12 @@ export class GitService implements Disposable { @log() async getTags( repoPath: string | undefined, - options: { filter?: (t: GitTag) => boolean; includeRefs?: boolean; sort?: boolean } = {} + options: { filter?: (t: GitTag) => boolean; sort?: boolean } = {} ): Promise { if (repoPath === undefined) return []; let tags: GitTag[] | undefined; try { - if (options.includeRefs) { - tags = this._tagsWithRefsCache.get(repoPath); - if (tags !== undefined) return tags; - - const data = await Git.show_ref__tags(repoPath); - tags = GitTagParser.parseWithRef(data, repoPath) || []; - - const repo = await this.getRepository(repoPath); - if (repo !== undefined && repo.supportsChangeEvents) { - this._tagsWithRefsCache.set(repoPath, tags); - } - - return tags; - } - tags = this._tagsCache.get(repoPath); if (tags !== undefined) return tags; diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 9a92a7a..e7f3bc7 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -360,7 +360,7 @@ export class Repository implements Disposable { return Container.git.getStatusForRepo(this.path); } - getTags(options?: { filter?: (t: GitTag) => boolean; includeRefs?: boolean; sort?: boolean }): Promise { + getTags(options?: { filter?: (t: GitTag) => boolean; sort?: boolean }): Promise { return Container.git.getTags(this.path, options); } diff --git a/src/git/models/tag.ts b/src/git/models/tag.ts index 7c78aa2..3bfd6c7 100644 --- a/src/git/models/tag.ts +++ b/src/git/models/tag.ts @@ -1,7 +1,17 @@ 'use strict'; -import { memoize } from '../../system'; +import { Dates, memoize } from '../../system'; import { GitReference } from './models'; -import { configuration, TagSorting } from '../../configuration'; +import { configuration, DateStyle, TagSorting } from '../../configuration'; + +export const TagDateFormatting = { + dateFormat: undefined! as string | null, + dateStyle: undefined! as DateStyle, + + reset: () => { + TagDateFormatting.dateFormat = configuration.get('defaultDateFormat'); + TagDateFormatting.dateStyle = configuration.get('defaultDateStyle'); + } +}; export class GitTag implements GitReference { static is(tag: any): tag is GitTag { @@ -16,6 +26,10 @@ export class GitTag implements GitReference { const order = configuration.get('sortTagsBy'); switch (order) { + case TagSorting.DateAsc: + return tags.sort((a, b) => a.date.getTime() - b.date.getTime()); + case TagSorting.DateDesc: + return tags.sort((a, b) => b.date.getTime() - a.date.getTime()); case TagSorting.NameAsc: return tags.sort((a, b) => b.name.localeCompare(a.name, undefined, { numeric: true, sensitivity: 'base' }) @@ -32,15 +46,65 @@ export class GitTag implements GitReference { constructor( public readonly repoPath: string, public readonly name: string, - public readonly sha?: string, - public readonly annotation?: string + public readonly sha: string, + public readonly message: string, + public readonly date: Date, + public readonly commitDate: Date | undefined ) {} + get formattedDate(): string { + return TagDateFormatting.dateStyle === DateStyle.Absolute + ? this.formatDate(TagDateFormatting.dateFormat) + : this.formatDateFromNow(); + } + get ref() { return this.name; } @memoize() + private get commitDateFormatter(): Dates.DateFormatter | undefined { + return this.commitDate == null ? undefined : Dates.getFormatter(this.commitDate); + } + + @memoize() + private get dateFormatter(): Dates.DateFormatter { + return Dates.getFormatter(this.date); + } + + @memoize(format => (format == null ? 'MMMM Do, YYYY h:mma' : format)) + formatCommitDate(format?: string | null) { + const formatter = this.commitDateFormatter; + if (formatter == null) return ''; + + if (format == null) { + format = 'MMMM Do, YYYY h:mma'; + } + + return formatter.format(format); + } + + formatCommitDateFromNow() { + const formatter = this.commitDateFormatter; + if (formatter == null) return ''; + + return formatter.fromNow(); + } + + @memoize(format => (format == null ? 'MMMM Do, YYYY h:mma' : format)) + formatDate(format?: string | null) { + if (format == null) { + format = 'MMMM Do, YYYY h:mma'; + } + + return this.dateFormatter.format(format); + } + + formatDateFromNow() { + return this.dateFormatter.fromNow(); + } + + @memoize() getBasename(): string { const index = this.name.lastIndexOf('/'); return index !== -1 ? this.name.substring(index + 1) : this.name; diff --git a/src/git/parsers/tagParser.ts b/src/git/parsers/tagParser.ts index 6f057f0..896a96c 100644 --- a/src/git/parsers/tagParser.ts +++ b/src/git/parsers/tagParser.ts @@ -2,10 +2,21 @@ import { GitTag } from '../git'; import { debug } from '../../system'; -const tagWithRefRegex = /([0-9,a-f]+)\srefs\/tags\/(.*)/gm; -const tagWithAnnotationRegex = /^(.+?)(?:$|(?:\s+)(.*)$)/gm; +const tagRegex = /^(.+)(.*)(.*)(.*)(.*)$/gm; + +// Using %x00 codes because some shells seem to try to expand things if not +const lb = '%3c'; // `%${'<'.charCodeAt(0).toString(16)}`; +const rb = '%3e'; // `%${'>'.charCodeAt(0).toString(16)}`; export class GitTagParser { + static defaultFormat = [ + `${lb}n${rb}%(refname)`, // tag name + `${lb}r${rb}%(objectname)`, // ref + `${lb}d${rb}%(creatordate:iso8601)`, // created date + `${lb}ad${rb}%(authordate:iso8601)`, // author date + `${lb}s${rb}%(subject)` // message + ].join(''); + @debug({ args: false, singleLine: true }) static parse(data: string, repoPath: string): GitTag[] | undefined { if (!data) return undefined; @@ -13,52 +24,31 @@ export class GitTagParser { const tags: GitTag[] = []; let name; - let annotation; + let ref; + let date; + let commitDate; + let message; let match; do { - match = tagWithAnnotationRegex.exec(data); + match = tagRegex.exec(data); if (match == null) break; - [, name, annotation] = match; - - tags.push( - new GitTag( - repoPath, - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${name}`.substr(1), - undefined, - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - annotation == null || annotation.length === 0 ? undefined : ` ${annotation}`.substr(1) - ) - ); - } while (true); - - return tags; - } - - static parseWithRef(data: string, repoPath: string): GitTag[] | undefined { - if (!data) return undefined; - - const tags: GitTag[] = []; - - let sha; - let name; - - let match: RegExpExecArray | null; - do { - match = tagWithRefRegex.exec(data); - if (match == null) break; + [, name, ref, date, commitDate, message] = match; - [, sha, name] = match; + // Strip off refs/tags/ + name = name.substr(10); tags.push( new GitTag( repoPath, + name, // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${name}`.substr(1), + ` ${ref}`.substr(1), // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${sha}`.substr(1) + ` ${message}`.substr(1), + new Date(date), + commitDate == null || commitDate.length === 0 ? undefined : new Date(commitDate) ) ); } while (true); diff --git a/src/messages.ts b/src/messages.ts index fc0216d..610e906 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -72,10 +72,10 @@ export class Messages { ); } - static showGitVersionUnsupportedErrorMessage(version: string): Promise { + static showGitVersionUnsupportedErrorMessage(version: string, required: string): Promise { return Messages.showMessage( 'error', - `GitLens requires a newer version of Git (>= 2.2.0) than is currently installed (${version}). Please install a more recent version of Git.`, + `GitLens requires a newer version of Git (>= ${required}) than is currently installed (${version}). Please install a more recent version of Git.`, SuppressedMessages.GitVersionWarning ); } diff --git a/src/quickpicks/gitQuickPicks.ts b/src/quickpicks/gitQuickPicks.ts index 2bbd9fd..0e2e3d4 100644 --- a/src/quickpicks/gitQuickPicks.ts +++ b/src/quickpicks/gitQuickPicks.ts @@ -368,7 +368,7 @@ export namespace TagQuickPickItem { tag: GitTag, picked?: boolean, options: { - annotation?: boolean; + message?: boolean; checked?: boolean; ref?: boolean; type?: boolean; @@ -379,15 +379,19 @@ export namespace TagQuickPickItem { description = 'tag'; } - if (options.ref && tag.sha) { + if (options.ref) { description = description ? `${description}${Strings.pad('$(git-commit)', 2, 2)}${GitService.shortenSha(tag.sha)}` : `${Strings.pad('$(git-commit)', 0, 2)}${GitService.shortenSha(tag.sha)}`; + + description = description + ? `${description}${Strings.pad(GlyphChars.Dot, 2, 2)}${tag.formattedDate}` + : tag.formattedDate; } - if (options.annotation && tag.annotation) { - const annotation = emojify(tag.annotation); - description = description ? `${description}${Strings.pad(GlyphChars.Dot, 2, 2)}${annotation}` : annotation; + if (options.message) { + const message = emojify(tag.message); + description = description ? `${description}${Strings.pad(GlyphChars.Dot, 2, 2)}${message}` : message; } const item: TagQuickPickItem = { diff --git a/src/quickpicks/referencesQuickPick.ts b/src/quickpicks/referencesQuickPick.ts index 4e40d1f..c0faec9 100644 --- a/src/quickpicks/referencesQuickPick.ts +++ b/src/quickpicks/referencesQuickPick.ts @@ -160,11 +160,7 @@ export class ReferencesQuickPick { }) : undefined, include & ReferencesQuickPickIncludes.Tags - ? Container.git.getTags(this.repoPath, { - ...options, - filter: filterTags, - includeRefs: true - }) + ? Container.git.getTags(this.repoPath, { ...options, filter: filterTags }) : undefined ]), token diff --git a/src/views/nodes/tagNode.ts b/src/views/nodes/tagNode.ts index 42d178a..afd90bd 100644 --- a/src/views/nodes/tagNode.ts +++ b/src/views/nodes/tagNode.ts @@ -2,7 +2,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; import { Container } from '../../container'; -import { GitService, GitTag, GitUri } from '../../git/gitService'; +import { GitService, GitTag, GitUri, TagDateFormatting } from '../../git/gitService'; import { Iterables, Strings } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { CommitNode } from './commitNode'; @@ -11,7 +11,6 @@ import { insertDateMarkers } from './helpers'; import { PageableViewNode, ResourceType, ViewNode, ViewRefNode } from './viewNode'; import { emojify } from '../../emojis'; import { RepositoryNode } from './repositoryNode'; -import { Git } from '../../git/git'; import { GlyphChars } from '../../constants'; export class TagNode extends ViewRefNode implements PageableViewNode { @@ -72,9 +71,19 @@ export class TagNode extends ViewRefNode implements PageableVi const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed); item.id = this.id; item.contextValue = ResourceType.Tag; - item.description = this.tag.annotation !== undefined ? emojify(this.tag.annotation) : ''; - item.tooltip = `${this.tag.name}${ - this.tag.annotation !== undefined ? `\n${emojify(this.tag.annotation)}` : '' + item.description = `${GitService.shortenSha(this.tag.sha, { force: true })}${Strings.pad( + GlyphChars.Dot, + 2, + 2 + )}${emojify(this.tag.message)}`; + item.tooltip = `${this.tag.name}${Strings.pad(GlyphChars.Dash, 2, 2)}${GitService.shortenSha(this.tag.sha, { + force: true + })}\n${this.tag.formatDateFromNow()} (${this.tag.formatDate(TagDateFormatting.dateFormat)})\n\n${emojify( + this.tag.message + )}${ + this.tag.commitDate != null && this.tag.date !== this.tag.commitDate + ? `\n${this.tag.formatCommitDateFromNow()} (${this.tag.formatCommitDate(TagDateFormatting.dateFormat)})` + : '' }`; return item;