瀏覽代碼

Closes #1523 improves stashes by separating on ref

main
Eric Amodio 2 年之前
父節點
當前提交
db953aaf8d
共有 10 個檔案被更改,包括 123 行新增231 行删除
  1. +1
    -0
      CHANGELOG.md
  2. +1
    -1
      package.json
  3. +7
    -7
      src/env/node/git/git.ts
  4. +68
    -2
      src/env/node/git/localGitProvider.ts
  5. +15
    -0
      src/git/formatters/commitFormatter.ts
  6. +11
    -6
      src/git/models/commit.ts
  7. +0
    -1
      src/git/parsers.ts
  8. +12
    -13
      src/git/parsers/logParser.ts
  9. +0
    -197
      src/git/parsers/stashParser.ts
  10. +8
    -4
      src/views/nodes/stashNode.ts

+ 1
- 0
CHANGELOG.md 查看文件

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## Changed
- Improves how stashes are shown in the _Stashes_ view by separating the associated branch from the stash message — closes [#1523](https://github.com/gitkraken/vscode-gitlens/issues/1523)
- Changes previous Gerrit remote support to Google Source remote support — thanks to [PR #1954](https://github.com/gitkraken/vscode-gitlens/pull/1954) by Felipe Santos ([@felipecrs](https://github.com/felipecrs))
- Renames "Gutter Blame" annotations to "File Blame"
- Renames "Gutter Changes" annotations to "File Changes"

+ 1
- 1
package.json 查看文件

@ -780,7 +780,7 @@
},
"gitlens.views.formats.stashes.description": {
"type": "string",
"default": "${agoOrDate}",
"default": "${stashOnRef, }${agoOrDate}",
"markdownDescription": "Specifies the description format of stashes in the views. See [_Commit Tokens_](https://github.com/gitkraken/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window",
"order": 35

+ 7
- 7
src/env/node/git/git.ts 查看文件

@ -3,7 +3,7 @@ import { hrtime } from '@env/hrtime';
import { GlyphChars } from '../../../constants';
import { GitCommandOptions, GitErrorHandling } from '../../../git/commandOptions';
import { GitDiffFilter, GitRevision, GitUser } from '../../../git/models';
import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from '../../../git/parsers';
import { GitBranchParser, GitLogParser, GitReflogParser, GitTagParser } from '../../../git/parsers';
import { Logger } from '../../../logger';
import { dirname, isAbsolute, isFolderGlob, joinPaths, normalizePath, splitPath } from '../../../system/path';
import { getDurationMilliseconds } from '../../../system/string';
@ -1386,18 +1386,18 @@ export class Git {
stash__list(
repoPath: string,
{
format = GitStashParser.defaultFormat,
similarityThreshold,
}: { format?: string; similarityThreshold?: number | null } = {},
{ args, similarityThreshold }: { args?: string[]; similarityThreshold?: number | null },
) {
if (args == null) {
args = ['--name-status'];
}
return this.git<string>(
{ cwd: repoPath },
'stash',
'list',
'--name-status',
...args,
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
`--format=${format}`,
);
}

+ 68
- 2
src/env/node/git/localGitProvider.ts 查看文件

@ -61,12 +61,15 @@ import {
GitBranch,
GitBranchReference,
GitCommit,
GitCommitIdentity,
GitContributor,
GitDiff,
GitDiffFilter,
GitDiffHunkLine,
GitDiffShortStat,
GitFile,
GitFileChange,
GitFileStatus,
GitLog,
GitMergeStatus,
GitRebaseStatus,
@ -75,6 +78,7 @@ import {
GitRemote,
GitRevision,
GitStash,
GitStashCommit,
GitStatus,
GitStatusFile,
GitTag,
@ -95,7 +99,6 @@ import {
GitLogParser,
GitReflogParser,
GitRemoteParser,
GitStashParser,
GitStatusParser,
GitTagParser,
GitTreeParser,
@ -150,6 +153,8 @@ const doubleQuoteRegex = /"/g;
const driveLetterRegex = /(?<=^\/?)([a-zA-Z])(?=:\/)/;
const userConfigRegex = /^user\.(name|email) (.*)$/gm;
const mappedAuthorRegex = /(.+)\s<(.+)>/;
const stashSummaryRegex =
/(?:(?:(?<wip>WIP) on|On) (?<onref>[^/](?!.*\/\.)(?!.*\.\.)(?!.*\/\/)(?!.*@\{)[^\000-\037\177 ~^:?*[\\]+[^./]):\s*)?(?<summary>.*)$/s;
const reflogCommands = ['merge', 'pull'];
@ -3226,10 +3231,71 @@ export class LocalGitProvider implements GitProvider, Disposable {
let stash = this.useCaching ? this._stashesCache.get(repoPath) : undefined;
if (stash === undefined) {
const parser = GitLogParser.createWithFiles<{
sha: string;
date: string;
committedDate: string;
stashName: string;
summary: string;
}>({
sha: '%H',
date: '%at',
committedDate: '%ct',
stashName: '%gd',
summary: '%B',
});
const data = await this.git.stash__list(repoPath, {
args: parser.arguments,
similarityThreshold: this.container.config.advanced.similarityThreshold,
});
stash = GitStashParser.parse(this.container, data, repoPath);
const commits = new Map<string, GitStashCommit>();
const stashes = parser.parse(data);
for (const s of stashes) {
let onRef;
let summary;
let message;
const match = stashSummaryRegex.exec(s.summary);
if (match?.groups != null) {
onRef = match.groups.onref;
if (match.groups.wip) {
message = `WIP: ${match.groups.summary.trim()}`;
summary = `WIP on ${onRef}`;
} else {
message = match.groups.summary.trim();
summary = message.split('\n', 1)[0] ?? '';
}
} else {
message = s.summary.trim();
summary = message.split('\n', 1)[0] ?? '';
}
commits.set(
s.sha,
new GitCommit(
this.container,
repoPath,
s.sha,
new GitCommitIdentity('You', undefined, new Date((s.date as any) * 1000)),
new GitCommitIdentity('You', undefined, new Date((s.committedDate as any) * 1000)),
summary,
[],
message,
s.files?.map(
f => new GitFileChange(repoPath, f.path, f.status as GitFileStatus, f.originalPath),
) ?? [],
undefined,
[],
s.stashName,
onRef,
) as GitStashCommit,
);
}
stash = { repoPath: repoPath, commits: commits };
// sw.stop();
if (this.useCaching) {
this._stashesCache.set(repoPath, stash ?? null);

+ 15
- 0
src/git/formatters/commitFormatter.ts 查看文件

@ -77,6 +77,9 @@ export interface CommitFormatOptions extends FormatOptions {
pullRequestDate?: TokenOptions;
pullRequestState?: TokenOptions;
sha?: TokenOptions;
stashName?: TokenOptions;
stashNumber?: TokenOptions;
stashOnRef?: TokenOptions;
tips?: TokenOptions;
};
}
@ -590,6 +593,18 @@ export class CommitFormatter extends Formatter {
return this._padOrTruncate(this._item.shortSha ?? '', this._options.tokenOptions.sha);
}
get stashName(): string {
return this._padOrTruncate(this._item.stashName ?? '', this._options.tokenOptions.stashName);
}
get stashNumber(): string {
return this._padOrTruncate(this._item.number ?? '', this._options.tokenOptions.stashNumber);
}
get stashOnRef(): string {
return this._padOrTruncate(this._item.stashOnRef ?? '', this._options.tokenOptions.stashOnRef);
}
get tips(): string {
let branchAndTagTips = this._options.getBranchAndTagTips?.(this._item.sha, { icons: this._options.markdown });
if (branchAndTagTips != null && this._options.markdown) {

+ 11
- 6
src/git/models/commit.ts 查看文件

@ -49,6 +49,7 @@ export class GitCommit implements GitRevisionReference {
readonly stashName: string | undefined;
// TODO@eamodio rename to stashNumber
readonly number: string | undefined;
readonly stashOnRef: string | undefined;
constructor(
private readonly container: Container,
@ -63,11 +64,20 @@ export class GitCommit implements GitRevisionReference {
stats?: GitCommitStats,
lines?: GitCommitLine | GitCommitLine[] | undefined,
stashName?: string | undefined,
stashOnRef?: string | undefined,
) {
this.ref = this.sha;
this.refType = stashName ? 'stash' : 'revision';
this.shortSha = this.sha.substring(0, this.container.CommitShaFormatting.length);
if (stashName) {
this.refType = 'stash';
this.stashName = stashName || undefined;
this.stashOnRef = stashOnRef || undefined;
this.number = stashNumberRegex.exec(stashName)?.[1];
} else {
this.refType = 'revision';
}
// Add an ellipsis to the summary if there is or might be more message
if (message != null) {
this._message = message;
@ -115,11 +125,6 @@ export class GitCommit implements GitRevisionReference {
} else {
this.lines = [];
}
if (stashName) {
this.stashName = stashName || undefined;
this.number = stashNumberRegex.exec(stashName)?.[1];
}
}
get date(): Date {

+ 0
- 1
src/git/parsers.ts 查看文件

@ -4,7 +4,6 @@ export * from './parsers/diffParser';
export * from './parsers/logParser';
export * from './parsers/reflogParser';
export * from './parsers/remoteParser';
export * from './parsers/stashParser';
export * from './parsers/statusParser';
export * from './parsers/tagParser';
export * from './parsers/treeParser';

+ 12
- 13
src/git/parsers/logParser.ts 查看文件

@ -169,7 +169,7 @@ export class GitLogParser {
const fields = getLines(data, options?.separator ?? '\0');
if (options?.skip) {
for (let i = 0; i < options.skip; i++) {
field = fields.next();
fields.next();
}
}
@ -213,9 +213,9 @@ export class GitLogParser {
return { arguments: args, parse: parse };
}
static createWithFiles<T extends Record<string, string>>(fieldMapping: T): ParserWithFiles<T> {
let format = '%x00%x00';
const keys: (keyof T)[] = [];
static createWithFiles<T extends Record<string, unknown>>(fieldMapping: ExtractAll<T, string>): ParserWithFiles<T> {
let format = '%x00';
const keys: (keyof ExtractAll<T, string>)[] = [];
for (const key in fieldMapping) {
keys.push(key);
format += `%x00${fieldMapping[key]}`;
@ -224,24 +224,21 @@ export class GitLogParser {
const args = ['-z', `--format=${format}`, '--name-status'];
function* parse(data: string): Generator<ParsedEntryWithFiles<T>> {
const records = getLines(data, '\0\0\0\0');
const records = getLines(data, '\0\0\0');
let entry: ParsedEntryWithFiles<T>;
let files: ParsedEntryFile[];
let fields: IterableIterator<string>;
let first = true;
for (let record of records) {
if (first) {
first = false;
// Fix the first record (since it only has 3 nulls)
record = record.slice(3);
}
for (const record of records) {
entry = {} as any;
files = [];
fields = getLines(record, '\0');
// Skip the 2 starting NULs
fields.next();
fields.next();
let fieldCount = 0;
let field;
while (true) {
@ -259,6 +256,8 @@ export class GitLogParser {
field = fields.next();
file.originalPath = field.value;
}
files.push(file);
}
}

+ 0
- 197
src/git/parsers/stashParser.ts 查看文件

@ -1,197 +0,0 @@
import type { Container } from '../../container';
import { filterMap } from '../../system/array';
import { debug } from '../../system/decorators/log';
import { normalizePath } from '../../system/path';
import { getLines } from '../../system/string';
import {
GitCommit,
GitCommitIdentity,
GitFile,
GitFileChange,
GitFileIndexStatus,
GitStash,
GitStashCommit,
} from '../models';
import { fileStatusRegex } from './logParser';
// import { Logger } from './logger';
// Using %x00 codes because some shells seem to try to expand things if not
const lb = '%x3c'; // `%x${'<'.charCodeAt(0).toString(16)}`;
const rb = '%x3e'; // `%x${'>'.charCodeAt(0).toString(16)}`;
const sl = '%x2f'; // `%x${'/'.charCodeAt(0).toString(16)}`;
const sp = '%x20'; // `%x${' '.charCodeAt(0).toString(16)}`;
interface StashEntry {
ref?: string;
date?: string;
committedDate?: string;
fileNames?: string;
files?: GitFile[];
summary?: string;
stashName?: string;
}
export class GitStashParser {
static defaultFormat = [
`${lb}${sl}f${rb}`,
`${lb}r${rb}${sp}%H`, // ref
`${lb}d${rb}${sp}%at`, // date
`${lb}c${rb}${sp}%ct`, // committed date
`${lb}l${rb}${sp}%gd`, // reflog-selector
`${lb}s${rb}`,
'%B', // summary
`${lb}${sl}s${rb}`,
`${lb}f${rb}`,
].join('%n');
@debug({ args: false, singleLine: true })
static parse(container: Container, data: string, repoPath: string): GitStash | undefined {
if (!data) return undefined;
const lines = getLines(`${data}</f>`);
// Skip the first line since it will always be </f>
let next = lines.next();
if (next.done) return undefined;
if (repoPath !== undefined) {
repoPath = normalizePath(repoPath);
}
const commits = new Map<string, GitStashCommit>();
let entry: StashEntry = {};
let line: string | undefined = undefined;
let token: number;
let match;
let renamedFileName;
while (true) {
next = lines.next();
if (next.done) break;
line = next.value;
// <<1-char token>> <data>
// e.g. <r> bd1452a2dc
token = line.charCodeAt(1);
switch (token) {
case 114: // 'r': // ref
entry = {
ref: line.substring(4),
};
break;
case 100: // 'd': // author-date
entry.date = line.substring(4);
break;
case 99: // 'c': // committer-date
entry.committedDate = line.substring(4);
break;
case 108: // 'l': // reflog-selector
entry.stashName = line.substring(4);
break;
case 115: // 's': // summary
while (true) {
next = lines.next();
if (next.done) break;
line = next.value;
if (line === '</s>') break;
if (entry.summary === undefined) {
entry.summary = line;
} else {
entry.summary += `\n${line}`;
}
}
// Remove the trailing newline
if (entry.summary != null && entry.summary.charCodeAt(entry.summary.length - 1) === 10) {
entry.summary = entry.summary.slice(0, -1);
}
break;
case 102: // 'f': // files
// Skip the blank line git adds before the files
next = lines.next();
if (!next.done && next.value !== '</f>') {
while (true) {
next = lines.next();
if (next.done) break;
line = next.value;
if (line === '</f>') break;
if (line.startsWith('warning:')) continue;
match = fileStatusRegex.exec(line);
if (match != null) {
if (entry.files === undefined) {
entry.files = [];
}
renamedFileName = match[3];
if (renamedFileName !== undefined) {
entry.files.push({
status: match[1] as GitFileIndexStatus,
path: renamedFileName,
originalPath: match[2],
});
} else {
entry.files.push({
status: match[1] as GitFileIndexStatus,
path: match[2],
});
}
}
}
if (entry.files != null) {
entry.fileNames = filterMap(entry.files, f => (f.path ? f.path : undefined)).join(', ');
}
}
GitStashParser.parseEntry(container, entry, repoPath, commits);
entry = {};
}
}
const stash: GitStash = {
repoPath: repoPath,
commits: commits,
};
return stash;
}
private static parseEntry(
container: Container,
entry: StashEntry,
repoPath: string,
commits: Map<string, GitStashCommit>,
) {
let commit = commits.get(entry.ref!);
if (commit == null) {
commit = new GitCommit(
container,
repoPath,
entry.ref!,
new GitCommitIdentity('You', undefined, new Date((entry.date! as any) * 1000)),
new GitCommitIdentity('You', undefined, new Date((entry.committedDate! as any) * 1000)),
entry.summary?.split('\n', 1)[0] ?? '',
[],
entry.summary ?? '',
entry.files?.map(f => new GitFileChange(repoPath, f.path, f.status, f.originalPath)) ?? [],
undefined,
[],
entry.stashName,
) as GitStashCommit;
}
commits.set(entry.ref!, commit);
}
}

+ 8
- 4
src/views/nodes/stashNode.ts 查看文件

@ -66,10 +66,14 @@ export class StashNode extends ViewRefNode
dateFormat: this.view.container.config.defaultDateFormat,
});
item.contextValue = ContextValues.Stash;
item.tooltip = CommitFormatter.fromTemplate(`\${ago} (\${date})\n\n\${message}`, this.commit, {
dateFormat: this.view.container.config.defaultDateFormat,
// messageAutolinks: true,
});
item.tooltip = CommitFormatter.fromTemplate(
`\${'On 'stashOnRef\n}\${ago} (\${date})\n\n\${message}`,
this.commit,
{
dateFormat: this.view.container.config.defaultDateFormat,
// messageAutolinks: true,
},
);
return item;
}

Loading…
取消
儲存