ソースを参照

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年前
コミット
358fa9071c
17個のファイルの変更181行の追加115行の削除
  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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

@ -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 ファイルの表示

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

読み込み中…
キャンセル
保存