Browse Source

Changes tag parsing - hopefully fixes #855

Unifies tag parsing to include msg, ref, & dates
Bumps required Git to 2.7.2
Adds more tag sorting options similar to branches
main
Eric Amodio 5 years ago
parent
commit
358fa9071c
17 changed files with 181 additions and 115 deletions
  1. +6
    -2
      package.json
  2. +1
    -1
      src/commands/git/cherry-pick.ts
  3. +1
    -1
      src/commands/git/merge.ts
  4. +1
    -1
      src/commands/git/rebase.ts
  5. +2
    -2
      src/commands/git/switch.ts
  6. +39
    -21
      src/commands/quickCommand.helpers.ts
  7. +3
    -1
      src/config.ts
  8. +3
    -3
      src/extension.ts
  9. +2
    -2
      src/git/git.ts
  10. +2
    -23
      src/git/gitService.ts
  11. +1
    -1
      src/git/models/repository.ts
  12. +68
    -4
      src/git/models/tag.ts
  13. +26
    -36
      src/git/parsers/tagParser.ts
  14. +2
    -2
      src/messages.ts
  15. +9
    -5
      src/quickpicks/gitQuickPicks.ts
  16. +1
    -5
      src/quickpicks/referencesQuickPick.ts
  17. +14
    -5
      src/views/nodes/tagNode.ts

+ 6
- 2
package.json View File

@ -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"

+ 1
- 1
src/commands/git/cherry-pick.ts View File

@ -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)

+ 1
- 1
src/commands/git/merge.ts View File

@ -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
}),

+ 1
- 1
src/commands/git/rebase.ts View File

@ -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],

+ 2
- 2
src/commands/git/switch.ts View File

@ -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 }
);

+ 39
- 21
src/commands/quickCommand.helpers.ts View File

@ -9,21 +9,19 @@ export async function getBranches(
repos: Repository | Repository[],
options: { filterBranches?: (b: GitBranch) => boolean; picked?: string | string[] } = {}
): Promise<BranchQuickPickItem[]> {
return getBranchesAndOrTags(repos, false, options) as Promise<BranchQuickPickItem[]>;
return getBranchesAndOrTags(repos, ['branches'], options) as Promise<BranchQuickPickItem[]>;
}
export async function getTags(
repos: Repository | Repository[],
options: { filterTags?: (t: GitTag) => boolean; picked?: string | string[] } = {}
): Promise<TagQuickPickItem[]> {
return getBranchesAndOrTags(repos, true, { ...options, filterBranches: () => false }) as Promise<
TagQuickPickItem[]
>;
return getBranchesAndOrTags(repos, ['tags'], options) as Promise<TagQuickPickItem[]>;
}
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<GitBranch[], GitTag[] | undefined>([
repo.getBranches({ filter: filterBranches, sort: true }),
includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined
[branches, tags] = await Promise.all<GitBranch[] | undefined, GitTag[] | undefined>([
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<GitBranch[][], GitTag[][] | undefined>([
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<GitBranch[][] | undefined, GitTag[][] | undefined>([
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<BranchQuickPickItem | TagQuickPickItem>([
...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(

+ 3
- 1
src/config.ts View File

@ -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 {

+ 3
- 3
src/extension.ts View File

@ -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) {

+ 2
- 2
src/git/git.ts View File

@ -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<string>({ cwd: repoPath }, 'tag', '-l', '-n1');
return git<string>({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`);
}
}

+ 2
- 23
src/git/gitService.ts View File

@ -177,7 +177,6 @@ export class GitService implements Disposable {
private readonly _branchesCache = new Map<string, GitBranch[]>();
private readonly _tagsCache = new Map<string, GitTag[]>();
private readonly _tagsWithRefsCache = new Map<string, GitTag[]>();
private readonly _trackedCache = new Map<string, boolean | Promise<boolean>>();
private readonly _userMapCache = new Map<string, { name?: string; email?: string } | null>();
@ -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<GitTag[]> {
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;

+ 1
- 1
src/git/models/repository.ts View File

@ -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<GitTag[]> {
getTags(options?: { filter?: (t: GitTag) => boolean; sort?: boolean }): Promise<GitTag[]> {
return Container.git.getTags(this.path, options);
}

+ 68
- 4
src/git/models/tag.ts View File

@ -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<GitTag['formatCommitDate']>(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<GitTag['formatDate']>(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;

+ 26
- 36
src/git/parsers/tagParser.ts View File

@ -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 = /^<n>(.+)<r>(.*)<d>(.*)<ad>(.*)<s>(.*)$/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);

+ 2
- 2
src/messages.ts View File

@ -72,10 +72,10 @@ export class Messages {
);
}
static showGitVersionUnsupportedErrorMessage(version: string): Promise<MessageItem | undefined> {
static showGitVersionUnsupportedErrorMessage(version: string, required: string): Promise<MessageItem | undefined> {
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
);
}

+ 9
- 5
src/quickpicks/gitQuickPicks.ts View File

@ -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 = {

+ 1
- 5
src/quickpicks/referencesQuickPick.ts View File

@ -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

+ 14
- 5
src/views/nodes/tagNode.ts View File

@ -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<RepositoriesView> 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;

Loading…
Cancel
Save