Преглед на файлове

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;

Зареждане…
Отказ
Запис