Browse Source

Completes consolidation of git commit models

main
Eric Amodio 2 years ago
parent
commit
6ffe113669
101 changed files with 1853 additions and 2176 deletions
  1. +3
    -3
      src/annotations/annotations.ts
  2. +4
    -17
      src/annotations/blameAnnotationProvider.ts
  3. +7
    -5
      src/annotations/gutterBlameAnnotationProvider.ts
  4. +18
    -6
      src/annotations/gutterChangesAnnotationProvider.ts
  5. +3
    -3
      src/annotations/gutterHeatmapBlameAnnotationProvider.ts
  6. +3
    -3
      src/annotations/lineAnnotationController.ts
  7. +9
    -9
      src/codelens/codeLensProvider.ts
  8. +3
    -3
      src/commands/common.ts
  9. +13
    -15
      src/commands/copyMessageToClipboard.ts
  10. +1
    -1
      src/commands/copyShaToClipboard.ts
  11. +1
    -1
      src/commands/diffLineWithPrevious.ts
  12. +4
    -4
      src/commands/diffLineWithWorking.ts
  13. +12
    -13
      src/commands/diffWith.ts
  14. +3
    -3
      src/commands/diffWithNext.ts
  15. +6
    -6
      src/commands/diffWithPrevious.ts
  16. +9
    -9
      src/commands/diffWithRevisionFrom.ts
  17. +2
    -2
      src/commands/externalDiff.ts
  18. +40
    -9
      src/commands/git/branch.ts
  19. +17
    -7
      src/commands/git/log.ts
  20. +2
    -2
      src/commands/git/search.ts
  21. +49
    -26
      src/commands/git/show.ts
  22. +1
    -5
      src/commands/git/stash.ts
  23. +77
    -53
      src/commands/gitCommands.actions.ts
  24. +4
    -6
      src/commands/openFileAtRevision.ts
  25. +3
    -3
      src/commands/openFileAtRevisionFrom.ts
  26. +1
    -1
      src/commands/openOnRemote.ts
  27. +3
    -3
      src/commands/openRevisionFile.ts
  28. +22
    -18
      src/commands/quickCommand.steps.ts
  29. +5
    -5
      src/commands/showQuickCommit.ts
  30. +4
    -4
      src/commands/showQuickCommitFile.ts
  31. +5
    -2
      src/commands/stashApply.ts
  32. +2
    -9
      src/env/node/git/git.ts
  33. +66
    -83
      src/env/node/git/localGitProvider.ts
  34. +67
    -81
      src/git/formatters/commitFormatter.ts
  35. +38
    -16
      src/git/formatters/statusFormatter.ts
  36. +3
    -6
      src/git/gitProvider.ts
  37. +5
    -7
      src/git/gitProviderService.ts
  38. +3
    -14
      src/git/gitUri.ts
  39. +0
    -2
      src/git/models.ts
  40. +12
    -7
      src/git/models/blame.ts
  41. +1
    -1
      src/git/models/branch.ts
  42. +345
    -359
      src/git/models/commit.ts
  43. +132
    -14
      src/git/models/file.ts
  44. +2
    -4
      src/git/models/log.ts
  45. +0
    -259
      src/git/models/logCommit.ts
  46. +1
    -1
      src/git/models/merge.ts
  47. +1
    -1
      src/git/models/rebase.ts
  48. +6
    -6
      src/git/models/reference.ts
  49. +2
    -1
      src/git/models/reflog.ts
  50. +2
    -2
      src/git/models/remote.ts
  51. +2
    -2
      src/git/models/repository.ts
  52. +1
    -1
      src/git/models/stash.ts
  53. +0
    -97
      src/git/models/stashCommit.ts
  54. +71
    -97
      src/git/models/status.ts
  55. +1
    -1
      src/git/models/tag.ts
  56. +43
    -63
      src/git/parsers/blameParser.ts
  57. +5
    -4
      src/git/parsers/diffParser.ts
  58. +118
    -154
      src/git/parsers/logParser.ts
  59. +32
    -24
      src/git/parsers/stashParser.ts
  60. +2
    -2
      src/git/remotes/provider.ts
  61. +19
    -19
      src/hovers/hovers.ts
  62. +2
    -2
      src/hovers/lineHoverController.ts
  63. +3
    -5
      src/messages.ts
  64. +7
    -16
      src/premium/github/github.ts
  65. +193
    -173
      src/premium/github/githubGitProvider.ts
  66. +2
    -2
      src/quickpicks/commitPicker.ts
  67. +28
    -28
      src/quickpicks/commitQuickPickItems.ts
  68. +12
    -13
      src/quickpicks/gitQuickPickItems.ts
  69. +2
    -2
      src/quickpicks/quickPicksItems.ts
  70. +33
    -29
      src/statusbar/statusBarController.ts
  71. +3
    -3
      src/system/array.ts
  72. +24
    -0
      src/system/iterable.ts
  73. +1
    -1
      src/system/promise.ts
  74. +7
    -3
      src/trackers/gitLineTracker.ts
  75. +2
    -2
      src/views/branchesView.ts
  76. +2
    -2
      src/views/commitsView.ts
  77. +26
    -15
      src/views/nodes/branchTrackingStatusFilesNode.ts
  78. +18
    -9
      src/views/nodes/commitFileNode.ts
  79. +16
    -14
      src/views/nodes/commitNode.ts
  80. +1
    -1
      src/views/nodes/compareBranchNode.ts
  81. +2
    -2
      src/views/nodes/compareResultsNode.ts
  82. +14
    -13
      src/views/nodes/fileHistoryNode.ts
  83. +23
    -22
      src/views/nodes/fileRevisionAsCommitNode.ts
  84. +3
    -3
      src/views/nodes/helpers.ts
  85. +35
    -104
      src/views/nodes/lineHistoryNode.ts
  86. +4
    -4
      src/views/nodes/mergeConflictCurrentChangesNode.ts
  87. +3
    -3
      src/views/nodes/mergeConflictFileNode.ts
  88. +4
    -8
      src/views/nodes/mergeConflictIncomingChangesNode.ts
  89. +1
    -1
      src/views/nodes/pullRequestNode.ts
  90. +8
    -22
      src/views/nodes/rebaseStatusNode.ts
  91. +1
    -1
      src/views/nodes/resultsFileNode.ts
  92. +3
    -3
      src/views/nodes/resultsFilesNode.ts
  93. +2
    -2
      src/views/nodes/stashFileNode.ts
  94. +4
    -7
      src/views/nodes/stashNode.ts
  95. +7
    -7
      src/views/nodes/statusFileNode.ts
  96. +27
    -57
      src/views/nodes/statusFilesNode.ts
  97. +2
    -2
      src/views/remotesView.ts
  98. +2
    -2
      src/views/repositoriesView.ts
  99. +6
    -6
      src/views/viewCommands.ts
  100. +1
    -1
      src/webviews/rebaseEditor.ts

+ 3
- 3
src/annotations/annotations.ts View File

@ -15,7 +15,7 @@ import { Config, configuration } from '../configuration';
import { Colors, GlyphChars } from '../constants';
import { Container } from '../container';
import { CommitFormatOptions, CommitFormatter } from '../git/formatters';
import { GitCommit2 } from '../git/models';
import { GitCommit } from '../git/models';
import { Strings } from '../system';
import { toRgba } from '../webviews/apps/shared/colors';
@ -141,7 +141,7 @@ export class Annotations {
}
static gutter(
commit: GitCommit2,
commit: GitCommit,
format: string,
dateFormatOrFormatOptions: string | null | CommitFormatOptions,
renderOptions: RenderOptions,
@ -227,7 +227,7 @@ export class Annotations {
}
static trailing(
commit: GitCommit2,
commit: GitCommit,
// uri: GitUri,
// editorLine: number,
format: string,

+ 4
- 17
src/annotations/blameAnnotationProvider.ts View File

@ -2,7 +2,7 @@ import { CancellationToken, Disposable, Hover, languages, Position, Range, TextD
import { FileAnnotationType } from '../config';
import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { GitBlame, GitCommit2 } from '../git/models';
import { GitBlame, GitCommit } from '../git/models';
import { Hovers } from '../hovers/hovers';
import { log } from '../system';
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
@ -171,24 +171,11 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
);
}
private async getDetailsHoverMessage(commit: GitCommit2, document: TextDocument) {
// // Get the full commit message -- since blame only returns the summary
// let logCommit: GitCommit | undefined = undefined;
// if (!commit.isUncommitted) {
// logCommit = await this.container.git.getCommitForFile(commit.repoPath, commit.uri, {
// ref: commit.sha,
// });
// if (logCommit != null) {
// // Preserve the previous commit from the blame commit
// logCommit.previousFileName = commit.previousFileName;
// logCommit.previousSha = commit.previousSha;
// }
// }
private async getDetailsHoverMessage(commit: GitCommit, document: TextDocument) {
let editorLine = this.editor.selection.active.line;
const line = editorLine + 1;
const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0];
editorLine = commitLine.originalLine - 1;
const commitLine = commit.lines.find(l => l.to.line === line) ?? commit.lines[0];
editorLine = commitLine.from.line - 1;
return Hovers.detailsMessage(
commit,

+ 7
- 5
src/annotations/gutterBlameAnnotationProvider.ts View File

@ -3,7 +3,7 @@ import { FileAnnotationType, GravatarDefaultStyle } from '../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { CommitFormatOptions, CommitFormatter } from '../git/formatters';
import { GitBlame, GitCommit2 } from '../git/models';
import { GitBlame, GitCommit } from '../git/models';
import { Logger } from '../logger';
import { Arrays, Iterables, log, Stopwatch, Strings } from '../system';
import { GitDocumentState } from '../trackers/gitDocumentTracker';
@ -75,7 +75,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
const decorationsMap = new Map<string, DecorationOptions | undefined>();
const avatarDecorationsMap = avatars ? new Map<string, ThemableDecorationAttachmentRenderOptions>() : undefined;
let commit: GitCommit2 | undefined;
let commit: GitCommit | undefined;
let compacted = false;
let gutter: DecorationOptions | undefined;
let previousSha: string | undefined;
@ -87,7 +87,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
for (const l of blame.lines) {
// editor lines are 0-based
const editorLine = l.line - 1;
const editorLine = l.to.line - 1;
if (previousSha === l.sha) {
if (gutter == null) continue;
@ -200,7 +200,9 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
const highlightDecorationRanges = Arrays.filterMap(blame.lines, l =>
l.sha === sha
? // editor lines are 0-based
this.editor.document.validateRange(new Range(l.line - 1, 0, l.line - 1, Number.MAX_SAFE_INTEGER))
this.editor.document.validateRange(
new Range(l.to.line - 1, 0, l.to.line - 1, Number.MAX_SAFE_INTEGER),
)
: undefined,
);
@ -208,7 +210,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
}
private async applyAvatarDecoration(
commit: GitCommit2,
commit: GitCommit,
gutter: DecorationOptions,
gravatarDefault: GravatarDefaultStyle,
map: Map<string, ThemableDecorationAttachmentRenderOptions>,

+ 18
- 6
src/annotations/gutterChangesAnnotationProvider.ts View File

@ -14,7 +14,7 @@ import {
} from 'vscode';
import { FileAnnotationType } from '../configuration';
import { Container } from '../container';
import { GitDiff, GitLogCommit } from '../git/models';
import { GitCommit, GitDiff } from '../git/models';
import { Hovers } from '../hovers/hovers';
import { Logger } from '../logger';
import { log, Stopwatch } from '../system';
@ -28,7 +28,7 @@ export interface ChangesAnnotationContext extends AnnotationContext {
}
export class GutterChangesAnnotationProvider extends AnnotationProviderBase<ChangesAnnotationContext> {
private state: { commit: GitLogCommit | undefined; diffs: GitDiff[] } | undefined;
private state: { commit: GitCommit | undefined; diffs: GitDiff[] } | undefined;
private hoverProviderDisposable: Disposable | undefined;
constructor(
@ -73,7 +73,7 @@ export class GutterChangesAnnotationProvider extends AnnotationProviderBase
let ref1 = this.trackedDocument.uri.sha;
let ref2 = context?.sha != null && context.sha !== ref1 ? `${context.sha}^` : undefined;
let commit: GitLogCommit | undefined;
let commit: GitCommit | undefined;
let localChanges = ref1 == null && ref2 == null;
if (localChanges) {
@ -103,7 +103,7 @@ export class GutterChangesAnnotationProvider extends AnnotationProviderBase
this.trackedDocument.uri.repoPath!,
this.trackedDocument.uri.fsPath,
);
const commits = status?.toPsuedoCommits(
const commits = status?.getPseudoCommits(
await this.container.git.getCurrentUser(this.trackedDocument.uri.repoPath!),
);
if (commits?.length) {
@ -293,7 +293,11 @@ export class GutterChangesAnnotationProvider extends AnnotationProviderBase
);
}
provideHover(document: TextDocument, position: Position, _token: CancellationToken): Hover | undefined {
async provideHover(
document: TextDocument,
position: Position,
_token: CancellationToken,
): Promise<Hover | undefined> {
if (this.state == null) return undefined;
if (this.container.config.hovers.annotations.over !== 'line' && position.character !== 0) return undefined;
@ -307,8 +311,16 @@ export class GutterChangesAnnotationProvider extends AnnotationProviderBase
position.line >= hunk.current.position.start - 1 &&
position.line <= hunk.current.position.end - (hasMoreDeletedLines ? 0 : 1)
) {
const markdown = await Hovers.localChangesMessage(
commit,
this.trackedDocument.uri,
position.line,
hunk,
);
if (markdown == null) return undefined;
return new Hover(
Hovers.localChangesMessage(commit, this.trackedDocument.uri, position.line, hunk),
markdown,
document.validateRange(
new Range(
hunk.current.position.start - 1,

+ 3
- 3
src/annotations/gutterHeatmapBlameAnnotationProvider.ts View File

@ -1,7 +1,7 @@
import { Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { FileAnnotationType } from '../configuration';
import { Container } from '../container';
import { GitCommit2 } from '../git/models';
import { GitCommit } from '../git/models';
import { Logger } from '../logger';
import { log, Stopwatch } from '../system';
import { GitDocumentState } from '../trackers/gitDocumentTracker';
@ -32,10 +32,10 @@ export class GutterHeatmapBlameAnnotationProvider extends BlameAnnotationProvide
>();
const computedHeatmap = await this.getComputedHeatmap(blame);
let commit: GitCommit2 | undefined;
let commit: GitCommit | undefined;
for (const l of blame.lines) {
// editor lines are 0-based
const editorLine = l.line - 1;
const editorLine = l.to.line - 1;
commit = blame.commits.get(l.sha);
if (commit == null) continue;

+ 3
- 3
src/annotations/lineAnnotationController.ts View File

@ -14,7 +14,7 @@ import { configuration } from '../configuration';
import { GlyphChars, isTextEditor } from '../constants';
import { Container } from '../container';
import { CommitFormatter } from '../git/formatters';
import { GitCommit2, PullRequest } from '../git/models';
import { GitCommit, PullRequest } from '../git/models';
import { Authentication } from '../git/remotes/provider';
import { LogCorrelationContext, Logger } from '../logger';
import { debug, log } from '../system/decorators/log';
@ -155,7 +155,7 @@ export class LineAnnotationController implements Disposable {
private async getPullRequests(
repoPath: string,
lines: [number, GitCommit2][],
lines: [number, GitCommit][],
{ timeout }: { timeout?: number } = {},
) {
if (lines.length === 0) return undefined;
@ -250,7 +250,7 @@ export class LineAnnotationController implements Disposable {
}
const commitLines = [
...filterMap<LineSelection, [number, GitCommit2]>(selections, selection => {
...filterMap<LineSelection, [number, GitCommit]>(selections, selection => {
const state = this.container.lineTracker.getState(selection.active);
if (state?.commit == null) {
Logger.debug(cc, `Line ${selection.active} returned no commit`);

+ 9
- 9
src/codelens/codeLensProvider.ts View File

@ -38,7 +38,7 @@ import {
import { BuiltInCommands, DocumentSchemes } from '../constants';
import { Container } from '../container';
import type { GitUri } from '../git/gitUri';
import { GitBlame, GitBlameLines, GitCommit2 } from '../git/models';
import { GitBlame, GitBlameLines, GitCommit } from '../git/models';
import { RemoteResourceType } from '../git/remotes/provider';
import { Logger } from '../logger';
import { is, once } from '../system/function';
@ -635,7 +635,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2 | undefined,
commit: GitCommit | undefined,
): T {
lens.command = command<[undefined, DiffWithPreviousCommandArgs]>({
title: title,
@ -654,7 +654,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyCopyOrOpenCommitOnRemoteCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2,
commit: GitCommit,
clipboard: boolean = false,
): T {
lens.command = command<[OpenOnRemoteCommandArgs]>({
@ -677,7 +677,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyCopyOrOpenFileOnRemoteCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2,
commit: GitCommit,
clipboard: boolean = false,
): T {
lens.command = command<[OpenOnRemoteCommandArgs]>({
@ -701,7 +701,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyRevealCommitInViewCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2 | undefined,
commit: GitCommit | undefined,
): T {
lens.command = command<[Uri, ShowQuickCommitCommandArgs]>({
title: title,
@ -721,7 +721,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
title: string,
lens: T,
blame: GitBlameLines,
commit?: GitCommit2,
commit?: GitCommit,
): T {
let refs;
if (commit === undefined) {
@ -746,7 +746,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2 | undefined,
commit: GitCommit | undefined,
): T {
lens.command = command<[Uri, ShowQuickCommitCommandArgs]>({
title: title,
@ -765,7 +765,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2 | undefined,
commit: GitCommit | undefined,
): T {
lens.command = command<[Uri, ShowQuickCommitFileCommandArgs]>({
title: title,
@ -825,7 +825,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
private applyToggleFileChangesCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(
title: string,
lens: T,
commit: GitCommit2,
commit: GitCommit,
only?: boolean,
): T {
lens.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({

+ 3
- 3
src/commands/common.ts View File

@ -20,11 +20,11 @@ import { GitUri } from '../git/gitUri';
import {
GitBranch,
GitCommit,
GitCommit2,
GitContributor,
GitFile,
GitReference,
GitRemote,
GitStashCommit,
GitTag,
Repository,
} from '../git/models';
@ -325,12 +325,12 @@ export function isCommandContextViewNodeHasBranch(
return GitBranch.is((context.node as ViewNode & { branch: GitBranch }).branch);
}
export function isCommandContextViewNodeHasCommit<T extends GitCommit | GitCommit2>(
export function isCommandContextViewNodeHasCommit<T extends GitCommit | GitStashCommit>(
context: CommandContext,
): context is CommandViewNodeContext & { node: ViewNode & { commit: T } } {
if (context.type !== 'viewItem') return false;
return GitCommit.is((context.node as ViewNode & { commit: GitCommit | GitCommit2 }).commit);
return GitCommit.is((context.node as ViewNode & { commit: GitCommit | GitStashCommit }).commit);
}
export function isCommandContextViewNodeHasContributor(

+ 13
- 15
src/commands/copyMessageToClipboard.ts View File

@ -14,6 +14,7 @@ import {
isCommandContextViewNodeHasCommit,
isCommandContextViewNodeHasTag,
} from './common';
import { GitActions } from './gitCommands.actions';
export interface CopyMessageToClipboardCommandArgs {
message?: string;
@ -30,7 +31,7 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand {
if (isCommandContextViewNodeHasCommit(context)) {
args = { ...args };
args.sha = context.node.commit.sha;
return this.execute(context.editor, context.node.commit.uri, args);
return this.execute(context.editor, context.node.commit.file?.uri, args);
} else if (isCommandContextViewNodeHasBranch(context)) {
args = { ...args };
args.sha = context.node.branch.sha;
@ -50,6 +51,7 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand {
try {
let repoPath;
// If we don't have an editor then get the message of the last commit to the branch
if (uri == null) {
repoPath = this.container.git.getBestRepository(editor)?.path;
@ -58,7 +60,10 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand {
const log = await this.container.git.getLog(repoPath, { limit: 1 });
if (log == null) return;
args.message = Iterables.first(log.commits.values()).message;
const commit = Iterables.first(log.commits.values());
if (commit?.message == null) return;
args.message = commit.message;
} else if (args.message == null) {
const gitUri = await GitUri.fromUri(uri);
repoPath = gitUri.repoPath;
@ -75,27 +80,20 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand {
editor.document.getText(),
)
: await this.container.git.getBlameForLine(gitUri, blameline);
if (blame == null) return;
if (blame.commit.isUncommitted) return;
if (blame == null || blame.commit.isUncommitted) return;
args.sha = blame.commit.sha;
if (!repoPath) {
repoPath = blame.commit.repoPath;
}
void (await GitActions.Commit.copyMessageToClipboard(blame.commit));
return;
} catch (ex) {
Logger.error(ex, 'CopyMessageToClipboardCommand', `getBlameForLine(${blameline})`);
void Messages.showGenericErrorMessage('Unable to copy message');
return;
}
} else {
void (await GitActions.Commit.copyMessageToClipboard({ ref: args.sha, repoPath: repoPath! }));
return;
}
// Get the full commit message -- since blame only returns the summary
const commit = await this.container.git.getCommit(repoPath!, args.sha);
if (commit == null) return;
args.message = commit.message;
}
void (await env.clipboard.writeText(args.message));

+ 1
- 1
src/commands/copyShaToClipboard.ts View File

@ -31,7 +31,7 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand {
args.sha = this.container.config.advanced.abbreviateShaOnCopy
? context.node.commit.shortSha
: context.node.commit.sha;
return this.execute(context.editor, context.node.commit.uri, args);
return this.execute(context.editor, context.node.commit.file?.uri, args);
} else if (isCommandContextViewNodeHasBranch(context)) {
args = { ...args };
args.sha = context.node.branch.sha;

+ 1
- 1
src/commands/diffLineWithPrevious.ts View File

@ -29,7 +29,7 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand {
args.line = editor?.selection.active.line ?? 0;
}
const gitUri = args.commit != null ? GitUri.fromCommit(args.commit) : await GitUri.fromUri(uri);
const gitUri = args.commit?.getGitUri() ?? (await GitUri.fromUri(uri));
try {
const diffUris = await this.container.git.getPreviousLineDiffUris(

+ 4
- 4
src/commands/diffLineWithWorking.ts View File

@ -1,14 +1,14 @@
import { TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { GitCommit, GitCommit2, GitRevision } from '../git/models';
import { GitCommit, GitRevision } from '../git/models';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common';
import { DiffWithCommandArgs } from './diffWith';
export interface DiffLineWithWorkingCommandArgs {
commit?: GitCommit | GitCommit2;
commit?: GitCommit;
line?: number;
showOptions?: TextDocumentShowOptions;
@ -56,7 +56,7 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand {
if (status?.indexStatus != null) {
lhsSha = GitRevision.uncommittedStaged;
lhsUri = this.container.git.getAbsoluteUri(
status.originalFileName || status.fileName,
status.originalPath || status.path,
args.commit.repoPath,
);
} else {
@ -68,7 +68,7 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand {
lhsUri = args.commit.file!.uri;
}
// editor lines are 0-based
args.line = blame.line.line - 1;
args.line = blame.line.to.line - 1;
} catch (ex) {
Logger.error(ex, 'DiffLineWithWorkingCommand', `getBlameForLine(${blameline})`);
void Messages.showGenericErrorMessage('Unable to open compare');

+ 12
- 13
src/commands/diffWith.ts View File

@ -1,7 +1,7 @@
import { commands, Range, TextDocumentShowOptions, Uri, ViewColumn } from 'vscode';
import { BuiltInCommands, GlyphChars } from '../constants';
import type { Container } from '../container';
import { GitCommit, GitCommit2, GitRevision } from '../git/models';
import { GitCommit, GitRevision } from '../git/models';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { basename } from '../system/path';
@ -25,31 +25,30 @@ export interface DiffWithCommandArgs {
@command()
export class DiffWithCommand extends Command {
static getMarkdownCommandArgs(args: DiffWithCommandArgs): string;
static getMarkdownCommandArgs(commit: GitCommit | GitCommit2, line?: number): string;
static getMarkdownCommandArgs(argsOrCommit: DiffWithCommandArgs | GitCommit | GitCommit2, line?: number): string {
let args: DiffWithCommandArgs | GitCommit | GitCommit2;
if (GitCommit.is(argsOrCommit) || GitCommit2.is(argsOrCommit)) {
static getMarkdownCommandArgs(commit: GitCommit, line?: number): string;
static getMarkdownCommandArgs(argsOrCommit: DiffWithCommandArgs | GitCommit, line?: number): string {
let args: DiffWithCommandArgs | GitCommit;
if (GitCommit.is(argsOrCommit)) {
const commit = argsOrCommit;
if (commit.file == null) {
debugger;
throw new Error('Commit has no file');
}
if (commit.isUncommitted) {
args = {
repoPath: commit.repoPath,
lhs: {
sha: 'HEAD',
uri: commit.uri,
uri: commit.file.uri,
},
rhs: {
sha: '',
uri: commit.uri,
uri: commit.file.uri,
},
line: line,
};
} else {
if (commit.file == null) {
debugger;
throw new Error('Commit has no file');
}
args = {
repoPath: commit.repoPath,
lhs: {
@ -58,7 +57,7 @@ export class DiffWithCommand extends Command {
},
rhs: {
sha: commit.sha,
uri: commit.uri,
uri: commit.file.uri,
},
line: line,
};

+ 3
- 3
src/commands/diffWithNext.ts View File

@ -1,14 +1,14 @@
import { Range, TextDocumentShowOptions, TextEditor, Uri } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { GitLogCommit } from '../git/models';
import { GitCommit } from '../git/models';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { ActiveEditorCommand, command, CommandContext, Commands, executeCommand, getCommandUri } from './common';
import { DiffWithCommandArgs } from './diffWith';
export interface DiffWithNextCommandArgs {
commit?: GitLogCommit;
commit?: GitCommit;
range?: Range;
inDiffLeftEditor?: boolean;
@ -39,7 +39,7 @@ export class DiffWithNextCommand extends ActiveEditorCommand {
args.line = editor?.selection.active.line ?? 0;
}
const gitUri = args.commit != null ? GitUri.fromCommit(args.commit) : await GitUri.fromUri(uri);
const gitUri = args.commit?.getGitUri() ?? (await GitUri.fromUri(uri));
try {
const diffUris = await this.container.git.getNextDiffUris(
gitUri.repoPath!,

+ 6
- 6
src/commands/diffWithPrevious.ts View File

@ -1,7 +1,7 @@
import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { GitCommit, GitCommit2, GitRevision } from '../git/models';
import { GitCommit, GitRevision } from '../git/models';
import { Logger } from '../logger';
import { Messages } from '../messages';
import {
@ -16,7 +16,7 @@ import {
import { DiffWithCommandArgs } from './diffWith';
export interface DiffWithPreviousCommandArgs {
commit?: GitCommit2 | GitCommit;
commit?: GitCommit;
inDiffRightEditor?: boolean;
uri?: Uri;
@ -52,17 +52,17 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
}
let gitUri;
if (args.commit != null) {
if (args.commit?.file != null) {
if (!args.commit.isUncommitted) {
void (await executeCommand<DiffWithCommandArgs>(Commands.DiffWith, {
repoPath: args.commit.repoPath,
lhs: {
sha: `${args.commit.sha}^`,
uri: args.commit.originalUri ?? args.commit.uri,
uri: args.commit.file.originalUri ?? args.commit.file.uri,
},
rhs: {
sha: args.commit.sha || '',
uri: args.commit.uri,
uri: args.commit.file.uri,
},
line: args.line,
showOptions: args.showOptions,
@ -71,7 +71,7 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
return;
}
gitUri = GitUri.fromCommit(args.commit);
gitUri = args.commit?.getGitUri();
} else {
gitUri = await GitUri.fromUri(uri);
}

+ 9
- 9
src/commands/diffWithRevisionFrom.ts View File

@ -6,7 +6,7 @@ import { GitReference, GitRevision } from '../git/models';
import { Messages } from '../messages';
import { ReferencePicker, StashPicker } from '../quickpicks';
import { Strings } from '../system';
import { basename, normalizePath, relative } from '../system/path';
import { basename } from '../system/path';
import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common';
import { DiffWithCommandArgs } from './diffWith';
@ -38,11 +38,11 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand {
args.line = editor?.selection.active.line ?? 0;
}
const path = this.container.git.getRelativePath(gitUri, gitUri.repoPath);
let ref;
let sha;
if (args?.stash) {
const fileName = normalizePath(relative(gitUri.repoPath, gitUri.fsPath));
const title = `Open Changes with Stash${Strings.pad(GlyphChars.Dot, 2, 2)}`;
const pick = await StashPicker.show(
this.container.git.getStash(gitUri.repoPath),
@ -50,7 +50,8 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand {
'Choose a stash to compare with',
{
empty: `No stashes with '${gitUri.getFormattedFileName()}' found`,
filter: c => c.files.some(f => f.fileName === fileName || f.originalFileName === fileName),
// Stashes should always come with files, so this should be fine (but protect it just in case)
filter: c => c.files?.some(f => f.path === path || f.originalPath === path) ?? true,
},
);
if (pick == null) return;
@ -82,11 +83,10 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand {
// Check to see if this file has been renamed
const files = await this.container.git.getDiffStatus(gitUri.repoPath, 'HEAD', ref, { filters: ['R', 'C'] });
if (files != null) {
const fileName = normalizePath(relative(gitUri.repoPath, gitUri.fsPath));
const rename = files.find(s => s.fileName === fileName);
if (rename?.originalFileName != null) {
renamedUri = this.container.git.getAbsoluteUri(rename.originalFileName, gitUri.repoPath);
renamedTitle = `${basename(rename.originalFileName)} (${GitRevision.shorten(ref)})`;
const rename = files.find(s => s.path === path);
if (rename?.originalPath != null) {
renamedUri = this.container.git.getAbsoluteUri(rename.originalPath, gitUri.repoPath);
renamedTitle = `${basename(rename.originalPath)} (${GitRevision.shorten(ref)})`;
}
}

+ 2
- 2
src/commands/externalDiff.ts View File

@ -38,9 +38,9 @@ export class ExternalDiffCommand extends Command {
args = { ...args };
if (isCommandContextViewNodeHasFileCommit(context)) {
const ref1 = GitRevision.isUncommitted(context.node.commit.previousFileSha)
const ref1 = GitRevision.isUncommitted(context.node.commit.previousSha)
? ''
: context.node.commit.previousFileSha;
: context.node.commit.previousSha;
const ref2 = context.node.commit.isUncommitted ? '' : context.node.commit.sha;
args.files = [

+ 40
- 9
src/commands/git/branch.ts View File

@ -60,9 +60,39 @@ interface RenameState {
type State = CreateState | DeleteState | RenameState;
type BranchStepState<T extends State> = SomeNonNullable<StepState<T>, 'subcommand'>;
type CreateStepState<T extends CreateState = CreateState> = BranchStepState<ExcludeSome<T, 'repo', string>>;
function assertStateStepCreate(state: PartialStepState<State>): asserts state is CreateStepState {
if (state.repo instanceof Repository && state.subcommand === 'create') return;
debugger;
throw new Error('Missing repository');
}
type DeleteStepState<T extends DeleteState = DeleteState> = BranchStepState<ExcludeSome<T, 'repo', string>>;
function assertStateStepDelete(state: PartialStepState<State>): asserts state is DeleteStepState {
if (state.repo instanceof Repository && state.subcommand === 'delete') return;
debugger;
throw new Error('Missing repository');
}
type RenameStepState<T extends RenameState = RenameState> = BranchStepState<ExcludeSome<T, 'repo', string>>;
function assertStateStepRename(state: PartialStepState<State>): asserts state is RenameStepState {
if (state.repo instanceof Repository && state.subcommand === 'rename') return;
debugger;
throw new Error('Missing repository');
}
function assertStateStepDeleteBranches(
state: DeleteStepState,
): asserts state is ExcludeSome<typeof state, 'references', GitBranchReference> {
if (Array.isArray(state.references)) return;
debugger;
throw new Error('Missing branches');
}
const subcommandToTitleMap = new Map<State['subcommand'], string>([
['create', 'Create'],
@ -191,17 +221,20 @@ export class BranchGitCommand extends QuickCommand {
switch (state.subcommand) {
case 'create':
yield* this.createCommandSteps(state as CreateStepState, context);
assertStateStepCreate(state);
yield* this.createCommandSteps(state, context);
// Clear any chosen name, since we are exiting this subcommand
state.name = undefined;
state.name = undefined!;
break;
case 'delete':
yield* this.deleteCommandSteps(state as DeleteStepState, context);
assertStateStepDelete(state);
yield* this.deleteCommandSteps(state, context);
break;
case 'rename':
yield* this.renameCommandSteps(state as RenameStepState, context);
assertStateStepRename(state);
yield* this.renameCommandSteps(state, context);
// Clear any chosen name, since we are exiting this subcommand
state.name = undefined;
state.name = undefined!;
break;
default:
QuickCommand.endSteps(state);
@ -359,10 +392,8 @@ export class BranchGitCommand extends QuickCommand {
state.subcommand,
);
const result = yield* this.deleteCommandConfirmStep(
state as ExcludeSome<typeof state, 'references', GitBranchReference>,
context,
);
assertStateStepDeleteBranches(state);
const result = yield* this.deleteCommandConfirmStep(state, context);
if (result === StepResult.Break) continue;
state.flags = result;

+ 17
- 7
src/commands/git/log.ts View File

@ -1,7 +1,7 @@
import { GlyphChars, quickPickTitleMaxChars } from '../../constants';
import { Container } from '../../container';
import { GitUri } from '../../git/gitUri';
import { GitLog, GitLogCommit, GitReference, Repository } from '../../git/models';
import { GitCommit, GitLog, GitReference, Repository } from '../../git/models';
import { Strings } from '../../system';
import { ViewsWithRepositoryFolders } from '../../views/viewBase';
import { GitCommandsCommand } from '../gitCommands';
@ -13,7 +13,6 @@ import {
QuickCommand,
StepGenerator,
StepResult,
StepState,
} from '../quickCommand';
interface Context {
@ -31,7 +30,16 @@ interface State {
fileName?: string;
}
type LogStepState<T extends State = State> = ExcludeSome<StepState<T>, 'repo', string>;
type RepositoryStepState<T extends State = State> = SomeNonNullable<
ExcludeSome<PartialStepState<T>, 'repo', string>,
'repo'
>;
function assertStateStepRepository(state: PartialStepState<State>): asserts state is RepositoryStepState {
if (state.repo instanceof Repository) return;
debugger;
throw new Error('Missing repository');
}
export interface LogGitCommandArgs {
readonly command: 'log';
@ -107,13 +115,15 @@ export class LogGitCommand extends QuickCommand {
}
}
assertStateStepRepository(state);
if (state.reference === 'HEAD') {
const branch = await state.repo.getBranch();
state.reference = branch;
}
if (state.counter < 2 || state.reference == null) {
const result = yield* pickBranchOrTagStep(state as LogStepState, context, {
const result = yield* pickBranchOrTagStep(state, context, {
placeholder: 'Choose a branch or tag to show its commit history',
picked: context.selectedBranchOrTag?.ref,
value: context.selectedBranchOrTag == null ? state.reference?.ref : undefined,
@ -159,7 +169,7 @@ export class LogGitCommand extends QuickCommand {
context.cache.set(ref, log);
}
const result = yield* pickCommitStep(state as LogStepState, context, {
const result = yield* pickCommitStep(state, context, {
ignoreFocusOut: true,
log: await log,
onDidLoadMore: log => context.cache.set(ref, Promise.resolve(log)),
@ -176,7 +186,7 @@ export class LogGitCommand extends QuickCommand {
state.reference = result;
}
if (!(state.reference instanceof GitLogCommit) || state.reference.isFile) {
if (!(state.reference instanceof GitCommit) || state.reference.file != null) {
state.reference = await this.container.git.getCommit(state.repo.path, state.reference.ref);
}
@ -186,7 +196,7 @@ export class LogGitCommand extends QuickCommand {
command: 'show',
state: {
repo: state.repo,
reference: state.reference as GitLogCommit,
reference: state.reference,
fileName: state.fileName,
},
},

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

@ -1,6 +1,6 @@
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitLog, GitLogCommit, Repository } from '../../git/models';
import { GitCommit, GitLog, Repository } from '../../git/models';
import { searchOperators, SearchOperators, SearchPattern } from '../../git/search';
import { ActionQuickPickItem, QuickPickItemOfT } from '../../quickpicks';
import { Strings } from '../../system';
@ -24,7 +24,7 @@ import {
interface Context {
repos: Repository[];
associatedView: ViewsWithRepositoryFolders;
commit: GitLogCommit | undefined;
commit: GitCommit | undefined;
resultsKey: string | undefined;
resultsPromise: Promise<GitLog | undefined> | undefined;
title: string;

+ 49
- 26
src/commands/git/show.ts View File

@ -1,5 +1,5 @@
import { Container } from '../../container';
import { GitAuthor, GitLogCommit, GitRevisionReference, GitStashCommit, Repository } from '../../git/models';
import { GitCommit, GitRevisionReference, GitStashCommit, Repository } from '../../git/models';
import { CommandQuickPickItem, CommitFilesQuickPickItem, GitCommandQuickPickItem } from '../../quickpicks';
import { ViewsWithRepositoryFolders } from '../../views/viewBase';
import {
@ -12,7 +12,6 @@ import {
showCommitOrStashStep,
StepGenerator,
StepResult,
StepState,
} from '../quickCommand';
interface Context {
@ -21,7 +20,7 @@ interface Context {
title: string;
}
interface State<Ref = GitRevisionReference | GitLogCommit | GitStashCommit> {
interface State<Ref = GitRevisionReference | GitCommit | GitStashCommit> {
repo: string | Repository;
reference: Ref;
fileName: string;
@ -32,7 +31,32 @@ export interface ShowGitCommandArgs {
state?: Partial<State>;
}
type ShowStepState<T extends State = State> = ExcludeSome<StepState<T>, 'repo', string>;
type RepositoryStepState<T extends State = State> = SomeNonNullable<
ExcludeSome<PartialStepState<T>, 'repo', string>,
'repo'
>;
function assertStateStepRepository(state: PartialStepState<State>): asserts state is RepositoryStepState {
if (state.repo instanceof Repository) return;
debugger;
throw new Error('Missing repository');
}
type CommitStepState = SomeNonNullable<RepositoryStepState<State<GitCommit | GitStashCommit>>, 'reference'>;
function assertsStateStepCommit(state: RepositoryStepState): asserts state is CommitStepState {
if (GitCommit.is(state.reference)) return;
debugger;
throw new Error('Missing reference');
}
type FileNameStepState = SomeNonNullable<CommitStepState, 'fileName'>;
function assertsStateStepFileName(state: CommitStepState): asserts state is FileNameStepState {
if (state.fileName) return;
debugger;
throw new Error('Missing filename');
}
export class ShowGitCommand extends QuickCommand<State> {
constructor(container: Container, args?: ShowGitCommandArgs) {
@ -105,22 +129,23 @@ export class ShowGitCommand extends QuickCommand {
}
}
assertStateStepRepository(state);
if (
state.counter < 2 ||
state.reference == null ||
!GitLogCommit.is(state.reference) ||
state.reference.isFile
!GitCommit.is(state.reference) ||
state.reference.file != null
) {
if (state.reference != null && (!GitLogCommit.is(state.reference) || state.reference.isFile)) {
if (state.reference != null && !GitCommit.is(state.reference)) {
state.reference = await this.container.git.getCommit(state.reference.repoPath, state.reference.ref);
}
if (state.counter < 2 || state.reference == null) {
const result = yield* pickCommitStep(state as ShowStepState, context, {
const result = yield* pickCommitStep(state, context, {
log: {
repoPath: state.repo.path,
authors: new Map<string, GitAuthor>(),
commits: new Map<string, GitLogCommit>(),
commits: new Map<string, GitCommit | GitStashCommit>(),
sha: undefined,
range: undefined,
count: 0,
@ -143,11 +168,14 @@ export class ShowGitCommand extends QuickCommand {
}
}
assertsStateStepCommit(state);
if (state.counter < 3) {
const result = yield* showCommitOrStashStep(
state as ShowStepState<State<GitLogCommit | GitStashCommit>>,
context,
);
if (state.reference.files == null) {
await state.reference.ensureFullDetails();
}
const result = yield* showCommitOrStashStep(state, context);
if (result === StepResult.Break) continue;
if (result instanceof GitCommandQuickPickItem) {
@ -169,13 +197,9 @@ export class ShowGitCommand extends QuickCommand {
}
if (state.counter < 4 || state.fileName == null) {
const result = yield* showCommitOrStashFilesStep(
state as ShowStepState<State<GitLogCommit | GitStashCommit>>,
context,
{
picked: state.fileName,
},
);
const result = yield* showCommitOrStashFilesStep(state, context, {
picked: state.fileName,
});
if (result === StepResult.Break) continue;
if (result instanceof CommitFilesQuickPickItem) {
@ -185,13 +209,12 @@ export class ShowGitCommand extends QuickCommand {
continue;
}
state.fileName = result.file.fileName;
state.fileName = result.file.path;
}
const result = yield* showCommitOrStashFileStep(
state as ShowStepState<State<GitLogCommit | GitStashCommit>>,
context,
);
assertsStateStepFileName(state);
const result = yield* showCommitOrStashFileStep(state, context);
if (result === StepResult.Break) continue;
if (result instanceof CommitFilesQuickPickItem) {

+ 1
- 5
src/commands/git/stash.ts View File

@ -47,7 +47,7 @@ interface DropState {
interface ListState {
subcommand: 'list';
repo: string | Repository;
reference: /*GitStashReference |*/ GitStashCommit;
reference: GitStashReference | GitStashCommit;
}
interface PopState {
@ -431,10 +431,6 @@ export class StashGitCommand extends QuickCommand {
state.reference = result;
}
// if (!(state.reference instanceof GitStashCommit)) {
// state.reference = await this.container.git.getCommit(state.repo.path, state.reference.ref);
// }
const result = yield* GitCommandsCommand.getSteps(
this.container,
{

+ 77
- 53
src/commands/gitCommands.actions.ts View File

@ -16,9 +16,9 @@ import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import {
GitBranchReference,
GitCommit,
GitContributor,
GitFile,
GitLogCommit,
GitReference,
GitRemote,
GitRevision,
@ -176,39 +176,47 @@ export namespace GitActions {
));
}
export async function copyIdToClipboard(ref: { repoPath: string; ref: string } | GitLogCommit) {
export async function copyIdToClipboard(ref: { repoPath: string; ref: string } | GitCommit) {
void (await env.clipboard.writeText(ref.ref));
}
export async function copyMessageToClipboard(ref: { repoPath: string; ref: string } | GitLogCommit) {
let message;
if (GitLogCommit.is(ref)) {
message = ref.message;
export async function copyMessageToClipboard(
ref: { repoPath: string; ref: string } | GitCommit,
): Promise<void> {
let commit;
if (GitCommit.is(ref)) {
commit = ref;
if (commit.message == null) {
await commit.ensureFullDetails();
}
} else {
const commit = await Container.instance.git.getCommit(ref.repoPath, ref.ref);
commit = await Container.instance.git.getCommit(ref.repoPath, ref.ref);
if (commit == null) return;
message = commit.message;
}
const message = commit.message ?? commit.summary;
void (await env.clipboard.writeText(message));
}
export async function openAllChanges(commit: GitLogCommit, options?: TextDocumentShowOptions): Promise<void>;
export async function openAllChanges(commit: GitCommit, options?: TextDocumentShowOptions): Promise<void>;
export async function openAllChanges(
files: GitFile[],
refs: { repoPath: string; ref1: string; ref2: string },
options?: TextDocumentShowOptions,
): Promise<void>;
export async function openAllChanges(
commitOrFiles: GitLogCommit | GitFile[],
commitOrFiles: GitCommit | GitFile[],
refsOrOptions: { repoPath: string; ref1: string; ref2: string } | TextDocumentShowOptions | undefined,
options?: TextDocumentShowOptions,
) {
let files;
let refs;
if (GitLogCommit.is(commitOrFiles)) {
files = commitOrFiles.files;
if (GitCommit.is(commitOrFiles)) {
if (commitOrFiles.files == null) {
await commitOrFiles.ensureFullDetails();
}
files = commitOrFiles.files ?? [];
refs = {
repoPath: commitOrFiles.repoPath,
ref1: commitOrFiles.previousSha != null ? commitOrFiles.previousSha : GitRevision.deletedOrMissing,
@ -237,18 +245,22 @@ export namespace GitActions {
}
}
export async function openAllChangesWithDiffTool(commit: GitLogCommit): Promise<void>;
export async function openAllChangesWithDiffTool(commit: GitCommit): Promise<void>;
export async function openAllChangesWithDiffTool(
files: GitFile[],
ref: { repoPath: string; ref: string },
): Promise<void>;
export async function openAllChangesWithDiffTool(
commitOrFiles: GitLogCommit | GitFile[],
commitOrFiles: GitCommit | GitFile[],
ref?: { repoPath: string; ref: string },
) {
let files;
if (GitLogCommit.is(commitOrFiles)) {
files = commitOrFiles.files;
if (GitCommit.is(commitOrFiles)) {
if (commitOrFiles.files == null) {
await commitOrFiles.ensureFullDetails();
}
files = commitOrFiles.files ?? [];
ref = {
repoPath: commitOrFiles.repoPath,
ref: commitOrFiles.sha,
@ -272,7 +284,7 @@ export namespace GitActions {
}
export async function openAllChangesWithWorking(
commit: GitLogCommit,
commit: GitCommit,
options?: TextDocumentShowOptions,
): Promise<void>;
export async function openAllChangesWithWorking(
@ -281,14 +293,18 @@ export namespace GitActions {
options?: TextDocumentShowOptions,
): Promise<void>;
export async function openAllChangesWithWorking(
commitOrFiles: GitLogCommit | GitFile[],
commitOrFiles: GitCommit | GitFile[],
refOrOptions: { repoPath: string; ref: string } | TextDocumentShowOptions | undefined,
options?: TextDocumentShowOptions,
) {
let files;
let ref;
if (GitLogCommit.is(commitOrFiles)) {
files = commitOrFiles.files;
if (GitCommit.is(commitOrFiles)) {
if (commitOrFiles.files == null) {
await commitOrFiles.ensureFullDetails();
}
files = commitOrFiles.files ?? [];
ref = {
repoPath: commitOrFiles.repoPath,
ref: commitOrFiles.sha,
@ -318,7 +334,7 @@ export namespace GitActions {
export async function openChanges(
file: string | GitFile,
commit: GitLogCommit,
commit: GitCommit,
options?: TextDocumentShowOptions,
): Promise<void>;
export async function openChanges(
@ -328,13 +344,13 @@ export namespace GitActions {
): Promise<void>;
export async function openChanges(
file: string | GitFile,
commitOrRefs: GitLogCommit | { repoPath: string; ref1: string; ref2: string },
commitOrRefs: GitCommit | { repoPath: string; ref1: string; ref2: string },
options?: TextDocumentShowOptions,
) {
if (typeof file === 'string') {
if (!GitLogCommit.is(commitOrRefs)) throw new Error('Invalid arguments');
if (!GitCommit.is(commitOrRefs)) throw new Error('Invalid arguments');
const f = commitOrRefs.findFile(file);
const f = await commitOrRefs.findFile(file);
if (f == null) throw new Error('Invalid arguments');
file = f;
@ -342,7 +358,7 @@ export namespace GitActions {
if (file.status === 'A') return;
const refs = GitLogCommit.is(commitOrRefs)
const refs = GitCommit.is(commitOrRefs)
? {
repoPath: commitOrRefs.repoPath,
ref1:
@ -369,7 +385,7 @@ export namespace GitActions {
export function openChangesWithDiffTool(
file: string | GitFile,
commit: GitLogCommit,
commit: GitCommit,
tool?: string,
): Promise<void>;
export function openChangesWithDiffTool(
@ -379,13 +395,13 @@ export namespace GitActions {
): Promise<void>;
export async function openChangesWithDiffTool(
file: string | GitFile,
commitOrRef: GitLogCommit | { repoPath: string; ref: string },
commitOrRef: GitCommit | { repoPath: string; ref: string },
tool?: string,
) {
if (typeof file === 'string') {
if (!GitLogCommit.is(commitOrRef)) throw new Error('Invalid arguments');
if (!GitCommit.is(commitOrRef)) throw new Error('Invalid arguments');
const f = commitOrRef.findFile(file);
const f = await commitOrRef.findFile(file);
if (f == null) throw new Error('Invalid arguments');
file = f;
@ -405,7 +421,7 @@ export namespace GitActions {
export async function openChangesWithWorking(
file: string | GitFile,
commit: GitLogCommit,
commit: GitCommit,
options?: TextDocumentShowOptions,
): Promise<void>;
export async function openChangesWithWorking(
@ -415,13 +431,13 @@ export namespace GitActions {
): Promise<void>;
export async function openChangesWithWorking(
file: string | GitFile,
commitOrRef: GitLogCommit | { repoPath: string; ref: string },
commitOrRef: GitCommit | { repoPath: string; ref: string },
options?: TextDocumentShowOptions,
) {
if (typeof file === 'string') {
if (!GitLogCommit.is(commitOrRef)) throw new Error('Invalid arguments');
if (!GitCommit.is(commitOrRef)) throw new Error('Invalid arguments');
const f = commitOrRef.files.find(f => f.fileName === file);
const f = await commitOrRef.findFile(file);
if (f == null) throw new Error('Invalid arguments');
file = f;
@ -430,7 +446,7 @@ export namespace GitActions {
if (file.status === 'D') return;
let ref;
if (GitLogCommit.is(commitOrRef)) {
if (GitCommit.is(commitOrRef)) {
ref = {
repoPath: commitOrRef.repoPath,
ref: commitOrRef.sha,
@ -457,13 +473,13 @@ export namespace GitActions {
}
export async function openDirectoryCompareWithPrevious(
ref: { repoPath: string; ref: string } | GitLogCommit,
ref: { repoPath: string; ref: string } | GitCommit,
): Promise<void> {
return openDirectoryCompare(ref.repoPath, ref.ref, `${ref.ref}^`);
}
export async function openDirectoryCompareWithWorking(
ref: { repoPath: string; ref: string } | GitLogCommit,
ref: { repoPath: string; ref: string } | GitCommit,
): Promise<void> {
return openDirectoryCompare(ref.repoPath, ref.ref, undefined);
}
@ -502,28 +518,28 @@ export namespace GitActions {
): Promise<void>;
export async function openFileAtRevision(
file: string | GitFile,
commit: GitLogCommit,
commit: GitCommit,
options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number },
): Promise<void>;
export async function openFileAtRevision(
fileOrRevisionUri: string | GitFile | Uri,
commitOrOptions?: GitLogCommit | TextDocumentShowOptions,
commitOrOptions?: GitCommit | TextDocumentShowOptions,
options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number },
): Promise<void> {
let uri;
if (fileOrRevisionUri instanceof Uri) {
if (GitLogCommit.is(commitOrOptions)) throw new Error('Invalid arguments');
if (GitCommit.is(commitOrOptions)) throw new Error('Invalid arguments');
uri = fileOrRevisionUri;
options = commitOrOptions;
} else {
if (!GitLogCommit.is(commitOrOptions)) throw new Error('Invalid arguments');
if (!GitCommit.is(commitOrOptions)) throw new Error('Invalid arguments');
const commit = commitOrOptions;
let file;
if (typeof fileOrRevisionUri === 'string') {
const f = commit.findFile(fileOrRevisionUri);
const f = await commit.findFile(fileOrRevisionUri);
if (f == null) throw new Error('Invalid arguments');
file = f;
@ -532,7 +548,7 @@ export namespace GitActions {
}
uri = Container.instance.git.getRevisionUri(
file.status === 'D' ? commit.previousFileSha : commit.sha,
file.status === 'D' ? commit.previousSha : commit.sha,
file,
commit.repoPath,
);
@ -556,16 +572,20 @@ export namespace GitActions {
}
}
export async function openFiles(commit: GitLogCommit): Promise<void>;
export async function openFiles(commit: GitCommit): Promise<void>;
export async function openFiles(files: GitFile[], repoPath: string, ref: string): Promise<void>;
export async function openFiles(
commitOrFiles: GitLogCommit | GitFile[],
commitOrFiles: GitCommit | GitFile[],
repoPath?: string,
ref?: string,
): Promise<void> {
let files;
if (GitLogCommit.is(commitOrFiles)) {
files = commitOrFiles.files;
if (GitCommit.is(commitOrFiles)) {
if (commitOrFiles.files == null) {
await commitOrFiles.ensureFullDetails();
}
files = commitOrFiles.files ?? [];
repoPath = commitOrFiles.repoPath;
ref = commitOrFiles.sha;
} else {
@ -591,7 +611,7 @@ export namespace GitActions {
findOrOpenEditors(uris);
}
export async function openFilesAtRevision(commit: GitLogCommit): Promise<void>;
export async function openFilesAtRevision(commit: GitCommit): Promise<void>;
export async function openFilesAtRevision(
files: GitFile[],
repoPath: string,
@ -599,17 +619,21 @@ export namespace GitActions {
ref2: string,
): Promise<void>;
export async function openFilesAtRevision(
commitOrFiles: GitLogCommit | GitFile[],
commitOrFiles: GitCommit | GitFile[],
repoPath?: string,
ref1?: string,
ref2?: string,
): Promise<void> {
let files;
if (GitLogCommit.is(commitOrFiles)) {
files = commitOrFiles.files;
if (GitCommit.is(commitOrFiles)) {
if (commitOrFiles.files == null) {
await commitOrFiles.ensureFullDetails();
}
files = commitOrFiles.files ?? [];
repoPath = commitOrFiles.repoPath;
ref1 = commitOrFiles.sha;
ref2 = commitOrFiles.previousFileSha;
ref2 = commitOrFiles.previousSha;
} else {
files = commitOrFiles;
}
@ -632,7 +656,7 @@ export namespace GitActions {
export async function restoreFile(file: string | GitFile, ref: GitRevisionReference) {
void (await Container.instance.git.checkout(ref.repoPath, ref.ref, {
fileName: typeof file === 'string' ? file : file.fileName,
fileName: typeof file === 'string' ? file : file.path,
}));
}

+ 4
- 6
src/commands/openFileAtRevision.ts View File

@ -72,9 +72,7 @@ export class OpenFileAtRevisionCommand extends ActiveEditorCommand {
return undefined;
}
} else if (blame?.commit.previousSha != null) {
args.revisionUri = this.container.git.getRevisionUri(
GitUri.fromCommit(blame.commit, true),
);
args.revisionUri = this.container.git.getRevisionUri(blame.commit.getGitUri(true));
} else {
void Messages.showCommitHasNoPreviousCommitWarningMessage(blame.commit);
return undefined;
@ -134,7 +132,7 @@ export class OpenFileAtRevisionCommand extends ActiveEditorCommand {
picked: gitUri.sha,
keys: ['right', 'alt+right', 'ctrl+right'],
onDidPressKey: async (key, item) => {
void (await GitActions.Commit.openFileAtRevision(item.item.uri.fsPath, item.item, {
void (await GitActions.Commit.openFileAtRevision(item.item.file!, item.item, {
annotationType: args!.annotationType,
line: args!.line,
preserveFocus: true,
@ -154,9 +152,9 @@ export class OpenFileAtRevisionCommand extends ActiveEditorCommand {
],
},
);
if (pick == null) return;
if (pick?.file == null) return;
void (await GitActions.Commit.openFileAtRevision(pick.fileName, pick, {
void (await GitActions.Commit.openFileAtRevision(pick.file, pick, {
annotationType: args.annotationType,
line: args.line,
...args.showOptions,

+ 3
- 3
src/commands/openFileAtRevisionFrom.ts View File

@ -7,7 +7,6 @@ import { GitReference } from '../git/models';
import { Messages } from '../messages';
import { ReferencePicker, StashPicker } from '../quickpicks';
import { Strings } from '../system';
import { normalizePath, relative } from '../system/path';
import { ActiveEditorCommand, command, Commands, getCommandUri } from './common';
import { GitActions } from './gitCommands';
@ -43,14 +42,15 @@ export class OpenFileAtRevisionFromCommand extends ActiveEditorCommand {
if (args.reference == null) {
if (args?.stash) {
const fileName = normalizePath(relative(gitUri.repoPath, gitUri.fsPath));
const path = this.container.git.getRelativePath(gitUri, gitUri.repoPath);
const title = `Open Changes with Stash${Strings.pad(GlyphChars.Dot, 2, 2)}`;
const pick = await StashPicker.show(
this.container.git.getStash(gitUri.repoPath),
`${title}${gitUri.getFormattedFileName({ truncateTo: quickPickTitleMaxChars - title.length })}`,
'Choose a stash to compare with',
{ filter: c => c.files.some(f => f.fileName === fileName || f.originalFileName === fileName) },
// Stashes should always come with files, so this should be fine (but protect it just in case)
{ filter: c => c.files?.some(f => f.path === path || f.originalPath === path) ?? true },
);
if (pick == null) return;

+ 1
- 1
src/commands/openOnRemote.ts View File

@ -58,7 +58,7 @@ export class OpenOnRemoteCommand extends Command {
} else if (args.resource.type === RemoteResourceType.Revision) {
const { commit, fileName } = args.resource;
if (commit != null) {
const file = commit?.files.find(f => f.fileName === fileName);
const file = await commit.findFile(fileName);
if (file?.status === 'D') {
// Resolve to the previous commit to that file
args.resource.sha = await this.container.git.resolveReference(

+ 3
- 3
src/commands/openRevisionFile.ts View File

@ -38,10 +38,10 @@ export class OpenRevisionFileCommand extends ActiveEditorCommand {
const commit = await this.container.git.getCommit(gitUri.repoPath!, gitUri.sha);
args.revisionUri =
commit != null && commit.status === 'D'
commit?.file?.status === 'D'
? this.container.git.getRevisionUri(
commit.previousSha!,
commit.previousUri.fsPath,
commit.previousSha,
commit.file.previousUri.fsPath,
commit.repoPath,
)
: this.container.git.getRevisionUri(gitUri);

+ 22
- 18
src/commands/quickCommand.steps.ts View File

@ -8,9 +8,9 @@ import {
BranchSortOptions,
GitBranch,
GitBranchReference,
GitCommit,
GitContributor,
GitLog,
GitLogCommit,
GitReference,
GitRemote,
GitRevision,
@ -830,11 +830,11 @@ export async function* pickCommitStep<
showInSideBarCommand?: CommandQuickPickItem;
showInSideBarButton?: {
button: QuickInputButton;
onDidClick: (items: Readonly<CommitQuickPickItem<GitLogCommit>[]>) => void;
onDidClick: (items: Readonly<CommitQuickPickItem<GitCommit>[]>) => void;
};
titleContext?: string;
},
): AsyncStepResultGenerator<GitLogCommit> {
): AsyncStepResultGenerator<GitCommit> {
function getItems(log: GitLog | undefined) {
return log == null
? [DirectiveQuickPickItem.create(Directive.Back, true), DirectiveQuickPickItem.create(Directive.Cancel)]
@ -909,8 +909,8 @@ export async function* pickCommitStep<
onDidClickButton: (quickpick, button) => {
if (log == null) return;
const items = quickpick.activeItems.filter<CommitQuickPickItem<GitLogCommit>>(
(i): i is CommitQuickPickItem<GitLogCommit> => !CommandQuickPickItem.is(i),
const items = quickpick.activeItems.filter<CommitQuickPickItem<GitCommit>>(
(i): i is CommitQuickPickItem<GitCommit> => !CommandQuickPickItem.is(i),
);
if (button === showInSideBar?.button) {
@ -921,8 +921,8 @@ export async function* pickCommitStep<
onDidPressKey: async (quickpick, key) => {
if (quickpick.activeItems.length === 0) return;
const items = quickpick.activeItems.filter<CommitQuickPickItem<GitLogCommit>>(
(i): i is CommitQuickPickItem<GitLogCommit> => !CommandQuickPickItem.is(i),
const items = quickpick.activeItems.filter<CommitQuickPickItem<GitCommit>>(
(i): i is CommitQuickPickItem<GitCommit> => !CommandQuickPickItem.is(i),
);
if (key === 'ctrl+right') {
@ -1367,7 +1367,7 @@ export async function* pickTagsStep<
}
export async function* showCommitOrStashStep<
State extends PartialStepState & { repo: Repository; reference: GitLogCommit | GitStashCommit },
State extends PartialStepState & { repo: Repository; reference: GitCommit | GitStashCommit },
Context extends { repos: Repository[]; title: string },
>(
state: State,
@ -1435,7 +1435,7 @@ export async function* showCommitOrStashStep<
}
async function getShowCommitOrStashStepItems<
State extends PartialStepState & { repo: Repository; reference: GitLogCommit | GitStashCommit },
State extends PartialStepState & { repo: Repository; reference: GitCommit | GitStashCommit },
>(state: State) {
const items: CommandQuickPickItem[] = [new CommitFilesQuickPickItem(state.reference)];
@ -1443,7 +1443,7 @@ async function getShowCommitOrStashStepItems<
let remotes: GitRemote<RemoteProvider>[] | undefined;
let isStash = false;
if (GitStashCommit.is(state.reference)) {
if (GitCommit.isStash(state.reference)) {
isStash = true;
items.push(new RevealInSideBarQuickPickItem(state.reference));
@ -1616,7 +1616,7 @@ async function getShowCommitOrStashStepItems<
export function* showCommitOrStashFilesStep<
State extends PartialStepState & {
repo: Repository;
reference: GitLogCommit | GitStashCommit;
reference: GitCommit | GitStashCommit;
fileName?: string | undefined;
},
Context extends { repos: Repository[]; title: string },
@ -1625,6 +1625,10 @@ export function* showCommitOrStashFilesStep<
context: Context,
options?: { picked?: string },
): StepResultGenerator<CommitFilesQuickPickItem | CommitFileQuickPickItem> {
if (state.reference.files == null) {
debugger;
}
const step: QuickPickStep<CommitFilesQuickPickItem | CommitFileQuickPickItem> = QuickCommand.createPickStep({
title: appendReposToTitle(
GitReference.toString(state.reference, {
@ -1638,9 +1642,9 @@ export function* showCommitOrStashFilesStep<
ignoreFocusOut: true,
items: [
new CommitFilesQuickPickItem(state.reference, state.fileName == null),
...state.reference.files.map(
fs => new CommitFileQuickPickItem(state.reference, fs, options?.picked === fs.fileName),
),
...(state.reference.files?.map(
fs => new CommitFileQuickPickItem(state.reference, fs, options?.picked === fs.path),
) ?? []),
],
matchOnDescription: true,
additionalButtons: [QuickCommandButtons.RevealInSideBar, QuickCommandButtons.SearchInSideBar],
@ -1692,7 +1696,7 @@ export function* showCommitOrStashFilesStep<
export async function* showCommitOrStashFileStep<
State extends PartialStepState & {
repo: Repository;
reference: GitLogCommit | GitStashCommit;
reference: GitCommit | GitStashCommit;
fileName: string;
},
Context extends { repos: Repository[]; title: string },
@ -1764,11 +1768,11 @@ export async function* showCommitOrStashFileStep<
async function getShowCommitOrStashFileStepItems<
State extends PartialStepState & {
repo: Repository;
reference: GitLogCommit | GitStashCommit;
reference: GitCommit | GitStashCommit;
fileName: string;
},
>(state: State) {
const file = state.reference.files.find(f => f.fileName === state.fileName);
const file = await state.reference.findFile(state.fileName);
if (file == null) return [];
const items: CommandQuickPickItem[] = [
@ -1778,7 +1782,7 @@ async function getShowCommitOrStashFileStepItems<
let remotes: GitRemote<RemoteProvider>[] | undefined;
let isStash = false;
if (GitStashCommit.is(state.reference)) {
if (GitCommit.is(state.reference)) {
isStash = true;
items.push(new RevealInSideBarQuickPickItem(state.reference));

+ 5
- 5
src/commands/showQuickCommit.ts View File

@ -1,7 +1,7 @@
import { TextEditor, Uri } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { GitCommit, GitCommit2, GitLog, GitLogCommit } from '../git/models';
import { GitCommit, GitLog, GitStashCommit } from '../git/models';
import { Logger } from '../logger';
import { Messages } from '../messages';
import {
@ -17,7 +17,7 @@ import { executeGitCommand, GitActions } from './gitCommands';
export interface ShowQuickCommitCommandArgs {
repoPath?: string;
sha?: string;
commit?: GitCommit2 | GitCommit | GitLogCommit;
commit?: GitCommit | GitStashCommit;
repoLog?: GitLog;
revealInView?: boolean;
}
@ -72,11 +72,11 @@ export class ShowQuickCommitCommand extends ActiveEditorCachedCommand {
args.sha = args.commit.sha;
}
gitUri = args.commit.toGitUri();
gitUri = args.commit.getGitUri();
repoPath = args.commit.repoPath;
if (uri == null) {
uri = args.commit.uri;
uri = args.commit.file?.uri;
}
}
@ -149,7 +149,7 @@ export class ShowQuickCommitCommand extends ActiveEditorCachedCommand {
command: 'show',
state: {
repo: repoPath,
reference: args.commit as GitLogCommit,
reference: args.commit,
},
}));
} catch (ex) {

+ 4
- 4
src/commands/showQuickCommitFile.ts View File

@ -1,7 +1,7 @@
import { TextEditor, Uri, window } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { GitCommit2, GitLog, GitLogCommit } from '../git/models';
import { GitCommit, GitLog, GitStashCommit } from '../git/models';
import { Logger } from '../logger';
import { Messages } from '../messages';
import {
@ -16,7 +16,7 @@ import { executeGitCommand } from './gitCommands';
export interface ShowQuickCommitFileCommandArgs {
sha?: string;
commit?: GitCommit2 | GitLogCommit;
commit?: GitCommit | GitStashCommit;
fileLog?: GitLog;
revisionUri?: string;
}
@ -132,7 +132,7 @@ export class ShowQuickCommitFileCommand extends ActiveEditorCachedCommand {
}
const path = args.commit?.file?.path ?? gitUri.fsPath;
if (GitCommit2.is(args.commit)) {
if (GitCommit.is(args.commit)) {
if (args.commit.files == null) {
await args.commit.ensureFullDetails();
}
@ -182,7 +182,7 @@ export class ShowQuickCommitFileCommand extends ActiveEditorCachedCommand {
// [args.commit.toGitUri(), args],
// );
// const pick = await CommitFileQuickPick.show(args.commit as GitLogCommit, uri, {
// const pick = await CommitFileQuickPick.show(args.commit as GitCommit, uri, {
// goBackCommand: args.goBackCommand,
// currentCommand: currentCommand,
// fileLog: args.fileLog,

+ 5
- 2
src/commands/stashApply.ts View File

@ -14,7 +14,7 @@ import {
export interface StashApplyCommandArgs {
deleteAfter?: boolean;
repoPath?: string;
stashItem?: GitStashReference & { message: string };
stashItem?: GitStashReference & { message: string | undefined };
goBackCommand?: CommandQuickPickItem;
}
@ -25,8 +25,11 @@ export class StashApplyCommand extends Command {
super(Commands.StashApply);
}
protected override preExecute(context: CommandContext, args?: StashApplyCommandArgs) {
protected override async preExecute(context: CommandContext, args?: StashApplyCommandArgs) {
if (isCommandContextViewNodeHasCommit<GitStashCommit>(context)) {
if (context.node.commit.message == null) {
await context.node.commit.ensureFullDetails();
}
args = { ...args, stashItem: context.node.commit };
} else if (isCommandContextViewNodeHasRepository(context)) {
args = { ...args, repoPath: context.node.repo.path };

+ 2
- 9
src/env/node/git/git.ts View File

@ -682,7 +682,6 @@ export class Git {
limit,
merges,
ordering,
reverse,
similarityThreshold,
since,
}: {
@ -692,7 +691,6 @@ export class Git {
limit?: number;
merges?: boolean;
ordering?: string | null;
reverse?: boolean;
similarityThreshold?: number | null;
since?: string;
},
@ -717,7 +715,7 @@ export class Git {
params.push(`--${ordering}-order`);
}
if (limit && !reverse) {
if (limit) {
params.push(`-n${limit + 1}`);
}
@ -741,12 +739,7 @@ export class Git {
}
if (ref && !GitRevision.isUncommittedStaged(ref)) {
// If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking
if (reverse) {
params.push('--reverse', '--ancestry-path', `${ref}..HEAD`);
} else {
params.push(ref);
}
params.push(ref);
}
return this.git<string>(

+ 66
- 83
src/env/node/git/localGitProvider.ts View File

@ -40,14 +40,13 @@ import { GitProviderService } from '../../../git/gitProviderService';
import { encodeGitLensRevisionUriAuthority, GitUri } from '../../../git/gitUri';
import {
BranchSortOptions,
GitAuthor,
GitBlame,
GitBlameAuthor,
GitBlameLine,
GitBlameLines,
GitBranch,
GitBranchReference,
GitCommit2,
GitCommitType,
GitCommit,
GitContributor,
GitDiff,
GitDiffFilter,
@ -55,7 +54,6 @@ import {
GitDiffShortStat,
GitFile,
GitLog,
GitLogCommit,
GitMergeStatus,
GitRebaseStatus,
GitReference,
@ -85,6 +83,7 @@ import {
GitStatusParser,
GitTagParser,
GitTreeParser,
LogType,
} from '../../../git/parsers';
import { RemoteProviderFactory, RemoteProviders } from '../../../git/remotes/factory';
import { RemoteProvider, RichRemoteProvider } from '../../../git/remotes/provider';
@ -952,14 +951,14 @@ export class LocalGitProvider implements GitProvider, Disposable {
return emptyPromise as Promise<GitBlame>;
}
const [file, root] = paths;
const [relativePath, root] = paths;
try {
const data = await this.git.blame(root, file, uri.sha, {
const data = await this.git.blame(root, relativePath, uri.sha, {
args: this.container.config.advanced.blame.customArguments,
ignoreWhitespace: this.container.config.blame.ignoreWhitespace,
});
const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root));
const blame = GitBlameParser.parse(data, root, await this.getCurrentUser(root));
return blame;
} catch (ex) {
// Trap and cache expected blame errors
@ -1032,15 +1031,15 @@ export class LocalGitProvider implements GitProvider, Disposable {
return emptyPromise as Promise<GitBlame>;
}
const [file, root] = paths;
const [relativePath, root] = paths;
try {
const data = await this.git.blame__contents(root, file, contents, {
const data = await this.git.blame__contents(root, relativePath, contents, {
args: this.container.config.advanced.blame.customArguments,
correlationKey: `:${key}`,
ignoreWhitespace: this.container.config.blame.ignoreWhitespace,
});
const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root));
const blame = GitBlameParser.parse(data, root, await this.getCurrentUser(root));
return blame;
} catch (ex) {
// Trap and cache expected blame errors
@ -1091,16 +1090,16 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
const lineToBlame = editorLine + 1;
const [path, root] = splitPath(uri.fsPath, uri.repoPath);
const [relativePath, root] = splitPath(uri.fsPath, uri.repoPath);
try {
const data = await this.git.blame(root, path, uri.sha, {
const data = await this.git.blame(root, relativePath, uri.sha, {
args: this.container.config.advanced.blame.customArguments,
ignoreWhitespace: this.container.config.blame.ignoreWhitespace,
startLine: lineToBlame,
endLine: lineToBlame,
});
const blame = GitBlameParser.parse(data, root, path, await this.getCurrentUser(root));
const blame = GitBlameParser.parse(data, root, await this.getCurrentUser(root));
if (blame == null) return undefined;
return {
@ -1142,16 +1141,16 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
const lineToBlame = editorLine + 1;
const [path, root] = splitPath(uri.fsPath, uri.repoPath);
const [relativePath, root] = splitPath(uri.fsPath, uri.repoPath);
try {
const data = await this.git.blame__contents(root, path, contents, {
const data = await this.git.blame__contents(root, relativePath, contents, {
args: this.container.config.advanced.blame.customArguments,
ignoreWhitespace: this.container.config.blame.ignoreWhitespace,
startLine: lineToBlame,
endLine: lineToBlame,
});
const blame = GitBlameParser.parse(data, root, path, await this.getCurrentUser(root));
const blame = GitBlameParser.parse(data, root, await this.getCurrentUser(root));
if (blame == null) return undefined;
return {
@ -1195,13 +1194,13 @@ export class LocalGitProvider implements GitProvider, Disposable {
const startLine = range.start.line + 1;
const endLine = range.end.line + 1;
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitCommit2>();
const authors = new Map<string, GitBlameAuthor>();
const commits = new Map<string, GitCommit>();
for (const c of blame.commits.values()) {
if (!shas.has(c.sha)) continue;
const commit = c.with({
lines: c.lines.filter(l => l.line >= startLine && l.line <= endLine),
lines: c.lines.filter(l => l.to.line >= startLine && l.to.line <= endLine),
});
commits.set(c.sha, commit);
@ -1357,7 +1356,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
@log()
async getCommit(repoPath: string, ref: string): Promise<GitLogCommit | undefined> {
async getCommit(repoPath: string, ref: string): Promise<GitCommit | undefined> {
const log = await this.getLog(repoPath, { limit: 2, ref: ref });
if (log == null) return undefined;
@ -1385,8 +1384,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
async getCommitForFile(
repoPath: string | undefined,
uri: Uri,
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range; reverse?: boolean },
): Promise<GitLogCommit | undefined> {
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range },
): Promise<GitCommit | undefined> {
const cc = Logger.getCorrelationContext();
const [path, root] = splitPath(uri.fsPath, repoPath);
@ -1396,7 +1395,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
limit: 2,
ref: options?.ref,
range: options?.range,
reverse: options?.reverse,
});
if (log == null) return undefined;
@ -1896,7 +1894,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
since?: string;
},
): Promise<GitLog | undefined> {
@ -1905,22 +1902,52 @@ export class LocalGitProvider implements GitProvider, Disposable {
const limit = options?.limit ?? this.container.config.advanced.maxListItems ?? 0;
try {
// const parser = GitLogParser.defaultParser;
const data = await this.git.log(repoPath, options?.ref, {
...options,
// args: parser.arguments,
limit: limit,
merges: options?.merges == null ? true : options.merges,
ordering: options?.ordering ?? this.container.config.advanced.commitOrdering,
similarityThreshold: this.container.config.advanced.similarityThreshold,
});
// const commits = [];
// const entries = parser.parse(data);
// for (const entry of entries) {
// commits.push(
// new GitCommit2(
// repoPath,
// entry.sha,
// new GitCommitIdentity(
// entry.author,
// entry.authorEmail,
// new Date((entry.authorDate as any) * 1000),
// ),
// new GitCommitIdentity(
// entry.committer,
// entry.committerEmail,
// new Date((entry.committerDate as any) * 1000),
// ),
// entry.message.split('\n', 1)[0],
// entry.parents.split(' '),
// entry.message,
// entry.files.map(f => new GitFileChange(repoPath, f.path, f.status as any, f.originalPath)),
// [],
// ),
// );
// }
const log = GitLogParser.parse(
data,
GitCommitType.Log,
LogType.Log,
repoPath,
undefined,
options?.ref,
await this.getCurrentUser(repoPath),
limit,
options?.reverse ?? false,
false,
undefined,
);
@ -1949,7 +1976,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
since?: string;
},
): Promise<Set<string> | undefined> {
@ -1965,7 +1991,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
argsOrFormat: parser.arguments,
limit: limit,
merges: options?.merges == null ? true : options.merges,
reverse: options?.reverse,
similarityThreshold: this.container.config.advanced.similarityThreshold,
since: options?.since,
ordering: options?.ordering ?? this.container.config.advanced.commitOrdering,
@ -1988,7 +2013,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
},
): (limit: number | { until: string } | undefined) => Promise<GitLog> {
return async (limit: number | { until: string } | undefined) => {
@ -2022,22 +2046,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
// If we can't find any more, assume we have everything
if (moreLog == null) return { ...log, hasMore: false };
// Merge authors
const authors = new Map([...log.authors]);
for (const [key, addAuthor] of moreLog.authors) {
const author = authors.get(key);
if (author == null) {
authors.set(key, addAuthor);
} else {
author.lineCount += addAuthor.lineCount;
}
}
const commits = new Map([...log.commits, ...moreLog.commits]);
const mergedLog: GitLog = {
repoPath: log.repoPath,
authors: authors,
commits: commits,
sha: log.sha,
range: undefined,
@ -2149,7 +2161,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
});
const log = GitLogParser.parse(
data,
GitCommitType.Log,
LogType.Log,
repoPath,
undefined,
undefined,
@ -2191,22 +2203,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
return { ...log, hasMore: false };
}
// Merge authors
const authors = new Map([...log.authors]);
for (const [key, addAuthor] of moreLog.authors) {
const author = authors.get(key);
if (author == null) {
authors.set(key, addAuthor);
} else {
author.lineCount += addAuthor.lineCount;
}
}
const commits = new Map([...log.commits, ...moreLog.commits]);
const mergedLog: GitLog = {
repoPath: log.repoPath,
authors: authors,
commits: commits,
sha: log.sha,
range: log.range,
@ -2312,9 +2312,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
// Create a copy of the log starting at the requested commit
let skip = true;
let i = 0;
const authors = new Map<string, GitAuthor>();
const commits = new Map(
filterMapIterable<[string, GitLogCommit], [string, GitLogCommit]>(
filterMapIterable<[string, GitCommit], [string, GitCommit]>(
log.commits.entries(),
([ref, c]) => {
if (skip) {
@ -2327,7 +2326,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
return undefined;
}
authors.set(c.author.name, log.authors.get(c.author.name)!);
return [ref, c];
},
),
@ -2339,7 +2337,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
limit: options.limit,
count: commits.size,
commits: commits,
authors: authors,
query: (limit: number | undefined) =>
this.getLogForFile(repoPath, path, { ...opts, limit: limit }),
};
@ -2373,7 +2370,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
private async getLogForFileCore(
repoPath: string | undefined,
fileName: string,
path: string,
{
ref,
range,
@ -2394,9 +2391,9 @@ export class LocalGitProvider implements GitProvider, Disposable {
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitLog | undefined> {
const paths = await this.isTracked(fileName, repoPath, ref);
const paths = await this.isTracked(path, repoPath, ref);
if (paths == null) {
Logger.log(cc, `Skipping blame; '${fileName}' is not tracked`);
Logger.log(cc, `Skipping blame; '${path}' is not tracked`);
return emptyPromise as Promise<GitLog>;
}
@ -2417,22 +2414,22 @@ export class LocalGitProvider implements GitProvider, Disposable {
const log = GitLogParser.parse(
data,
// If this is the log of a folder, parse it as a normal log rather than a file log
isFolderGlob(file) ? GitCommitType.Log : GitCommitType.LogFile,
isFolderGlob(file) ? LogType.Log : LogType.LogFile,
root,
file,
ref,
await this.getCurrentUser(root),
options.limit,
options.reverse!,
options.reverse ?? false,
range,
);
if (log != null) {
const opts = { ...options, ref: ref, range: range };
log.query = (limit: number | undefined) =>
this.getLogForFile(repoPath, fileName, { ...opts, limit: limit });
this.getLogForFile(repoPath, path, { ...opts, limit: limit });
if (log.hasMore) {
log.more = this.getLogForFileMoreFn(log, fileName, opts);
log.more = this.getLogForFileMoreFn(log, path, opts);
}
}
@ -2489,22 +2486,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
// If we can't find any more, assume we have everything
if (moreLog == null) return { ...log, hasMore: false };
// Merge authors
const authors = new Map([...log.authors]);
for (const [key, addAuthor] of moreLog.authors) {
const author = authors.get(key);
if (author == null) {
authors.set(key, addAuthor);
} else {
author.lineCount += addAuthor.lineCount;
}
}
const commits = new Map([...log.commits, ...moreLog.commits]);
const mergedLog: GitLog = {
repoPath: log.repoPath,
authors: authors,
commits: commits,
sha: log.sha,
range: log.range,
@ -2518,11 +2503,9 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (options.renames) {
const renamed = find(
moreLog.commits.values(),
c => Boolean(c.originalFileName) && c.originalFileName !== fileName,
c => Boolean(c.file?.originalPath) && c.file?.originalPath !== fileName,
);
if (renamed != null) {
fileName = renamed.originalFileName!;
}
fileName = renamed?.file?.originalPath ?? fileName;
}
mergedLog.more = this.getLogForFileMoreFn(mergedLog, fileName, options);
@ -2907,7 +2890,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
ref = blameLine.commit.sha;
path = blameLine.commit.file?.path ?? blameLine.commit.file?.originalPath ?? path;
uri = this.getAbsoluteUri(path, repoPath);
editorLine = blameLine.line.originalLine - 1;
editorLine = blameLine.line.from.line - 1;
if (skip === 0 && blameLine.commit.file?.previousSha) {
previous = GitUri.fromFile(path, repoPath, blameLine.commit.file.previousSha);
@ -2936,7 +2919,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
ref = blameLine.commit.sha;
path = blameLine.commit.file?.path ?? blameLine.commit.file?.originalPath ?? path;
uri = this.getAbsoluteUri(path, repoPath);
editorLine = blameLine.line.originalLine - 1;
editorLine = blameLine.line.from.line - 1;
if (skip === 0 && blameLine.commit.file?.previousSha) {
previous = GitUri.fromFile(path, repoPath, blameLine.commit.file.previousSha);

+ 67
- 81
src/git/formatters/commitFormatter.ts View File

@ -15,19 +15,12 @@ import { DateStyle, FileAnnotationType } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { emojify } from '../../emojis';
import { Iterables, Strings } from '../../system';
import { join, map } from '../../system/iterable';
import { PromiseCancelledError } from '../../system/promise';
import { escapeMarkdown, getSuperscript, TokenOptions } from '../../system/string';
import { ContactPresence } from '../../vsls/vsls';
import type { GitUri } from '../gitUri';
import {
GitCommit,
GitCommit2,
GitLogCommit,
GitRemote,
GitRevision,
IssueOrPullRequest,
PullRequest,
} from '../models';
import { GitCommit, GitRemote, GitRevision, IssueOrPullRequest, PullRequest } from '../models';
import { RemoteProvider } from '../remotes/provider';
import { FormatOptions, Formatter } from './formatter';
@ -50,40 +43,40 @@ export interface CommitFormatOptions extends FormatOptions {
unpublished?: boolean;
tokenOptions?: {
ago?: Strings.TokenOptions;
agoOrDate?: Strings.TokenOptions;
agoOrDateShort?: Strings.TokenOptions;
author?: Strings.TokenOptions;
authorAgo?: Strings.TokenOptions;
authorAgoOrDate?: Strings.TokenOptions;
authorAgoOrDateShort?: Strings.TokenOptions;
authorDate?: Strings.TokenOptions;
authorNotYou?: Strings.TokenOptions;
avatar?: Strings.TokenOptions;
changes?: Strings.TokenOptions;
changesDetail?: Strings.TokenOptions;
changesShort?: Strings.TokenOptions;
commands?: Strings.TokenOptions;
committerAgo?: Strings.TokenOptions;
committerAgoOrDate?: Strings.TokenOptions;
committerAgoOrDateShort?: Strings.TokenOptions;
committerDate?: Strings.TokenOptions;
date?: Strings.TokenOptions;
email?: Strings.TokenOptions;
footnotes?: Strings.TokenOptions;
id?: Strings.TokenOptions;
message?: Strings.TokenOptions;
pullRequest?: Strings.TokenOptions;
pullRequestAgo?: Strings.TokenOptions;
pullRequestAgoOrDate?: Strings.TokenOptions;
pullRequestDate?: Strings.TokenOptions;
pullRequestState?: Strings.TokenOptions;
sha?: Strings.TokenOptions;
tips?: Strings.TokenOptions;
ago?: TokenOptions;
agoOrDate?: TokenOptions;
agoOrDateShort?: TokenOptions;
author?: TokenOptions;
authorAgo?: TokenOptions;
authorAgoOrDate?: TokenOptions;
authorAgoOrDateShort?: TokenOptions;
authorDate?: TokenOptions;
authorNotYou?: TokenOptions;
avatar?: TokenOptions;
changes?: TokenOptions;
changesDetail?: TokenOptions;
changesShort?: TokenOptions;
commands?: TokenOptions;
committerAgo?: TokenOptions;
committerAgoOrDate?: TokenOptions;
committerAgoOrDateShort?: TokenOptions;
committerDate?: TokenOptions;
date?: TokenOptions;
email?: TokenOptions;
footnotes?: TokenOptions;
id?: TokenOptions;
message?: TokenOptions;
pullRequest?: TokenOptions;
pullRequestAgo?: TokenOptions;
pullRequestAgoOrDate?: TokenOptions;
pullRequestDate?: TokenOptions;
pullRequestState?: TokenOptions;
sha?: TokenOptions;
tips?: TokenOptions;
};
}
export class CommitFormatter extends Formatter<GitCommit | GitCommit2, CommitFormatOptions> {
export class CommitFormatter extends Formatter<GitCommit, CommitFormatOptions> {
private get _authorDate() {
return this._item.author.formatDate(this._options.dateFormat);
}
@ -249,21 +242,21 @@ export class CommitFormatter extends Formatter
get changes(): string {
return this._padOrTruncate(
GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus() : '',
GitCommit.is(this._item) ? this._item.formatStats() : '',
this._options.tokenOptions.changes,
);
}
get changesDetail(): string {
return this._padOrTruncate(
GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus({ expand: true, separator: ', ' }) : '',
GitCommit.is(this._item) ? this._item.formatStats({ expand: true, separator: ', ' }) : '',
this._options.tokenOptions.changesDetail,
);
}
get changesShort(): string {
return this._padOrTruncate(
GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus({ compact: true, separator: '' }) : '',
GitCommit.is(this._item) ? this._item.formatStats({ compact: true, separator: '' }) : '',
this._options.tokenOptions.changesShort,
);
}
@ -325,10 +318,10 @@ export class CommitFormatter extends Formatter
this._options.editor?.line,
)} "Open Changes with Previous Revision")`;
if (this._item.previousSha != null) {
if (this._item.previousSha != null && this._item.file?.previousPath != null) {
const uri = Container.instance.git.getRevisionUri(
this._item.previousSha,
this._item.previousUri.fsPath,
this._item.file.previousPath,
this._item.repoPath,
);
commands += ` &nbsp;&nbsp;[$(versions)](${OpenFileAtRevisionCommand.getMarkdownCommandArgs(
@ -357,7 +350,7 @@ export class CommitFormatter extends Formatter
pullRequest: { id: pr.id, url: pr.url },
})} "Open Pull Request \\#${pr.id}${
Container.instance.actionRunners.count('openPullRequest') == 1 ? ` on ${pr.provider.name}` : '...'
}\n${GlyphChars.Dash.repeat(2)}\n${Strings.escapeMarkdown(pr.title).replace(/"/g, '\\"')}\n${
}\n${GlyphChars.Dash.repeat(2)}\n${escapeMarkdown(pr.title).replace(/"/g, '\\"')}\n${
pr.state
}, ${pr.formatDateFromNow()}")`;
} else if (pr instanceof PromiseCancelledError) {
@ -396,9 +389,14 @@ export class CommitFormatter extends Formatter
})} "Show Team Actions")`;
}
commands += `${separator}[$(ellipsis)](${ShowQuickCommitFileCommand.getMarkdownCommandArgs({
revisionUri: Container.instance.git.getRevisionUri(this._item.toGitUri()).toString(true),
})} "Show More Actions")`;
const gitUri = this._item.getGitUri();
commands += `${separator}[$(ellipsis)](${ShowQuickCommitFileCommand.getMarkdownCommandArgs(
gitUri != null
? {
revisionUri: Container.instance.git.getRevisionUri(gitUri).toString(true),
}
: { commit: this._item },
)} "Show More Actions")`;
return this._padOrTruncate(commands, this._options.tokenOptions.commands);
}
@ -442,9 +440,9 @@ export class CommitFormatter extends Formatter
return this._padOrTruncate(
this._options.footnotes == null || this._options.footnotes.size === 0
? ''
: Iterables.join(
Iterables.map(this._options.footnotes, ([i, footnote]) =>
this._options.markdown ? footnote : `${Strings.getSuperscript(i)} ${footnote}`,
: join(
map(this._options.footnotes, ([i, footnote]) =>
this._options.markdown ? footnote : `${getSuperscript(i)} ${footnote}`,
),
this._options.markdown ? '\\\n' : '\n',
),
@ -463,7 +461,7 @@ export class CommitFormatter extends Formatter
get message(): string {
if (this._item.isUncommitted) {
const confliced = this._item.hasConflicts;
const confliced = this._item.file?.hasConflicts ?? false;
const staged =
this._item.isUncommittedStaged ||
(this._options.previousLineDiffUris?.current?.isUncommittedStaged ?? false);
@ -476,20 +474,16 @@ export class CommitFormatter extends Formatter
);
}
let message = this._item.message ?? this._item.summary;
if (this._options.messageTruncateAtNewLine) {
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
}
let message = this._options.messageTruncateAtNewLine
? this._item.summary
: this._item.message ?? this._item.summary;
message = emojify(message);
message = this._padOrTruncate(message, this._options.tokenOptions.message);
if (this._options.messageAutolinks) {
message = Container.instance.autolinks.linkify(
this._options.markdown ? Strings.escapeMarkdown(message, { quoted: true }) : message,
this._options.markdown ? escapeMarkdown(message, { quoted: true }) : message,
this._options.markdown ?? false,
this._options.remotes,
this._options.autolinkedIssuesOrPullRequests,
@ -511,7 +505,7 @@ export class CommitFormatter extends Formatter
let text;
if (PullRequest.is(pr)) {
if (this._options.markdown) {
const prTitle = Strings.escapeMarkdown(pr.title).replace(/"/g, '\\"').trim();
const prTitle = escapeMarkdown(pr.title).replace(/"/g, '\\"').trim();
text = `PR [**#${pr.id}**](${getMarkdownActionCommand<OpenPullRequestActionContext>('openPullRequest', {
repoPath: this._item.repoPath,
@ -519,7 +513,7 @@ export class CommitFormatter extends Formatter
pullRequest: { id: pr.id, url: pr.url },
})} "Open Pull Request \\#${pr.id}${
Container.instance.actionRunners.count('openPullRequest') == 1 ? ` on ${pr.provider.name}` : '...'
}\n${GlyphChars.Dash.repeat(2)}\n${Strings.escapeMarkdown(pr.title).replace(/"/g, '\\"')}\n${
}\n${GlyphChars.Dash.repeat(2)}\n${escapeMarkdown(pr.title).replace(/"/g, '\\"')}\n${
pr.state
}, ${pr.formatDateFromNow()}")`;
@ -541,7 +535,7 @@ export class CommitFormatter extends Formatter
`PR #${pr.id}: ${pr.title} ${GlyphChars.Dot} ${pr.state}, ${pr.formatDateFromNow()}`,
);
text = `PR #${pr.id}${Strings.getSuperscript(index)}`;
text = `PR #${pr.id}${getSuperscript(index)}`;
} else {
text = `PR #${pr.id}`;
}
@ -591,16 +585,16 @@ export class CommitFormatter extends Formatter
return this._padOrTruncate(branchAndTagTips ?? '', this._options.tokenOptions.tips);
}
static fromTemplate(template: string, commit: GitCommit | GitCommit2, dateFormat: string | null): string;
static fromTemplate(template: string, commit: GitCommit | GitCommit2, options?: CommitFormatOptions): string;
static fromTemplate(template: string, commit: GitCommit, dateFormat: string | null): string;
static fromTemplate(template: string, commit: GitCommit, options?: CommitFormatOptions): string;
static fromTemplate(
template: string,
commit: GitCommit | GitCommit2,
commit: GitCommit,
dateFormatOrOptions?: string | null | CommitFormatOptions,
): string;
static fromTemplate(
template: string,
commit: GitCommit | GitCommit2,
commit: GitCommit,
dateFormatOrOptions?: string | null | CommitFormatOptions,
): string {
if (dateFormatOrOptions == null || typeof dateFormatOrOptions === 'string') {
@ -623,24 +617,16 @@ export class CommitFormatter extends Formatter
return super.fromTemplateCore(this, template, commit, dateFormatOrOptions);
}
static fromTemplateAsync(template: string, commit: GitCommit, dateFormat: string | null): Promise<string>;
static fromTemplateAsync(template: string, commit: GitCommit, options?: CommitFormatOptions): Promise<string>;
static fromTemplateAsync(
template: string,
commit: GitCommit | GitCommit2,
dateFormat: string | null,
): Promise<string>;
static fromTemplateAsync(
template: string,
commit: GitCommit | GitCommit2,
options?: CommitFormatOptions,
): Promise<string>;
static fromTemplateAsync(
template: string,
commit: GitCommit | GitCommit2,
commit: GitCommit,
dateFormatOrOptions?: string | null | CommitFormatOptions,
): Promise<string>;
static fromTemplateAsync(
template: string,
commit: GitCommit | GitCommit2,
commit: GitCommit,
dateFormatOrOptions?: string | null | CommitFormatOptions,
): Promise<string> {
if (CommitFormatter.has(template, 'footnotes')) {

+ 38
- 16
src/git/formatters/statusFormatter.ts View File

@ -1,20 +1,23 @@
import { GlyphChars } from '../../constants';
import { Strings } from '../../system';
import { basename } from '../../system/path';
import { GitFile, GitFileWithCommit } from '../models/file';
import { TokenOptions } from '../../system/string';
import { GitFile, GitFileChange, GitFileWithCommit } from '../models/file';
import { FormatOptions, Formatter } from './formatter';
export interface StatusFormatOptions extends FormatOptions {
relativePath?: string;
tokenOptions?: {
directory?: Strings.TokenOptions;
file?: Strings.TokenOptions;
filePath?: Strings.TokenOptions;
originalPath?: Strings.TokenOptions;
path?: Strings.TokenOptions;
status?: Strings.TokenOptions;
working?: Strings.TokenOptions;
directory?: TokenOptions;
file?: TokenOptions;
filePath?: TokenOptions;
originalPath?: TokenOptions;
path?: TokenOptions;
status?: TokenOptions;
working?: TokenOptions;
changes?: TokenOptions;
changesDetail?: TokenOptions;
changesShort?: TokenOptions;
};
}
@ -25,7 +28,7 @@ export class StatusFileFormatter extends Formatter
}
get file() {
const file = basename(this._item.fileName);
const file = basename(this._item.path);
return this._padOrTruncate(file, this._options.tokenOptions.file);
}
@ -61,14 +64,12 @@ export class StatusFileFormatter extends Formatter
}
get working() {
const statusFile = (this._item as GitFileWithCommit).commit?.files?.[0] ?? this._item;
let icon;
if (statusFile.workingTreeStatus !== undefined && statusFile.indexStatus !== undefined) {
let icon = '';
if (this._item.workingTreeStatus != null && this._item.indexStatus != null) {
icon = `${GlyphChars.Pencil}${GlyphChars.Space}${GlyphChars.SpaceThinnest}${GlyphChars.Check}`;
} else if (statusFile.workingTreeStatus !== undefined) {
} else if (this._item.workingTreeStatus != null) {
icon = `${GlyphChars.Pencil}${GlyphChars.SpaceThin}${GlyphChars.SpaceThinnest}${GlyphChars.EnDash}${GlyphChars.Space}`;
} else if (statusFile.indexStatus !== undefined) {
} else if (this._item.indexStatus != null) {
icon = `${GlyphChars.Space}${GlyphChars.EnDash}${GlyphChars.Space.repeat(2)}${GlyphChars.Check}`;
} else {
icon = '';
@ -76,6 +77,27 @@ export class StatusFileFormatter extends Formatter
return this._padOrTruncate(icon, this._options.tokenOptions.working);
}
get changes(): string {
return this._padOrTruncate(
GitFileChange.is(this._item) ? this._item.formatStats() : '',
this._options.tokenOptions.changes,
);
}
get changesDetail(): string {
return this._padOrTruncate(
GitFileChange.is(this._item) ? this._item.formatStats({ expand: true, separator: ', ' }) : '',
this._options.tokenOptions.changesDetail,
);
}
get changesShort(): string {
return this._padOrTruncate(
GitFileChange.is(this._item) ? this._item.formatStats({ compact: true, separator: '' }) : '',
this._options.tokenOptions.changesShort,
);
}
static fromTemplate(template: string, file: GitFile | GitFileWithCommit, dateFormat: string | null): string;
static fromTemplate(template: string, file: GitFile | GitFileWithCommit, options?: StatusFormatOptions): string;
static fromTemplate(

+ 3
- 6
src/git/gitProvider.ts View File

@ -8,6 +8,7 @@ import {
GitBlameLines,
GitBranch,
GitBranchReference,
GitCommit,
GitContributor,
GitDiff,
GitDiffFilter,
@ -15,7 +16,6 @@ import {
GitDiffShortStat,
GitFile,
GitLog,
GitLogCommit,
GitMergeStatus,
GitRebaseStatus,
GitReflog,
@ -174,7 +174,7 @@ export interface GitProvider extends Disposable {
},
): Promise<PagedResult<GitBranch>>;
getChangedFilesCount(repoPath: string, ref?: string): Promise<GitDiffShortStat | undefined>;
getCommit(repoPath: string, ref: string): Promise<GitLogCommit | undefined>;
getCommit(repoPath: string, ref: string): Promise<GitCommit | undefined>;
getCommitBranches(
repoPath: string,
ref: string,
@ -188,9 +188,8 @@ export interface GitProvider extends Disposable {
ref?: string | undefined;
firstIfNotFound?: boolean | undefined;
range?: Range | undefined;
reverse?: boolean | undefined;
},
): Promise<GitLogCommit | undefined>;
): Promise<GitCommit | undefined>;
getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise<string | undefined>;
getContributors(
repoPath: string,
@ -243,7 +242,6 @@ export interface GitProvider extends Disposable {
merges?: boolean | undefined;
ordering?: string | null | undefined;
ref?: string | undefined;
reverse?: boolean | undefined;
since?: string | undefined;
},
): Promise<GitLog | undefined>;
@ -256,7 +254,6 @@ export interface GitProvider extends Disposable {
merges?: boolean | undefined;
ordering?: string | null | undefined;
ref?: string | undefined;
reverse?: boolean | undefined;
since?: string | undefined;
},
): Promise<Set<string> | undefined>;

+ 5
- 7
src/git/gitProviderService.ts View File

@ -47,6 +47,7 @@ import {
GitBlameLines,
GitBranch,
GitBranchReference,
GitCommit,
GitContributor,
GitDiff,
GitDiffFilter,
@ -54,7 +55,6 @@ import {
GitDiffShortStat,
GitFile,
GitLog,
GitLogCommit,
GitMergeStatus,
GitRebaseStatus,
GitReference,
@ -681,7 +681,7 @@ export class GitProviderService implements Disposable {
if (typeof pathOrFile === 'string') {
path = pathOrFile;
} else {
path = pathOrFile!.originalFileName ?? pathOrFile!.fileName;
path = pathOrFile!.originalPath ?? pathOrFile!.path;
}
} else {
ref = refOrUri.sha;
@ -1044,7 +1044,7 @@ export class GitProviderService implements Disposable {
}
@log()
getCommit(repoPath: string | Uri, ref: string): Promise<GitLogCommit | undefined> {
getCommit(repoPath: string | Uri, ref: string): Promise<GitCommit | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommit(path, ref);
}
@ -1078,8 +1078,8 @@ export class GitProviderService implements Disposable {
async getCommitForFile(
repoPath: string | Uri | undefined,
uri: Uri,
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range; reverse?: boolean },
): Promise<GitLogCommit | undefined> {
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range },
): Promise<GitCommit | undefined> {
if (repoPath == null) return undefined;
const { provider, path } = this.getProvider(repoPath);
@ -1195,7 +1195,6 @@ export class GitProviderService implements Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
since?: string;
},
): Promise<GitLog | undefined> {
@ -1212,7 +1211,6 @@ export class GitProviderService implements Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
since?: string;
},
): Promise<Set<string> | undefined> {

+ 3
- 14
src/git/gitUri.ts View File

@ -10,7 +10,7 @@ import { memoize } from '../system/decorators/memoize';
import { basename, dirname, isAbsolute, normalizePath, relative } from '../system/path';
import { CharCode, truncateLeft, truncateMiddle } from '../system/string';
import { RevisionUriData } from './gitProvider';
import { GitCommit, GitCommit2, GitFile, GitRevision } from './models';
import { GitFile, GitRevision } from './models';
export interface GitCommitish {
fileName?: string;
@ -230,23 +230,12 @@ export class GitUri extends (Uri as any as UriEx) {
return Container.instance.git.getAbsoluteUri(this.fsPath, this.repoPath);
}
static fromCommit(commit: GitCommit | GitCommit2, previous: boolean = false) {
if (!previous) return new GitUri(commit.uri, commit);
return new GitUri(commit.previousUri, {
repoPath: commit.repoPath,
sha: commit.previousSha,
});
}
static fromFile(file: string | GitFile, repoPath: string, ref?: string, original: boolean = false): GitUri {
const uri = Container.instance.git.getAbsoluteUri(
typeof file === 'string' ? file : (original && file.originalFileName) || file.fileName,
typeof file === 'string' ? file : (original && file.originalPath) || file.path,
repoPath,
);
return ref == null || ref.length === 0
? new GitUri(uri, repoPath)
: new GitUri(uri, { repoPath: repoPath, sha: ref });
return !ref ? new GitUri(uri, repoPath) : new GitUri(uri, { repoPath: repoPath, sha: ref });
}
static fromRepoPath(repoPath: string, ref?: string) {

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

@ -8,7 +8,6 @@ export * from './models/diff';
export * from './models/file';
export * from './models/issue';
export * from './models/log';
export * from './models/logCommit';
export * from './models/merge';
export * from './models/pullRequest';
export * from './models/reference';
@ -19,7 +18,6 @@ export * from './models/remoteProvider';
export * from './models/repository';
export * from './models/shortlog';
export * from './models/stash';
export * from './models/stashCommit';
export * from './models/status';
export * from './models/tag';
export * from './models/tree';

+ 12
- 7
src/git/models/blame.ts View File

@ -1,15 +1,20 @@
import { GitAuthor, GitCommit2, GitCommitLine } from './commit';
import { GitCommit, GitCommitLine } from './commit';
export interface GitBlame {
readonly repoPath: string;
readonly authors: Map<string, GitAuthor>;
readonly commits: Map<string, GitCommit2>;
readonly authors: Map<string, GitBlameAuthor>;
readonly commits: Map<string, GitCommit>;
readonly lines: GitCommitLine[];
}
export interface GitBlameAuthor {
name: string;
lineCount: number;
}
export interface GitBlameLine {
readonly author?: GitAuthor;
readonly commit: GitCommit2;
readonly author?: GitBlameAuthor;
readonly commit: GitCommit;
readonly line: GitCommitLine;
}
@ -18,7 +23,7 @@ export interface GitBlameLines extends GitBlame {
}
export interface GitBlameCommitLines {
readonly author: GitAuthor;
readonly commit: GitCommit2;
readonly author: GitBlameAuthor;
readonly commit: GitCommit;
readonly lines: GitCommitLine[];
}

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

@ -5,8 +5,8 @@ import { formatDate, fromNow } from '../../system/date';
import { debug } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { sortCompare } from '../../system/string';
import { GitBranchReference, GitReference, GitRevision } from '../models';
import { PullRequest, PullRequestState } from './pullRequest';
import { GitBranchReference, GitReference, GitRevision } from './reference';
import { GitRemote } from './remote';
import { GitStatus } from './status';

+ 345
- 359
src/git/models/commit.ts View File

@ -1,45 +1,24 @@
import { Uri } from 'vscode';
import { getAvatarUri } from '../../avatars';
import { configuration, DateSource, DateStyle, GravatarDefaultStyle } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { formatDate, fromNow } from '../../system/date';
import { gate } from '../../system/decorators/gate';
import { memoize } from '../../system/decorators/memoize';
import { CommitFormatter } from '../formatters';
import { cancellable } from '../../system/promise';
import { pad, pluralize } from '../../system/string';
import { GitUri } from '../gitUri';
import {
GitFileIndexStatus,
GitFileStatus,
GitReference,
GitRevision,
GitRevisionReference,
PullRequest,
} from '../models';
export interface GitAuthor {
name: string;
lineCount: number;
}
export interface GitCommitLine {
sha: string;
previousSha?: string;
line: number;
originalLine: number;
code?: string;
}
import { GitFile, GitFileChange, GitFileWorkingTreeStatus } from './file';
import { PullRequest } from './pullRequest';
import { GitReference, GitRevision, GitRevisionReference, GitStashReference } from './reference';
export const enum GitCommitType {
Blame = 'blame',
Log = 'log',
LogFile = 'logFile',
Stash = 'stash',
StashFile = 'stashFile',
}
const stashNumberRegex = /stash@{(\d+)}/;
export const CommitDateFormatting = {
dateFormat: undefined! as string | null,
dateSource: undefined! as DateSource,
dateStyle: undefined! as DateStyle,
dateFormat: null as string | null,
dateSource: DateSource.Authored,
dateStyle: DateStyle.Relative,
reset: () => {
CommitDateFormatting.dateFormat = configuration.get('defaultDateFormat');
@ -49,7 +28,7 @@ export const CommitDateFormatting = {
};
export const CommitShaFormatting = {
length: undefined! as number,
length: 7,
reset: () => {
// Don't allow shas to be shortened to less than 5 characters
@ -57,115 +36,92 @@ export const CommitShaFormatting = {
},
};
export class GitCommitIdentity {
constructor(
public readonly name: string,
public readonly email: string | undefined,
public readonly date: Date,
private readonly avatarUrl?: string | undefined,
) {}
@memoize<GitCommitIdentity['formatDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatDate(format?: string | null) {
if (format == null) {
format = 'MMMM Do, YYYY h:mma';
}
return formatDate(this.date, format);
}
fromNow(short?: boolean) {
return fromNow(this.date, short);
}
getAvatarUri(
commit: GitCommit2,
options?: { defaultStyle?: GravatarDefaultStyle; size?: number },
): Uri | Promise<Uri> {
if (this.avatarUrl != null) Uri.parse(this.avatarUrl);
return getAvatarUri(this.email, commit, options);
}
}
export class GitFileChange {
constructor(
public readonly repoPath: string,
public readonly path: string,
public readonly status: GitFileStatus,
public readonly originalPath?: string | undefined,
public readonly previousSha?: string | undefined,
) {}
@memoize()
get uri(): Uri {
return Container.instance.git.getAbsoluteUri(this.path, this.repoPath);
}
@memoize()
get originalUri(): Uri | undefined {
return this.originalPath ? Container.instance.git.getAbsoluteUri(this.originalPath, this.repoPath) : undefined;
}
@memoize()
get previousUri(): Uri {
return Container.instance.git.getAbsoluteUri(this.originalPath || this.path, this.repoPath);
export class GitCommit implements GitRevisionReference {
static is(commit: any): commit is GitCommit {
return commit instanceof GitCommit;
}
@memoize()
getWorkingUri(): Promise<Uri | undefined> {
return Container.instance.git.getWorkingUri(this.repoPath, this.uri);
static isStash(commit: any): commit is GitStashCommit {
return commit instanceof GitCommit && commit.refType === 'stash' && Boolean(commit.stashName);
}
}
const stashNumberRegex = /stash@{(\d+)}/;
export class GitCommit2 implements GitRevisionReference {
static is(commit: any): commit is GitCommit2 {
return commit instanceof GitCommit2;
static isOfRefType(commit: GitReference | undefined): boolean {
return commit?.refType === 'revision' || commit?.refType === 'stash';
}
static hasFullDetails(commit: GitCommit2): commit is GitCommit2 & SomeNonNullable<GitCommit2, 'message' | 'files'> {
return commit.message != null && commit.files != null && commit.parents.length !== 0;
static hasFullDetails(commit: GitCommit): commit is GitCommit & SomeNonNullable<GitCommit, 'message' | 'files'> {
return (
commit.message != null &&
commit.files != null &&
commit.parents.length !== 0 &&
(!commit.stashName || commit._stashUntrackedFilesLoaded)
);
}
static isOfRefType(commit: GitReference | undefined) {
return commit?.refType === 'revision' || commit?.refType === 'stash';
}
private _stashUntrackedFilesLoaded = false;
private _recomputeStats = false;
readonly lines: GitCommitLine[];
readonly ref: string;
readonly refType: GitRevisionReference['refType'];
readonly shortSha: string;
readonly stashName: string | undefined;
readonly stashNumber: number | undefined;
// TODO@eamodio rename to stashNumber
readonly number: string | undefined;
constructor(
public readonly repoPath: string,
public readonly sha: string,
public readonly author: GitCommitIdentity,
public readonly committer: GitCommitIdentity,
public readonly summary: string,
summary: string,
public readonly parents: string[],
message?: string | undefined,
files?: GitFileChange | GitFileChange[] | undefined,
files?: GitFileChange | GitFileChange[] | { file?: GitFileChange; files?: GitFileChange[] } | undefined,
stats?: GitCommitStats,
lines?: GitCommitLine | GitCommitLine[] | undefined,
stashName?: string | undefined,
) {
this.ref = this.sha;
this.refType = 'revision';
this.refType = stashName ? 'stash' : 'revision';
this.shortSha = this.sha.substring(0, CommitShaFormatting.length);
// Add an ellipsis to the summary if there is or might be more message
if (message != null) {
this._message = message;
if (this.summary !== message) {
this._summary = `${summary} ${GlyphChars.Ellipsis}`;
} else {
this._summary = summary;
}
} else {
this._summary = `${summary} ${GlyphChars.Ellipsis}`;
}
// Keep this above files, because we check this in computing the stats
if (stats != null) {
this._stats = stats;
}
if (files != null) {
if (Array.isArray(files)) {
this._files = files;
} else {
} else if (files instanceof GitFileChange) {
this._file = files;
if (GitRevision.isUncommitted(sha, true)) {
this._files = [files];
}
} else {
if (files.file != null) {
this._file = files.file;
}
if (files.files != null) {
this._files = files.files;
}
}
this._recomputeStats = true;
}
if (lines != null) {
@ -180,7 +136,7 @@ export class GitCommit2 implements GitRevisionReference {
if (stashName) {
this.stashName = stashName || undefined;
this.stashNumber = Number(stashNumberRegex.exec(stashName)?.[1]);
this.number = stashNumberRegex.exec(stashName)?.[1];
}
}
@ -194,7 +150,7 @@ export class GitCommit2 implements GitRevisionReference {
}
private _files: GitFileChange[] | undefined;
get files(): GitFileChange[] | undefined {
get files(): readonly GitFileChange[] | undefined {
return this._files;
}
@ -204,20 +160,6 @@ export class GitCommit2 implements GitRevisionReference {
: this.formatDateFromNow();
}
get hasConflicts(): boolean | undefined {
return undefined;
// return this._files?.some(f => f.conflictStatus != null);
}
private _message: string | undefined;
get message(): string | undefined {
return this._message;
}
get name() {
return this.stashName ? this.stashName : this.shortSha;
}
@memoize()
get isUncommitted(): boolean {
return GitRevision.isUncommitted(this.sha);
@ -228,40 +170,123 @@ export class GitCommit2 implements GitRevisionReference {
return GitRevision.isUncommittedStaged(this.sha);
}
/** @deprecated use `file.uri` */
get uri(): Uri /*| undefined*/ {
return this.file?.uri ?? Container.instance.git.getAbsoluteUri(this.repoPath, this.repoPath);
private _message: string | undefined;
get message(): string | undefined {
return this._message;
}
/** @deprecated use `file.originalUri` */
get originalUri(): Uri | undefined {
return this.file?.originalUri;
get name(): string {
return this.stashName ? this.stashName : this.shortSha;
}
/** @deprecated use `file.getWorkingUri` */
getWorkingUri(): Promise<Uri | undefined> {
return Promise.resolve(this.file?.getWorkingUri());
private _stats: GitCommitStats | undefined;
get stats(): GitCommitStats | undefined {
if (this._recomputeStats) {
this.computeFileStats();
}
return this._stats;
}
/** @deprecated use `file.previousUri` */
get previousUri(): Uri /*| undefined*/ {
return this.file?.previousUri ?? Container.instance.git.getAbsoluteUri(this.repoPath, this.repoPath);
private _summary: string;
get summary(): string {
return this._summary;
}
/** @deprecated use `file.previousSha` */
get previousSha(): string | undefined {
return this.file?.previousSha;
get previousSha(): string {
return this.file?.previousSha ?? this.parents[0] ?? `${this.sha}^`;
}
@gate()
async ensureFullDetails(): Promise<void> {
if (this.isUncommitted || GitCommit2.hasFullDetails(this)) return;
if (this.isUncommitted || GitCommit.hasFullDetails(this)) return;
const [commitResult, untrackedResult] = await Promise.allSettled([
Container.instance.git.getCommit(this.repoPath, this.sha),
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
// See https://stackoverflow.com/questions/12681529/
this.stashName ? Container.instance.git.getCommit(this.repoPath, `${this.stashName}^3`) : undefined,
]);
if (commitResult.status !== 'fulfilled' || commitResult.value == null) return;
let commit = commitResult.value;
this.parents.push(...(commit.parents ?? []));
this._summary = commit.summary;
this._message = commit.message;
this._files = commit.files as GitFileChange[];
if (untrackedResult.status === 'fulfilled' && untrackedResult.value != null) {
this._stashUntrackedFilesLoaded = true;
commit = untrackedResult.value;
if (commit?.files != null && commit.files.length !== 0) {
// Since these files are untracked -- make them look that way
const files = commit.files.map(
f => new GitFileChange(this.repoPath, f.path, GitFileWorkingTreeStatus.Untracked, f.originalPath),
);
if (this._files == null) {
this._files = files;
} else {
this._files.push(...files);
}
}
}
const commit = await Container.instance.git.getCommit(this.repoPath, this.sha);
if (commit == null) return;
this._recomputeStats = true;
}
this.parents.push(...(commit.parentShas ?? []));
this._message = commit.message;
this._files = commit.files.map(f => new GitFileChange(this.repoPath, f.fileName, f.status, f.originalFileName));
private computeFileStats(): void {
if (!this._recomputeStats || this._files == null) return;
this._recomputeStats = false;
const changedFiles = {
added: 0,
deleted: 0,
changed: 0,
};
let additions = 0;
let deletions = 0;
for (const file of this._files) {
if (file.stats != null) {
additions += file.stats.additions;
deletions += file.stats.deletions;
}
switch (file.status) {
case 'A':
case '?':
changedFiles.added++;
break;
case 'D':
changedFiles.deleted++;
break;
default:
changedFiles.changed++;
break;
}
}
if (this._stats != null) {
if (additions === 0 && this._stats.additions !== 0) {
additions = this._stats.additions;
}
if (deletions === 0 && this._stats.deletions !== 0) {
deletions = this._stats.deletions;
}
}
this._stats = { ...this._stats, changedFiles: changedFiles, additions: additions, deletions: deletions };
}
async findFile(path: string): Promise<GitFileChange | undefined> {
if (this._files == null) {
await this.ensureFullDetails();
if (this._files == null) return undefined;
}
path = Container.instance.git.getRelativePath(path, this.repoPath);
return this._files.find(f => f.path === path);
}
formatDate(format?: string | null) {
@ -276,19 +301,127 @@ export class GitCommit2 implements GitRevisionReference {
: this.author.fromNow(short);
}
// TODO@eamodio deal with memoization, since we don't want the timeout to apply
@memoize()
formatStats(options?: {
compact?: boolean;
empty?: string;
expand?: boolean;
prefix?: string;
sectionSeparator?: string;
separator?: string;
suffix?: string;
}): string {
const stats = this.stats;
if (stats == null) return options?.empty ?? '';
const { changedFiles, additions, deletions } = stats;
if (changedFiles <= 0 && additions <= 0 && deletions <= 0) return options?.empty ?? '';
const {
compact = false,
expand = false,
prefix = '',
sectionSeparator = ` ${pad(GlyphChars.Dot, 1, 1, GlyphChars.Space)} `,
separator = ' ',
suffix = '',
} = options ?? {};
let status = prefix;
if (typeof changedFiles === 'number') {
if (changedFiles) {
status += expand ? `${pluralize('file', changedFiles)} changed` : `~${changedFiles}`;
}
} else {
const { added, changed, deleted } = changedFiles;
if (added) {
status += expand ? `${pluralize('file', added)} added` : `+${added}`;
} else if (!expand && !compact) {
status += '+0';
}
if (changed) {
status += `${added ? separator : ''}${
expand ? `${pluralize('file', changed)} changed` : `~${changed}`
}`;
} else if (!expand && !compact) {
status += '~0';
}
if (deleted) {
status += `${changed | additions ? separator : ''}${
expand ? `${pluralize('file', deleted)} deleted` : `-${deleted}`
}`;
} else if (!expand && !compact) {
status += '-0';
}
}
if (expand) {
if (additions) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
status += `${changedFiles ? sectionSeparator : ''}${pluralize('addition', additions)}`;
}
if (deletions) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
status += `${changedFiles || additions ? separator : ''}${pluralize('deletion', deletions)}`;
}
}
status += suffix;
return status;
}
private _pullRequest: Promise<PullRequest | undefined> | undefined;
async getAssociatedPullRequest(options?: { timeout?: number }): Promise<PullRequest | undefined> {
const remote = await Container.instance.git.getRichRemoteProvider(this.repoPath);
if (remote?.provider == null) return undefined;
if (this._pullRequest == null) {
async function getCore(this: GitCommit): Promise<PullRequest | undefined> {
const remote = await Container.instance.git.getRichRemoteProvider(this.repoPath);
if (remote?.provider == null) return undefined;
return Container.instance.git.getPullRequestForCommit(this.ref, remote, options);
return Container.instance.git.getPullRequestForCommit(this.ref, remote, options);
}
this._pullRequest = getCore.call(this);
}
return cancellable(this._pullRequest, options?.timeout);
}
getAvatarUri(options?: { defaultStyle?: GravatarDefaultStyle; size?: number }): Uri | Promise<Uri> {
return this.author.getAvatarUri(this, options);
}
async getCommitForFile(file: string | GitFile): Promise<GitCommit | undefined> {
const path = typeof file === 'string' ? Container.instance.git.getRelativePath(file, this.repoPath) : file.path;
const foundFile = await this.findFile(path);
if (foundFile == null) return undefined;
const commit = this.with({ files: { file: foundFile } });
return commit;
}
async getCommitsForFiles(): Promise<GitCommit[]> {
if (this._files == null) {
await this.ensureFullDetails();
if (this._files == null) return [];
}
const commits = this._files.map(f => this.with({ files: { file: f } }));
return commits;
}
@memoize()
getGitUri(previous: boolean = false): GitUri {
const uri = this._file?.uri ?? Container.instance.git.getAbsoluteUri(this.repoPath, this.repoPath);
if (!previous) return new GitUri(uri, this);
return new GitUri(this._file?.previousUri ?? uri, {
repoPath: this.repoPath,
sha: this.previousSha,
});
}
@memoize<GitCommit['getPreviousLineDiffUris']>((u, e, r) => `${u.toString()}|${e}|${r ?? ''}`)
getPreviousLineDiffUris(uri: Uri, editorLine: number, ref: string | undefined) {
return this.file?.path
@ -296,18 +429,32 @@ export class GitCommit2 implements GitRevisionReference {
: Promise.resolve(undefined);
}
@memoize()
toGitUri(previous: boolean = false): GitUri {
return GitUri.fromCommit(this, previous);
}
with(changes: {
sha?: string;
parents?: string[];
files?: GitFileChange | GitFileChange[] | null;
files?: { file?: GitFileChange | null; files?: GitFileChange[] | null } | null;
lines?: GitCommitLine[];
}): GitCommit2 {
return new GitCommit2(
}): GitCommit {
let files;
if (changes.files != null) {
files = { file: this._file, files: this._files };
if (changes.files.file != null) {
files.file = changes.files.file;
} else if (changes.files.file === null) {
files.file = undefined;
}
if (changes.files.files != null) {
files.files = changes.files.files;
} else if (changes.files.files === null) {
files.files = undefined;
}
} else if (changes.files === null) {
files = undefined;
}
return new GitCommit(
this.repoPath,
changes.sha ?? this.sha,
this.author,
@ -315,7 +462,8 @@ export class GitCommit2 implements GitRevisionReference {
this.summary,
this.getChangedValue(changes.parents, this.parents) ?? [],
this.message,
this.getChangedValue(changes.files, this.files),
files,
this.stats,
this.getChangedValue(changes.lines, this.lines),
this.stashName,
);
@ -327,220 +475,58 @@ export class GitCommit2 implements GitRevisionReference {
}
}
export abstract class GitCommit implements GitRevisionReference {
get file() {
return this.fileName
? new GitFileChange(this.repoPath, this.fileName, GitFileIndexStatus.Modified, this.originalFileName)
: undefined;
}
get parents(): string[] {
return this.previousSha ? [this.previousSha] : [];
}
get summary(): string {
return this.message.split('\n', 1)[0];
}
get author(): GitCommitIdentity {
return new GitCommitIdentity(this.authorName, this.authorEmail, this.authorDate);
}
get committer(): GitCommitIdentity {
return new GitCommitIdentity('', '', this.committerDate);
}
static is(commit: any): commit is GitCommit {
return commit instanceof GitCommit;
}
static isOfRefType(commit: GitReference | undefined) {
return commit?.refType === 'revision' || commit?.refType === 'stash';
}
readonly refType: GitRevisionReference['refType'] = 'revision';
export class GitCommitIdentity {
constructor(
public readonly type: GitCommitType,
public readonly repoPath: string,
public readonly sha: string,
public readonly authorName: string,
public readonly authorEmail: string | undefined,
public readonly authorDate: Date,
public readonly committerDate: Date,
public readonly message: string,
fileName: string,
public readonly originalFileName: string | undefined,
public previousSha: string | undefined,
public previousFileName: string | undefined,
) {
this._fileName = fileName || '';
}
get hasConflicts(): boolean {
return false;
}
get ref() {
return this.sha;
}
get name() {
return this.shortSha;
}
private readonly _fileName: string;
get fileName() {
// If we aren't a single-file commit, return an empty file name (makes it default to the repoPath)
return this.isFile ? this._fileName : '';
}
get date(): Date {
return CommitDateFormatting.dateSource === DateSource.Committed ? this.committerDate : this.authorDate;
}
get formattedDate(): string {
return CommitDateFormatting.dateStyle === DateStyle.Absolute
? this.formatDate(CommitDateFormatting.dateFormat)
: this.formatDateFromNow();
}
@memoize()
get shortSha() {
return GitRevision.shorten(this.sha);
}
get isFile() {
return (
this.type === GitCommitType.Blame ||
this.type === GitCommitType.LogFile ||
this.type === GitCommitType.StashFile
);
}
get isStash() {
return this.type === GitCommitType.Stash || this.type === GitCommitType.StashFile;
}
@memoize()
get isUncommitted(): boolean {
return GitRevision.isUncommitted(this.sha);
}
@memoize()
get isUncommittedStaged(): boolean {
return GitRevision.isUncommittedStaged(this.sha);
}
@memoize()
get originalUri(): Uri {
return this.originalFileName
? Container.instance.git.getAbsoluteUri(this.originalFileName, this.repoPath)
: this.uri;
}
get previousFileSha(): string {
return `${this.sha}^`;
}
get previousShortSha() {
return this.previousSha && GitRevision.shorten(this.previousSha);
}
get previousUri(): Uri {
return this.previousFileName
? Container.instance.git.getAbsoluteUri(this.previousFileName, this.repoPath)
: this.uri;
}
@memoize()
get uri(): Uri {
return Container.instance.git.getAbsoluteUri(this.fileName, this.repoPath);
}
@memoize()
async getAssociatedPullRequest(options?: { timeout?: number }): Promise<PullRequest | undefined> {
const remote = await Container.instance.git.getRichRemoteProvider(this.repoPath);
if (remote?.provider == null) return undefined;
return Container.instance.git.getPullRequestForCommit(this.ref, remote, options);
}
@memoize<GitCommit['getPreviousLineDiffUris']>(
(uri, editorLine, ref) => `${uri.toString(true)}|${editorLine ?? ''}|${ref ?? ''}`,
)
getPreviousLineDiffUris(uri: Uri, editorLine: number, ref: string | undefined) {
if (!this.isFile) return Promise.resolve(undefined);
return Container.instance.git.getPreviousLineDiffUris(this.repoPath, uri, editorLine, ref);
}
@memoize()
getWorkingUri(): Promise<Uri | undefined> {
if (!this.isFile) return Promise.resolve(undefined);
return Container.instance.git.getWorkingUri(this.repoPath, this.uri);
}
@memoize<GitCommit['formatAuthorDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatAuthorDate(format?: string | null) {
return formatDate(this.authorDate, format ?? 'MMMM Do, YYYY h:mma');
}
formatAuthorDateFromNow(short?: boolean) {
return fromNow(this.authorDate, short);
}
@memoize<GitCommit['formatCommitterDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatCommitterDate(format?: string | null) {
return formatDate(this.committerDate, format ?? 'MMMM Do, YYYY h:mma');
}
formatCommitterDateFromNow(short?: boolean) {
return fromNow(this.committerDate, short);
}
public readonly name: string,
public readonly email: string | undefined,
public readonly date: Date,
private readonly avatarUrl?: string | undefined,
) {}
@memoize<GitCommitIdentity['formatDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatDate(format?: string | null) {
return CommitDateFormatting.dateSource === DateSource.Committed
? this.formatCommitterDate(format)
: this.formatAuthorDate(format);
}
if (format == null) {
format = 'MMMM Do, YYYY h:mma';
}
formatDateFromNow(short?: boolean) {
return CommitDateFormatting.dateSource === DateSource.Committed
? this.formatCommitterDateFromNow(short)
: this.formatAuthorDateFromNow(short);
return formatDate(this.date, format);
}
getFormattedPath(options: { relativeTo?: string; suffix?: string; truncateTo?: number } = {}): string {
return GitUri.getFormattedPath(this.fileName, options);
fromNow(short?: boolean) {
return fromNow(this.date, short);
}
getAvatarUri(options?: { defaultStyle?: GravatarDefaultStyle; size?: number }): Uri | Promise<Uri> {
return getAvatarUri(this.authorEmail, this, options);
}
getAvatarUri(
commit: GitCommit,
options?: { defaultStyle?: GravatarDefaultStyle; size?: number },
): Uri | Promise<Uri> {
if (this.avatarUrl != null) Uri.parse(this.avatarUrl);
@memoize()
getShortMessage() {
return CommitFormatter.fromTemplate(`\${message}`, this, { messageTruncateAtNewLine: true });
return getAvatarUri(this.email, commit, options);
}
}
@memoize()
toGitUri(previous: boolean = false): GitUri {
return GitUri.fromCommit(this, previous);
}
export interface GitCommitLine {
sha: string;
previousSha?: string | undefined;
from: {
line: number;
count: number;
};
to: {
line: number;
count: number;
};
}
abstract with(changes: {
type?: GitCommitType;
sha?: string;
fileName?: string;
originalFileName?: string | null;
previousFileName?: string | null;
previousSha?: string | null;
}): GitCommit;
export interface GitCommitStats {
readonly additions: number;
readonly deletions: number;
readonly changedFiles: number | { added: number; deleted: number; changed: number };
}
protected getChangedValue<T>(change: T | null | undefined, original: T | undefined): T | undefined {
if (change === undefined) return original;
return change !== null ? change : undefined;
}
export interface GitStashCommit extends GitCommit {
readonly refType: GitStashReference['refType'];
readonly stashName: string;
readonly number: string;
}

+ 132
- 14
src/git/models/file.ts View File

@ -1,7 +1,10 @@
import { Uri } from 'vscode';
import { GlyphChars } from '../../constants';
import { Strings } from '../../system';
import { Container } from '../../container';
import { memoize } from '../../system/decorators/memoize';
import { pad, pluralize } from '../../system/string';
import { GitUri } from '../gitUri';
import { GitLogCommit } from './logCommit';
import { GitCommit } from './commit';
export declare type GitFileStatus = GitFileConflictStatus | GitFileIndexStatus | GitFileWorkingTreeStatus;
@ -16,17 +19,21 @@ export const enum GitFileConflictStatus {
}
export const enum GitFileIndexStatus {
Modified = 'M',
Added = 'A',
Deleted = 'D',
Modified = 'M',
Renamed = 'R',
Copied = 'C',
Unchanged = '.',
Untracked = '?',
Ignored = '!',
UpdatedButUnmerged = 'U',
}
export const enum GitFileWorkingTreeStatus {
Modified = 'M',
Added = 'A',
Deleted = 'D',
Modified = 'M',
Untracked = '?',
Ignored = '!',
}
@ -37,12 +44,12 @@ export interface GitFile {
readonly conflictStatus?: GitFileConflictStatus;
readonly indexStatus?: GitFileIndexStatus;
readonly workingTreeStatus?: GitFileWorkingTreeStatus;
readonly fileName: string;
readonly originalFileName?: string;
readonly path: string;
readonly originalPath?: string;
}
export interface GitFileWithCommit extends GitFile {
readonly commit: GitLogCommit;
readonly commit: GitCommit;
}
export namespace GitFile {
@ -62,9 +69,9 @@ export namespace GitFile {
includeOriginal: boolean = false,
relativeTo?: string,
): string {
const directory = GitUri.getDirectory(file.fileName, relativeTo);
return includeOriginal && (file.status === 'R' || file.status === 'C') && file.originalFileName
? `${directory} ${Strings.pad(GlyphChars.ArrowLeft, 1, 1)} ${file.originalFileName}`
const directory = GitUri.getDirectory(file.path, relativeTo);
return includeOriginal && (file.status === 'R' || file.status === 'C') && file.originalPath
? `${directory} ${pad(GlyphChars.ArrowLeft, 1, 1)} ${file.originalPath}`
: directory;
}
@ -72,20 +79,21 @@ export namespace GitFile {
file: GitFile,
options: { relativeTo?: string; suffix?: string; truncateTo?: number } = {},
): string {
return GitUri.getFormattedPath(file.fileName, options);
return GitUri.getFormattedPath(file.path, options);
}
export function getOriginalRelativePath(file: GitFile, relativeTo?: string): string {
if (file.originalFileName == null || file.originalFileName.length === 0) return '';
if (file.originalPath == null || file.originalPath.length === 0) return '';
return GitUri.relativeTo(file.originalFileName, relativeTo);
return GitUri.relativeTo(file.originalPath, relativeTo);
}
export function getRelativePath(file: GitFile, relativeTo?: string): string {
return GitUri.relativeTo(file.fileName, relativeTo);
return GitUri.relativeTo(file.path, relativeTo);
}
const statusIconsMap = {
'.': undefined,
'!': 'icon-status-ignored.svg',
'?': 'icon-status-untracked.svg',
A: 'icon-status-added.svg',
@ -101,6 +109,7 @@ export namespace GitFile {
UD: 'icon-status-conflict.svg',
UU: 'icon-status-conflict.svg',
T: 'icon-status-modified.svg',
U: 'icon-status-modified.svg',
};
export function getStatusIcon(status: GitFileStatus): string {
@ -108,6 +117,7 @@ export namespace GitFile {
}
const statusCodiconsMap = {
'.': undefined,
'!': '$(diff-ignored)',
'?': '$(diff-added)',
A: '$(diff-added)',
@ -123,6 +133,7 @@ export namespace GitFile {
UD: '$(warning)',
UU: '$(warning)',
T: '$(diff-modified)',
U: '$(diff-modified)',
};
export function getStatusCodicon(status: GitFileStatus, missing: string = GlyphChars.Space.repeat(4)): string {
@ -130,6 +141,7 @@ export namespace GitFile {
}
const statusTextMap = {
'.': 'Unchanged',
'!': 'Ignored',
'?': 'Untracked',
A: 'Added',
@ -145,9 +157,115 @@ export namespace GitFile {
UD: 'Conflict',
UU: 'Conflict',
T: 'Modified',
U: 'Updated but Unmerged',
};
export function getStatusText(status: GitFileStatus): string {
return statusTextMap[status] ?? 'Unknown';
}
}
export interface GitFileChangeStats {
additions: number;
deletions: number;
changes: number;
}
export class GitFileChange {
static is(file: any): file is GitFileChange {
return file instanceof GitFileChange;
}
constructor(
public readonly repoPath: string,
public readonly path: string,
public readonly status: GitFileStatus,
public readonly originalPath?: string | undefined,
public readonly previousSha?: string | undefined,
public readonly stats?: GitFileChangeStats | undefined,
) {}
get hasConflicts() {
switch (this.status) {
case GitFileConflictStatus.AddedByThem:
case GitFileConflictStatus.AddedByUs:
case GitFileConflictStatus.AddedByBoth:
case GitFileConflictStatus.DeletedByThem:
case GitFileConflictStatus.DeletedByUs:
case GitFileConflictStatus.DeletedByBoth:
case GitFileConflictStatus.ModifiedByBoth:
return true;
default:
return false;
}
}
get previousPath(): string {
return this.originalPath || this.path;
}
@memoize()
get uri(): Uri {
return Container.instance.git.getAbsoluteUri(this.path, this.repoPath);
}
@memoize()
get originalUri(): Uri | undefined {
return this.originalPath ? Container.instance.git.getAbsoluteUri(this.originalPath, this.repoPath) : undefined;
}
@memoize()
get previousUri(): Uri {
return Container.instance.git.getAbsoluteUri(this.previousPath, this.repoPath);
}
@memoize()
getWorkingUri(): Promise<Uri | undefined> {
return Container.instance.git.getWorkingUri(this.repoPath, this.uri);
}
formatStats(options?: {
compact?: boolean;
empty?: string;
expand?: boolean;
prefix?: string;
separator?: string;
suffix?: string;
}): string {
if (this.stats == null) return options?.empty ?? '';
const { /*changes,*/ additions, deletions } = this.stats;
if (/*changes < 0 && */ additions < 0 && deletions < 0) return options?.empty ?? '';
const { compact = false, expand = false, prefix = '', separator = ' ', suffix = '' } = options ?? {};
let status = prefix;
if (additions) {
status += expand ? `${pluralize('line', additions)} added` : `+${additions}`;
} else if (!expand && !compact) {
status += '+0';
}
// if (changes) {
// status += `${additions ? separator : ''}${
// expand ? `${pluralize('line', changes)} changed` : `~${changes}`
// }`;
// } else if (!expand && !compact) {
// status += '~0';
// }
if (deletions) {
status += `${/*changes |*/ additions ? separator : ''}${
expand ? `${pluralize('line', deletions)} deleted` : `-${deletions}`
}`;
} else if (!expand && !compact) {
status += '-0';
}
status += suffix;
return status;
}
}

+ 2
- 4
src/git/models/log.ts View File

@ -1,11 +1,9 @@
import { Range } from 'vscode';
import { GitAuthor } from './commit';
import { GitLogCommit } from './logCommit';
import { GitCommit } from './commit';
export interface GitLog {
readonly repoPath: string;
readonly authors: Map<string, GitAuthor>;
readonly commits: Map<string, GitLogCommit>;
readonly commits: Map<string, GitCommit>;
readonly sha: string | undefined;
readonly range: Range | undefined;

+ 0
- 259
src/git/models/logCommit.ts View File

@ -1,259 +0,0 @@
import { Uri } from 'vscode';
import { Container } from '../../container';
import { memoize, Strings } from '../../system';
import { GitUri } from '../gitUri';
import { GitReference } from '../models';
import { GitCommit, GitCommitType, GitFileChange } from './commit';
import { GitFile, GitFileIndexStatus, GitFileStatus } from './file';
const emptyStats = Object.freeze({
added: 0,
deleted: 0,
changed: 0,
});
export interface GitLogCommitLine {
from: {
line: number;
count: number;
};
to: {
line: number;
count: number;
};
}
export class GitLogCommit extends GitCommit {
override get parents(): string[] {
return this.parentShas != null ? this.parentShas : [];
}
static override isOfRefType(commit: GitReference | undefined) {
return commit?.refType === 'revision';
}
static override is(commit: any): commit is GitLogCommit {
return (
commit instanceof GitLogCommit
// || (commit.repoPath !== undefined &&
// commit.sha !== undefined &&
// (commit.type === GitCommitType.Log || commit.type === GitCommitType.LogFile))
);
}
nextSha?: string;
nextFileName?: string;
readonly lines: GitLogCommitLine[];
constructor(
type: GitCommitType,
repoPath: string,
sha: string,
author: string,
email: string | undefined,
authorDate: Date,
committerDate: Date,
message: string,
fileName: string,
public readonly files: GitFile[],
public readonly status?: GitFileStatus | undefined,
originalFileName?: string | undefined,
previousSha?: string | undefined,
previousFileName?: string | undefined,
private readonly _fileStats?:
| {
insertions: number;
deletions: number;
}
| undefined,
public readonly parentShas?: string[],
lines?: GitLogCommitLine[],
) {
super(
type,
repoPath,
sha,
author,
email,
authorDate,
committerDate,
message,
fileName,
originalFileName,
previousSha ?? `${sha}^`,
previousFileName,
);
this.lines = lines ?? [];
}
override get file() {
return this.isFile
? new GitFileChange(
this.repoPath,
this.fileName,
this.status ?? GitFileIndexStatus.Modified,
this.originalFileName,
)
: undefined;
}
@memoize()
override get hasConflicts() {
return this.files.some(f => f.conflictStatus != null);
}
get isMerge() {
return this.parentShas != null && this.parentShas.length > 1;
}
get nextUri(): Uri {
return this.nextFileName ? Container.instance.git.getAbsoluteUri(this.nextFileName, this.repoPath) : this.uri;
}
override get previousFileSha(): string {
return this.isFile ? this.previousSha! : `${this.sha}^`;
}
findFile(fileName: string): GitFile | undefined {
fileName = GitUri.relativeTo(fileName, this.repoPath);
return this.files.find(f => f.fileName === fileName);
}
@memoize()
getDiffStatus() {
if (this._fileStats !== undefined) {
return {
added: this._fileStats.insertions,
deleted: this._fileStats.deletions,
changed: 0,
};
}
if (this.isFile || this.files.length === 0) return emptyStats;
const diff = {
added: 0,
deleted: 0,
changed: 0,
};
for (const f of this.files) {
switch (f.status) {
case 'A':
case '?':
diff.added++;
break;
case 'D':
diff.deleted++;
break;
default:
diff.changed++;
break;
}
}
return diff;
}
getFormattedDiffStatus({
compact,
empty,
expand,
prefix = '',
separator = ' ',
suffix = '',
}: {
compact?: boolean;
empty?: string;
expand?: boolean;
prefix?: string;
separator?: string;
suffix?: string;
} = {}): string {
const { added, changed, deleted } = this.getDiffStatus();
if (added === 0 && changed === 0 && deleted === 0) return empty ?? '';
if (expand) {
const type = this.isFile ? 'line' : 'file';
let status = '';
if (added) {
status += `${Strings.pluralize(type, added)} added`;
}
if (changed) {
status += `${status.length === 0 ? '' : separator}${Strings.pluralize(type, changed)} changed`;
}
if (deleted) {
status += `${status.length === 0 ? '' : separator}${Strings.pluralize(type, deleted)} deleted`;
}
return `${prefix}${status}${suffix}`;
}
// When `isFile` we are getting line changes -- and we can't get changed lines (only inserts and deletes)
return `${prefix}${compact && added === 0 ? '' : `+${added}${separator}`}${
(compact || this.isFile) && changed === 0 ? '' : `~${changed}${separator}`
}${compact && deleted === 0 ? '' : `-${deleted}`}${suffix}`;
}
toFileCommit(file: string | GitFile): GitLogCommit | undefined {
const fileName = typeof file === 'string' ? GitUri.relativeTo(file, this.repoPath) : file.fileName;
const foundFile = this.files.find(f => f.fileName === fileName);
if (foundFile == null) return undefined;
let sha;
// If this is a stash commit with an untracked file
if (this.type === GitCommitType.Stash && foundFile.status === '?') {
sha = `${this.sha}^3`;
}
// If this isn't a single-file commit, we can't trust the previousSha
const previousSha = this.isFile ? this.previousSha : `${this.sha}^`;
return this.with({
type: this.isStash ? GitCommitType.StashFile : GitCommitType.LogFile,
sha: sha,
fileName: foundFile.fileName,
originalFileName: foundFile.originalFileName,
previousSha: previousSha,
previousFileName: foundFile.originalFileName ?? foundFile.fileName,
status: foundFile.status,
files: [foundFile],
});
}
with(changes: {
type?: GitCommitType;
sha?: string | null;
fileName?: string;
author?: string;
email?: string;
authorDate?: Date;
committedDate?: Date;
message?: string;
originalFileName?: string | null;
previousFileName?: string | null;
previousSha?: string | null;
status?: GitFileStatus;
files?: GitFile[] | null;
}): GitLogCommit {
return new GitLogCommit(
changes.type ?? this.type,
this.repoPath,
this.getChangedValue(changes.sha, this.sha)!,
changes.author ?? this.authorName,
changes.email ?? this.authorEmail,
changes.authorDate ?? this.authorDate,
changes.committedDate ?? this.committerDate,
changes.message ?? this.message,
changes.fileName ?? this.fileName,
this.getChangedValue(changes.files, this.files) ?? [],
changes.status ?? this.status,
this.getChangedValue(changes.originalFileName, this.originalFileName),
this.getChangedValue(changes.previousSha, this.previousSha),
this.getChangedValue(changes.previousFileName, this.previousFileName),
this._fileStats,
this.parentShas,
this.lines,
);
}
}

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

@ -1,4 +1,4 @@
import { GitBranchReference, GitRevisionReference } from '../models';
import { GitBranchReference, GitRevisionReference } from './reference';
export interface GitMergeStatus {
type: 'merge';

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

@ -1,4 +1,4 @@
import { GitBranchReference, GitRevisionReference } from '../models';
import { GitBranchReference, GitRevisionReference } from './reference';
export interface GitRebaseStatus {
type: 'rebase';

+ 6
- 6
src/git/models/reference.ts View File

@ -43,12 +43,12 @@ export namespace GitRevision {
return isMatch(shaParentRegex, ref);
}
export function isUncommitted(ref: string | undefined) {
return ref === uncommitted || isMatch(uncommittedRegex, ref);
export function isUncommitted(ref: string | undefined, exact: boolean = false) {
return ref === uncommitted || ref === uncommittedStaged || (!exact && isMatch(uncommittedRegex, ref));
}
export function isUncommittedStaged(ref: string | undefined): boolean {
return ref === uncommittedStaged || isMatch(uncommittedStagedRegex, ref);
export function isUncommittedStaged(ref: string | undefined, exact: boolean = false): boolean {
return ref === uncommittedStaged || (!exact && isMatch(uncommittedStagedRegex, ref));
}
export function shorten(
@ -118,7 +118,7 @@ export interface GitRevisionReference {
repoPath: string;
number?: string | undefined;
message?: string;
message?: string | undefined;
}
export interface GitStashReference {
@ -128,7 +128,7 @@ export interface GitStashReference {
repoPath: string;
number: string | undefined;
message?: string;
message?: string | undefined;
}
export interface GitTagReference {

+ 2
- 1
src/git/models/reflog.ts View File

@ -1,7 +1,8 @@
import { DateStyle } from '../../config';
import { formatDate, fromNow } from '../../system/date';
import { memoize } from '../../system/decorators/memoize';
import { CommitDateFormatting, GitRevision } from '../models';
import { CommitDateFormatting } from './commit';
import { GitRevision } from './reference';
export interface GitReflog {
readonly repoPath: string;

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

@ -1,6 +1,6 @@
import { WorkspaceState } from '../../constants';
import { Container } from '../../container';
import { Strings } from '../../system';
import { sortCompare } from '../../system/string';
import { RemoteProvider, RichRemoteProvider } from '../remotes/provider';
export const enum GitRemoteType {
@ -43,7 +43,7 @@ export class GitRemote
(a, b) =>
(a.default ? -1 : 1) - (b.default ? -1 : 1) ||
(a.name === 'origin' ? -1 : 1) - (b.name === 'origin' ? -1 : 1) ||
Strings.sortCompare(a.name, b.name),
sortCompare(a.name, b.name),
);
}

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

@ -32,10 +32,10 @@ import { RemoteProviderFactory, RemoteProviders } from '../remotes/factory';
import { RichRemoteProvider } from '../remotes/provider';
import { SearchPattern } from '../search';
import { BranchSortOptions, GitBranch } from './branch';
import { GitCommit } from './commit';
import { GitContributor } from './contributor';
import { GitDiffShortStat } from './diff';
import { GitLog } from './log';
import { GitLogCommit } from './logCommit';
import { GitMergeStatus } from './merge';
import { GitRebaseStatus } from './rebase';
import { GitBranchReference, GitReference, GitTagReference } from './reference';
@ -517,7 +517,7 @@ export class Repository implements Disposable {
return this.container.git.getChangedFilesCount(this.path, ref);
}
getCommit(ref: string): Promise<GitLogCommit | undefined> {
getCommit(ref: string): Promise<GitCommit | undefined> {
return this.container.git.getCommit(this.path, ref);
}

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

@ -1,4 +1,4 @@
import { GitStashCommit } from './stashCommit';
import { GitStashCommit } from './commit';
export interface GitStash {
readonly repoPath: string;

+ 0
- 97
src/git/models/stashCommit.ts View File

@ -1,97 +0,0 @@
import { Container } from '../../container';
import { gate, memoize } from '../../system';
import { GitReference } from '../models';
import { GitCommitType } from './commit';
import { GitFile, GitFileWorkingTreeStatus } from './file';
import { GitLogCommit } from './logCommit';
const stashNumberRegex = /stash@{(\d+)}/;
export class GitStashCommit extends GitLogCommit {
static override isOfRefType(commit: GitReference | undefined) {
return commit?.refType === 'stash';
}
static override is(commit: any): commit is GitStashCommit {
return (
commit instanceof GitStashCommit
// || (commit.repoPath !== undefined &&
// commit.sha !== undefined &&
// (commit.type === GitCommitType.Stash || commit.type === GitCommitType.StashFile))
);
}
override readonly refType = 'stash';
constructor(
type: GitCommitType,
public readonly stashName: string,
repoPath: string,
sha: string,
authorDate: Date,
committedDate: Date,
message: string,
fileName: string,
files: GitFile[],
) {
super(type, repoPath, sha, 'You', undefined, authorDate, committedDate, message, fileName, files);
}
@memoize()
get number() {
const match = stashNumberRegex.exec(this.stashName);
if (match == null) return undefined;
return match[1];
}
override get shortSha() {
return this.stashName;
}
private _untrackedFilesChecked = false;
@gate()
async checkForUntrackedFiles() {
if (!this._untrackedFilesChecked) {
this._untrackedFilesChecked = true;
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
// See https://stackoverflow.com/questions/12681529/
const commit = await Container.instance.git.getCommit(this.repoPath, `${this.stashName}^3`);
if (commit != null && commit.files.length !== 0) {
// Since these files are untracked -- make them look that way
const files = commit.files.map(s => ({
...s,
status: GitFileWorkingTreeStatus.Untracked,
conflictStatus: undefined,
indexStatus: undefined,
workingTreeStatus: undefined,
}));
this.files.push(...files);
}
}
}
override with(changes: {
type?: GitCommitType;
sha?: string | null;
fileName?: string;
authorDate?: Date;
committedDate?: Date;
message?: string;
files?: GitFile[] | null;
}): GitLogCommit {
return new GitStashCommit(
changes.type ?? this.type,
this.stashName,
this.repoPath,
this.getChangedValue(changes.sha, this.sha)!,
changes.authorDate ?? this.authorDate,
changes.committedDate ?? this.committerDate,
changes.message ?? this.message,
changes.fileName ?? this.fileName,
this.getChangedValue(changes.files, this.files) ?? [],
);
}
}

+ 71
- 97
src/git/models/status.ts View File

@ -3,9 +3,19 @@ import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { memoize } from '../../system/decorators/memoize';
import { pluralize } from '../../system/string';
import { GitCommitType, GitLogCommit, GitRemote, GitRevision, GitUser } from '../models';
import { GitBranch, GitTrackingState } from './branch';
import { GitFile, GitFileConflictStatus, GitFileIndexStatus, GitFileStatus, GitFileWorkingTreeStatus } from './file';
import { GitCommit, GitCommitIdentity } from './commit';
import {
GitFile,
GitFileChange,
GitFileConflictStatus,
GitFileIndexStatus,
GitFileStatus,
GitFileWorkingTreeStatus,
} from './file';
import { GitRevision } from './reference';
import { GitRemote } from './remote';
import { GitUser } from './user';
export interface ComputedWorkingTreeGitStatus {
staged: number;
@ -316,8 +326,8 @@ export class GitStatusFile implements GitFile {
public readonly repoPath: string,
x: string | undefined,
y: string | undefined,
public readonly fileName: string,
public readonly originalFileName?: string,
public readonly path: string,
public readonly originalPath?: string,
) {
if (x != null && y != null) {
switch (x + y) {
@ -402,7 +412,7 @@ export class GitStatusFile implements GitFile {
@memoize()
get uri(): Uri {
return Container.instance.git.getAbsoluteUri(this.fileName, this.repoPath);
return Container.instance.git.getAbsoluteUri(this.path, this.repoPath);
}
getFormattedDirectory(includeOriginal: boolean = false): string {
@ -421,26 +431,30 @@ export class GitStatusFile implements GitFile {
return GitFile.getStatusText(this.status);
}
toPsuedoCommits(user: GitUser | undefined): GitLogCommit[] {
const commits: GitLogCommit[] = [];
getPseudoCommits(user: GitUser | undefined): GitCommit[] {
const commits: GitCommit[] = [];
const now = new Date();
if (this.conflictStatus != null) {
commits.push(
new GitLogCommit(
GitCommitType.LogFile,
new GitCommit(
this.repoPath,
GitRevision.uncommitted,
'You',
user?.email ?? undefined,
new Date(),
new Date(),
'',
this.fileName,
[this],
this.status,
this.originalFileName,
GitRevision.uncommittedStaged,
this.originalFileName ?? this.fileName,
new GitCommitIdentity('You', user?.email ?? undefined, now),
new GitCommitIdentity('You', user?.email ?? undefined, now),
'Uncommitted changes',
[GitRevision.uncommittedStaged],
'Uncommitted changes',
new GitFileChange(
this.repoPath,
this.path,
this.status,
this.originalPath,
GitRevision.uncommittedStaged,
),
undefined,
[],
),
);
return commits;
@ -449,99 +463,59 @@ export class GitStatusFile implements GitFile {
if (this.workingTreeStatus == null && this.indexStatus == null) return commits;
if (this.workingTreeStatus != null && this.indexStatus != null) {
// Decrements the date to guarantee the staged entry will be sorted after the working entry (most recent first)
const older = new Date(now);
older.setMilliseconds(older.getMilliseconds() - 1);
commits.push(
new GitLogCommit(
GitCommitType.LogFile,
new GitCommit(
this.repoPath,
GitRevision.uncommitted,
'You',
user?.email ?? undefined,
new Date(),
new Date(),
'',
this.fileName,
[this],
this.status,
this.originalFileName,
GitRevision.uncommittedStaged,
this.originalFileName ?? this.fileName,
new GitCommitIdentity('You', user?.email ?? undefined, now),
new GitCommitIdentity('You', user?.email ?? undefined, now),
'Uncommitted changes',
[GitRevision.uncommittedStaged],
'Uncommitted changes',
new GitFileChange(
this.repoPath,
this.path,
this.status,
this.originalPath,
GitRevision.uncommittedStaged,
),
undefined,
[],
),
new GitLogCommit(
GitCommitType.LogFile,
new GitCommit(
this.repoPath,
GitRevision.uncommittedStaged,
'You',
user != null ? user.email : undefined,
new Date(),
new Date(),
'',
this.fileName,
[this],
this.status,
this.originalFileName,
'HEAD',
this.originalFileName ?? this.fileName,
new GitCommitIdentity('You', user?.email ?? undefined, older),
new GitCommitIdentity('You', user?.email ?? undefined, older),
'Uncommitted changes',
['HEAD'],
'Uncommitted changes',
new GitFileChange(this.repoPath, this.path, this.status, this.originalPath, 'HEAD'),
undefined,
[],
),
);
} else {
commits.push(
new GitLogCommit(
GitCommitType.LogFile,
new GitCommit(
this.repoPath,
this.workingTreeStatus != null ? GitRevision.uncommitted : GitRevision.uncommittedStaged,
'You',
user?.email ?? undefined,
new Date(),
new Date(),
'',
this.fileName,
[this],
this.status,
this.originalFileName,
'HEAD',
this.originalFileName ?? this.fileName,
new GitCommitIdentity('You', user?.email ?? undefined, now),
new GitCommitIdentity('You', user?.email ?? undefined, now),
'Uncommitted changes',
['HEAD'],
'Uncommitted changes',
new GitFileChange(this.repoPath, this.path, this.status, this.originalPath, 'HEAD'),
undefined,
[],
),
);
}
return commits;
}
with(changes: {
conflictStatus?: GitFileConflictStatus | null;
indexStatus?: GitFileIndexStatus | null;
workTreeStatus?: GitFileWorkingTreeStatus | null;
fileName?: string;
originalFileName?: string | null;
}): GitStatusFile {
const working = this.getChangedValue(changes.workTreeStatus, this.workingTreeStatus);
let status: string;
switch (working) {
case GitFileWorkingTreeStatus.Untracked:
status = '??';
break;
case GitFileWorkingTreeStatus.Ignored:
status = '!!';
break;
default:
status =
this.getChangedValue(changes.conflictStatus, this.conflictStatus) ??
`${this.getChangedValue(changes.indexStatus, this.indexStatus) ?? ' '}${working ?? ' '}`;
break;
}
return new GitStatusFile(
this.repoPath,
status[0]?.trim() || undefined,
status[1]?.trim() || undefined,
changes.fileName ?? this.fileName,
this.getChangedValue(changes.originalFileName, this.originalFileName),
);
}
protected getChangedValue<T>(change: T | null | undefined, original: T | undefined): T | undefined {
if (change === undefined) return original;
return change !== null ? change : undefined;
}
}

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

@ -2,7 +2,7 @@ import { configuration, DateStyle, TagSorting } from '../../configuration';
import { formatDate, fromNow } from '../../system/date';
import { memoize } from '../../system/decorators/memoize';
import { sortCompare } from '../../system/string';
import { GitReference, GitTagReference } from '../models';
import { GitReference, GitTagReference } from './reference';
export const TagDateFormatting = {
dateFormat: undefined! as string | null,

+ 43
- 63
src/git/parsers/blameParser.ts View File

@ -1,10 +1,9 @@
import { debug } from '../../system/decorators/log';
import { normalizePath, relative } from '../../system/path';
import { getLines } from '../../system/string';
import {
GitAuthor,
GitBlame,
GitCommit2,
GitBlameAuthor,
GitCommit,
GitCommitIdentity,
GitCommitLine,
GitFileChange,
@ -31,68 +30,60 @@ interface BlameEntry {
committerEmail?: string;
previousSha?: string;
previousFileName?: string;
previousPath?: string;
fileName?: string;
path: string;
summary?: string;
}
export class GitBlameParser {
@debug({ args: false, singleLine: true })
static parse(
data: string,
repoPath: string | undefined,
fileName: string,
currentUser: GitUser | undefined,
): GitBlame | undefined {
static parse(data: string, repoPath: string, currentUser: GitUser | undefined): GitBlame | undefined {
if (!data) return undefined;
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitCommit2>();
const authors = new Map<string, GitBlameAuthor>();
const commits = new Map<string, GitCommit>();
const lines: GitCommitLine[] = [];
let relativeFileName;
let entry: BlameEntry | undefined = undefined;
let key: string;
let line: string;
let lineParts: string[];
let first = true;
for (line of getLines(data)) {
lineParts = line.split(' ');
if (lineParts.length < 2) continue;
if (entry === undefined) {
[key] = lineParts;
if (entry == null) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
entry = {
author: undefined!,
committer: undefined!,
sha: lineParts[0],
sha: key,
originalLine: parseInt(lineParts[1], 10),
line: parseInt(lineParts[2], 10),
lineCount: parseInt(lineParts[3], 10),
};
} as BlameEntry;
continue;
}
switch (lineParts[0]) {
switch (key) {
case 'author':
if (GitRevision.isUncommitted(entry.sha)) {
if (entry.sha === GitRevision.uncommitted) {
entry.author = 'You';
} else {
entry.author = lineParts.slice(1).join(' ').trim();
entry.author = line.slice(key.length + 1).trim();
}
break;
case 'author-mail': {
if (GitRevision.isUncommitted(entry.sha)) {
entry.authorEmail = currentUser !== undefined ? currentUser.email : undefined;
if (entry.sha === GitRevision.uncommitted) {
entry.authorEmail = currentUser?.email;
continue;
}
entry.authorEmail = lineParts.slice(1).join(' ').trim();
entry.authorEmail = line.slice(key.length + 1).trim();
const start = entry.authorEmail.indexOf('<');
if (start >= 0) {
const end = entry.authorEmail.indexOf('>', start);
@ -117,17 +108,17 @@ export class GitBlameParser {
if (GitRevision.isUncommitted(entry.sha)) {
entry.committer = 'You';
} else {
entry.committer = lineParts.slice(1).join(' ').trim();
entry.committer = line.slice(key.length + 1).trim();
}
break;
case 'committer-mail': {
if (GitRevision.isUncommitted(entry.sha)) {
entry.committerEmail = currentUser !== undefined ? currentUser.email : undefined;
entry.committerEmail = currentUser?.email;
continue;
}
entry.committerEmail = lineParts.slice(1).join(' ').trim();
entry.committerEmail = line.slice(key.length + 1).trim();
const start = entry.committerEmail.indexOf('<');
if (start >= 0) {
const end = entry.committerEmail.indexOf('>', start);
@ -149,29 +140,20 @@ export class GitBlameParser {
break;
case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim();
entry.summary = line.slice(key.length + 1).trim();
break;
case 'previous':
entry.previousSha = lineParts[1];
entry.previousFileName = lineParts.slice(2).join(' ');
entry.previousPath = lineParts.slice(2).join(' ');
break;
case 'filename':
entry.fileName = lineParts.slice(1).join(' ');
if (first && repoPath === undefined) {
// Try to get the repoPath from the most recent commit
repoPath = normalizePath(
fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName, ''),
);
relativeFileName = normalizePath(relative(repoPath, fileName));
} else {
relativeFileName = entry.fileName;
}
first = false;
// Don't trim to allow spaces in the filename
entry.path = line.slice(key.length + 1);
GitBlameParser.parseEntry(entry, repoPath, relativeFileName, commits, authors, lines, currentUser);
// Since the filename marks the end of a commit, parse the entry and clear it for the next
GitBlameParser.parseEntry(entry, repoPath, commits, authors, lines, currentUser);
entry = undefined;
break;
@ -193,7 +175,7 @@ export class GitBlameParser {
const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount));
const blame: GitBlame = {
repoPath: repoPath!,
repoPath: repoPath,
authors: sortedAuthors,
commits: commits,
lines: lines,
@ -203,10 +185,9 @@ export class GitBlameParser {
private static parseEntry(
entry: BlameEntry,
repoPath: string | undefined,
relativeFileName: string,
commits: Map<string, GitCommit2>,
authors: Map<string, GitAuthor>,
repoPath: string,
commits: Map<string, GitCommit>,
authors: Map<string, GitBlameAuthor>,
lines: GitCommitLine[],
currentUser: { name?: string; email?: string } | undefined,
) {
@ -235,8 +216,8 @@ export class GitBlameParser {
}
}
commit = new GitCommit2(
repoPath!,
commit = new GitCommit(
repoPath,
entry.sha,
new GitCommitIdentity(entry.author, entry.authorEmail, new Date((entry.authorDate as any) * 1000)),
new GitCommitIdentity(
@ -248,14 +229,13 @@ export class GitBlameParser {
[],
undefined,
new GitFileChange(
repoPath!,
relativeFileName,
repoPath,
entry.path,
GitFileIndexStatus.Modified,
entry.previousFileName && entry.previousFileName !== entry.fileName
? entry.previousFileName
: undefined,
entry.previousPath && entry.previousPath !== entry.path ? entry.previousPath : undefined,
entry.previousSha,
),
undefined,
[],
);
@ -265,13 +245,13 @@ export class GitBlameParser {
for (let i = 0, len = entry.lineCount; i < len; i++) {
const line: GitCommitLine = {
sha: entry.sha,
line: entry.line + i,
originalLine: entry.originalLine + i,
previousSha: commit.file?.previousSha,
previousSha: commit.file!.previousSha,
from: { line: entry.originalLine + i, count: 1 },
to: { line: entry.line + i, count: 1 },
};
commit.lines?.push(line);
lines[line.line - 1] = line;
commit.lines.push(line);
lines[line.to.line - 1] = line;
}
}
}

+ 5
- 4
src/git/parsers/diffParser.ts View File

@ -1,4 +1,5 @@
import { debug, Strings } from '../../system';
import { debug } from '../../system/decorators/log';
import { getLines } from '../../system/string';
import { GitDiff, GitDiffHunk, GitDiffHunkLine, GitDiffLine, GitDiffShortStat } from '../models/diff';
import { GitFile, GitFileStatus } from '../models/file';
@ -81,7 +82,7 @@ export class GitDiffParser {
let hasRemoved;
let removed = 0;
for (const l of Strings.getLines(hunk.diff)) {
for (const l of getLines(hunk.diff)) {
switch (l[0]) {
case '+':
hasAddedOrChanged = true;
@ -167,9 +168,9 @@ export class GitDiffParser {
indexStatus: undefined,
workingTreeStatus: undefined,
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
fileName: ` ${fileName}`.substr(1),
path: ` ${fileName}`.substr(1),
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
originalFileName:
originalPath:
originalFileName == null || originalFileName.length === 0
? undefined
: ` ${originalFileName}`.substr(1),

+ 118
- 154
src/git/parsers/logParser.ts View File

@ -3,21 +3,18 @@ import { Arrays, debug } from '../../system';
import { normalizePath, relative } from '../../system/path';
import { getLines } from '../../system/string';
import {
GitAuthor,
GitCommitType,
GitCommit,
GitCommitIdentity,
GitCommitLine,
GitFile,
GitFileChange,
GitFileChangeStats,
GitFileIndexStatus,
GitLog,
GitLogCommit,
GitLogCommitLine,
GitRevision,
GitUser,
} from '../models';
const emptyEntry: LogEntry = {};
const emptyStr = '';
const slash = '/';
const diffRegex = /diff --git a\/(.*) b\/(.*)/;
const diffRangeRegex = /^@@ -(\d+?),(\d+?) \+(\d+?),(\d+?) @@/;
@ -37,29 +34,38 @@ const rb = '%x3e'; // `%x${'>'.charCodeAt(0).toString(16)}`;
const sl = '%x2f'; // `%x${'/'.charCodeAt(0).toString(16)}`;
const sp = '%x20'; // `%x${' '.charCodeAt(0).toString(16)}`;
export const enum LogType {
Log = 0,
LogFile = 1,
}
interface LogEntry {
ref?: string;
sha?: string;
author?: string;
date?: string;
authorDate?: string;
authorEmail?: string;
committer?: string;
committedDate?: string;
email?: string;
committerEmail?: string;
parentShas?: string[];
fileName?: string;
originalFileName?: string;
/** @deprecated */
path?: string;
/** @deprecated */
originalPath?: string;
file?: GitFile;
files?: GitFile[];
status?: GitFileIndexStatus;
fileStats?: {
insertions: number;
deletions: number;
};
fileStats?: GitFileChangeStats;
summary?: string;
line?: GitLogCommitLine;
line?: GitCommitLine;
}
export type Parser<T> = {
@ -110,9 +116,11 @@ export class GitLogParser {
`${lb}${sl}f${rb}`,
`${lb}r${rb}${sp}%H`, // ref
`${lb}a${rb}${sp}%aN`, // author
`${lb}e${rb}${sp}%aE`, // email
`${lb}d${rb}${sp}%at`, // date
`${lb}c${rb}${sp}%ct`, // committed date
`${lb}e${rb}${sp}%aE`, // author email
`${lb}d${rb}${sp}%at`, // author date
`${lb}n${rb}${sp}%cN`, // committer
`${lb}m${rb}${sp}%cE`, // committer email
`${lb}c${rb}${sp}%ct`, // committer date
`${lb}p${rb}${sp}%P`, // parents
`${lb}s${rb}`,
'%B', // summary
@ -263,7 +271,7 @@ export class GitLogParser {
@debug({ args: false })
static parse(
data: string,
type: GitCommitType,
type: LogType,
repoPath: string | undefined,
fileName: string | undefined,
sha: string | undefined,
@ -275,9 +283,8 @@ export class GitLogParser {
if (!data) return undefined;
let relativeFileName: string;
let recentCommit: GitLogCommit | undefined = undefined;
let entry: LogEntry = emptyEntry;
let entry: LogEntry = {};
let line: string | undefined = undefined;
let token: number;
@ -293,8 +300,7 @@ export class GitLogParser {
repoPath = normalizePath(repoPath);
}
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitLogCommit>();
const commits = new Map<string, GitCommit>();
let truncationCount = limit;
let match;
@ -317,12 +323,12 @@ export class GitLogParser {
switch (token) {
case 114: // 'r': // ref
entry = {
ref: line.substring(4),
sha: line.substring(4),
};
break;
case 97: // 'a': // author
if (GitRevision.isUncommitted(entry.ref)) {
if (GitRevision.uncommitted === entry.sha) {
entry.author = 'You';
} else {
entry.author = line.substring(4);
@ -330,11 +336,19 @@ export class GitLogParser {
break;
case 101: // 'e': // author-mail
entry.email = line.substring(4);
entry.authorEmail = line.substring(4);
break;
case 100: // 'd': // author-date
entry.date = line.substring(4);
entry.authorDate = line.substring(4);
break;
case 110: // 'n': // committer
entry.committer = line.substring(4);
break;
case 109: // 'm': // committer-mail
entry.committedDate = line.substring(4);
break;
case 99: // 'c': // committer-date
@ -395,7 +409,7 @@ export class GitLogParser {
if (line.startsWith('warning:')) continue;
if (type === GitCommitType.Log) {
if (type === LogType.Log) {
match = fileStatusRegex.exec(line);
if (match != null) {
if (entry.files === undefined) {
@ -406,22 +420,22 @@ export class GitLogParser {
if (renamedFileName !== undefined) {
entry.files.push({
status: match[1] as GitFileIndexStatus,
fileName: renamedFileName,
originalFileName: match[2],
path: renamedFileName,
originalPath: match[2],
});
} else {
entry.files.push({
status: match[1] as GitFileIndexStatus,
fileName: match[2],
path: match[2],
});
}
}
} else {
match = diffRegex.exec(line);
if (match != null) {
[, entry.originalFileName, entry.fileName] = match;
if (entry.fileName === entry.originalFileName) {
entry.originalFileName = undefined;
[, entry.originalPath, entry.path] = match;
if (entry.path === entry.originalPath) {
entry.originalPath = undefined;
entry.status = GitFileIndexStatus.Modified;
} else {
entry.status = GitFileIndexStatus.Renamed;
@ -434,6 +448,7 @@ export class GitLogParser {
match = diffRangeRegex.exec(next.value);
if (match !== null) {
entry.line = {
sha: entry.sha!,
from: {
line: parseInt(match[1], 10),
count: parseInt(match[2], 10),
@ -455,14 +470,15 @@ export class GitLogParser {
match = fileStatusAndSummaryRegex.exec(`${line}\n${next.value}`);
if (match != null) {
entry.fileStats = {
insertions: Number(match[1]) || 0,
additions: Number(match[1]) || 0,
deletions: Number(match[2]) || 0,
changes: 0,
};
switch (match[4]) {
case undefined:
entry.status = 'M' as GitFileIndexStatus;
entry.fileName = match[3];
entry.path = match[3];
break;
case 'copy':
case 'rename':
@ -473,34 +489,34 @@ export class GitLogParser {
fileStatusAndSummaryRenamedFilePathRegex.exec(renamedFileName);
if (renamedMatch != null) {
// If there is no new path, the path part was removed so ensure we don't end up with //
entry.fileName =
entry.path =
renamedMatch[3] === ''
? `${renamedMatch[1]}${renamedMatch[4]}`.replace('//', '/')
: `${renamedMatch[1]}${renamedMatch[3]}${renamedMatch[4]}`;
entry.originalFileName = `${renamedMatch[1]}${renamedMatch[2]}${renamedMatch[4]}`;
entry.originalPath = `${renamedMatch[1]}${renamedMatch[2]}${renamedMatch[4]}`;
} else {
renamedMatch =
fileStatusAndSummaryRenamedFileRegex.exec(renamedFileName);
if (renamedMatch != null) {
entry.fileName = renamedMatch[2];
entry.originalFileName = renamedMatch[1];
entry.path = renamedMatch[2];
entry.originalPath = renamedMatch[1];
} else {
entry.fileName = renamedFileName;
entry.path = renamedFileName;
}
}
break;
case 'create':
entry.status = 'A' as GitFileIndexStatus;
entry.fileName = match[3];
entry.path = match[3];
break;
case 'delete':
entry.status = 'D' as GitFileIndexStatus;
entry.fileName = match[3];
entry.path = match[3];
break;
default:
entry.status = 'M' as GitFileIndexStatus;
entry.fileName = match[3];
entry.path = match[3];
break;
}
}
@ -511,26 +527,21 @@ export class GitLogParser {
}
if (entry.files !== undefined) {
entry.fileName = Arrays.filterMap(entry.files, f => (f.fileName ? f.fileName : undefined)).join(
', ',
);
entry.path = Arrays.filterMap(entry.files, f => (f.path ? f.path : undefined)).join(', ');
}
if (first && repoPath === undefined && type === GitCommitType.LogFile && fileName !== undefined) {
if (first && repoPath === undefined && type === LogType.LogFile && fileName !== undefined) {
// Try to get the repoPath from the most recent commit
repoPath = normalizePath(
fileName.replace(
fileName.startsWith(slash) ? `/${entry.fileName}` : entry.fileName!,
emptyStr,
),
fileName.replace(fileName.startsWith('/') ? `/${entry.path}` : entry.path!, ''),
);
relativeFileName = normalizePath(relative(repoPath, fileName));
} else {
relativeFileName = entry.fileName!;
relativeFileName = entry.path!;
}
first = false;
const commit = commits.get(entry.ref!);
const commit = commits.get(entry.sha!);
if (commit === undefined) {
i++;
if (limit && i > limit) break loop;
@ -539,17 +550,7 @@ export class GitLogParser {
truncationCount--;
}
recentCommit = GitLogParser.parseEntry(
entry,
commit,
type,
repoPath,
relativeFileName,
commits,
authors,
recentCommit,
currentUser,
);
GitLogParser.parseEntry(entry, commit, type, repoPath, relativeFileName, commits, currentUser);
break;
}
@ -558,7 +559,6 @@ export class GitLogParser {
const log: GitLog = {
repoPath: repoPath!,
authors: authors,
commits: commits,
sha: sha,
count: i,
@ -571,89 +571,77 @@ export class GitLogParser {
private static parseEntry(
entry: LogEntry,
commit: GitLogCommit | undefined,
type: GitCommitType,
commit: GitCommit | undefined,
type: LogType,
repoPath: string | undefined,
relativeFileName: string,
commits: Map<string, GitLogCommit>,
authors: Map<string, GitAuthor>,
recentCommit: GitLogCommit | undefined,
commits: Map<string, GitCommit>,
currentUser: { name?: string; email?: string } | undefined,
): GitLogCommit | undefined {
if (commit === undefined) {
if (entry.author !== undefined) {
): void {
if (commit == null) {
if (entry.author != null) {
if (
currentUser !== undefined &&
currentUser != null &&
// Name or e-mail is configured
(currentUser.name !== undefined || currentUser.email !== undefined) &&
(currentUser.name != null || currentUser.email != null) &&
// Match on name if configured
(currentUser.name === undefined || currentUser.name === entry.author) &&
(currentUser.name == null || currentUser.name === entry.author) &&
// Match on email if configured
(currentUser.email === undefined || currentUser.email === entry.email)
(currentUser.email == null || currentUser.email === entry.authorEmail)
) {
entry.author = 'You';
}
}
let author = authors.get(entry.author);
if (author === undefined) {
author = {
name: entry.author,
lineCount: 0,
};
authors.set(entry.author, author);
if (entry.committer != null) {
if (
currentUser != null &&
// Name or e-mail is configured
(currentUser.name != null || currentUser.email != null) &&
// Match on name if configured
(currentUser.name == null || currentUser.name === entry.committer) &&
// Match on email if configured
(currentUser.email == null || currentUser.email === entry.committerEmail)
) {
entry.committer = 'You';
}
}
const originalFileName =
entry.originalFileName ?? (relativeFileName !== entry.fileName ? entry.fileName : undefined);
if (type === GitCommitType.LogFile) {
entry.files = [
{
status: entry.status!,
fileName: relativeFileName,
originalFileName: originalFileName,
},
];
const originalFileName = entry.originalPath ?? (relativeFileName !== entry.path ? entry.path : undefined);
const files: { file?: GitFileChange; files?: GitFileChange[] } = {
files: entry.files?.map(f => new GitFileChange(repoPath!, f.path, f.status, f.originalPath)),
};
if (type === LogType.LogFile) {
files.file = new GitFileChange(
repoPath!,
relativeFileName,
entry.status!,
originalFileName,
undefined,
entry.fileStats,
);
}
commit = new GitLogCommit(
type,
commit = new GitCommit(
repoPath!,
entry.ref!,
entry.author!,
entry.email,
new Date((entry.date! as any) * 1000),
new Date((entry.committedDate! as any) * 1000),
entry.summary === undefined ? emptyStr : entry.summary,
relativeFileName,
entry.files ?? [],
entry.status,
originalFileName,
type === GitCommitType.Log ? entry.parentShas![0] : undefined,
entry.sha!,
new GitCommitIdentity(entry.author!, entry.authorEmail, new Date((entry.authorDate! as any) * 1000)),
new GitCommitIdentity(
entry.committer!,
entry.committerEmail,
new Date((entry.committedDate! as any) * 1000),
),
entry.summary?.split('\n', 1)[0] ?? '',
entry.parentShas ?? [],
entry.summary ?? '',
files,
undefined,
entry.fileStats,
entry.parentShas,
entry.line != null ? [entry.line] : [],
);
commits.set(entry.ref!, commit);
}
// else {
// Logger.log(`merge commit? ${entry.sha}`);
// }
if (recentCommit !== undefined) {
// If the commit sha's match (merge commit), just forward it along
commit.nextSha = commit.sha !== recentCommit.sha ? recentCommit.sha : recentCommit.nextSha;
// Only add a filename if this is a file log
if (type === GitCommitType.LogFile) {
recentCommit.previousFileName = commit.originalFileName ?? commit.fileName;
commit.nextFileName = recentCommit.originalFileName ?? recentCommit.fileName;
}
commits.set(entry.sha!, commit);
}
return commit;
}
@debug({ args: false })
@ -674,30 +662,6 @@ export class GitLogParser {
}
@debug({ args: false })
static parseRefsOnly(data: string): string[] {
const refs = [];
let ref;
let match;
do {
match = logRefsRegex.exec(data);
if (match == null) break;
[, ref] = match;
if (ref == null || ref.length === 0) continue;
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
refs.push(` ${ref}`.substr(1));
} while (true);
// Ensure the regex state is reset
logRefsRegex.lastIndex = 0;
return refs;
}
@debug({ args: false })
static parseSimple(
data: string,
skip: number,

+ 32
- 24
src/git/parsers/stashParser.ts View File

@ -1,6 +1,16 @@
import { Arrays, debug, Strings } from '../../system';
import { filterMap } from '../../system/array';
import { debug } from '../../system/decorators/log';
import { normalizePath } from '../../system/path';
import { GitCommitType, GitFile, GitFileIndexStatus, GitStash, GitStashCommit } from '../models';
import { getLines } from '../../system/string';
import {
GitCommit,
GitCommitIdentity,
GitFile,
GitFileChange,
GitFileIndexStatus,
GitStash,
GitStashCommit,
} from '../models';
import { fileStatusRegex } from './logParser';
// import { Logger } from './logger';
@ -10,9 +20,6 @@ const rb = '%x3e'; // `%x${'>'.charCodeAt(0).toString(16)}`;
const sl = '%x2f'; // `%x${'/'.charCodeAt(0).toString(16)}`;
const sp = '%x20'; // `%x${' '.charCodeAt(0).toString(16)}`;
const emptyStr = '';
const emptyEntry: StashEntry = {};
interface StashEntry {
ref?: string;
date?: string;
@ -40,7 +47,7 @@ export class GitStashParser {
static parse(data: string, repoPath: string): GitStash | undefined {
if (!data) return undefined;
const lines = Strings.getLines(`${data}</f>`);
const lines = getLines(`${data}</f>`);
// Skip the first line since it will always be </f>
let next = lines.next();
if (next.done) return undefined;
@ -51,7 +58,7 @@ export class GitStashParser {
const commits = new Map<string, GitStashCommit>();
let entry: StashEntry = emptyEntry;
let entry: StashEntry = {};
let line: string | undefined = undefined;
let token: number;
@ -131,26 +138,25 @@ export class GitStashParser {
if (renamedFileName !== undefined) {
entry.files.push({
status: match[1] as GitFileIndexStatus,
fileName: renamedFileName,
originalFileName: match[2],
path: renamedFileName,
originalPath: match[2],
});
} else {
entry.files.push({
status: match[1] as GitFileIndexStatus,
fileName: match[2],
path: match[2],
});
}
}
}
if (entry.files !== undefined) {
entry.fileNames = Arrays.filterMap(entry.files, f =>
f.fileName ? f.fileName : undefined,
).join(', ');
if (entry.files != null) {
entry.fileNames = filterMap(entry.files, f => (f.path ? f.path : undefined)).join(', ');
}
}
GitStashParser.parseEntry(entry, repoPath, commits);
entry = {};
}
}
@ -163,18 +169,20 @@ export class GitStashParser {
private static parseEntry(entry: StashEntry, repoPath: string, commits: Map<string, GitStashCommit>) {
let commit = commits.get(entry.ref!);
if (commit === undefined) {
commit = new GitStashCommit(
GitCommitType.Stash,
entry.stashName!,
if (commit == null) {
commit = new GitCommit(
repoPath,
entry.ref!,
new Date((entry.date! as any) * 1000),
new Date((entry.committedDate! as any) * 1000),
entry.summary === undefined ? emptyStr : entry.summary,
entry.fileNames!,
entry.files ?? [],
);
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);

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

@ -21,7 +21,7 @@ import { isPromise } from '../../system/promise';
import {
Account,
DefaultBranch,
GitLogCommit,
GitCommit,
IssueOrPullRequest,
PullRequest,
PullRequestState,
@ -81,7 +81,7 @@ export type RemoteResource =
| {
type: RemoteResourceType.Revision;
branchOrTag?: string;
commit?: GitLogCommit;
commit?: GitCommit;
fileName: string;
range?: Range;
sha?: string;

+ 19
- 19
src/hovers/hovers.ts View File

@ -5,15 +5,7 @@ import { GlyphChars } from '../constants';
import { Container } from '../container';
import { CommitFormatter } from '../git/formatters';
import { GitUri } from '../git/gitUri';
import {
GitCommit2,
GitDiffHunk,
GitDiffHunkLine,
GitLogCommit,
GitRemote,
GitRevision,
PullRequest,
} from '../git/models';
import { GitCommit, GitDiffHunk, GitDiffHunkLine, GitRemote, GitRevision, PullRequest } from '../git/models';
import { Logger, LogLevel } from '../logger';
import { count } from '../system/iterable';
import { PromiseCancelledError } from '../system/promise';
@ -21,7 +13,7 @@ import { getDurationMilliseconds } from '../system/string';
export namespace Hovers {
export async function changesMessage(
commit: GitCommit2,
commit: GitCommit,
uri: GitUri,
editorLine: number,
document: TextDocument,
@ -33,10 +25,16 @@ export namespace Hovers {
// TODO: Figure out how to optimize this
let ref;
let ref2 = documentRef;
if (commit.isUncommitted) {
if (GitRevision.isUncommittedStaged(documentRef)) {
ref = documentRef;
}
// Check for a staged diff
if (ref == null && ref2 == null) {
ref2 = GitRevision.uncommittedStaged;
}
} else {
ref = commit.file.previousSha;
if (ref == null) {
@ -45,7 +43,7 @@ export namespace Hovers {
}
const line = editorLine + 1;
const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0];
const commitLine = commit.lines.find(l => l.to.line === line) ?? commit.lines[0];
let originalPath = commit.file.originalPath;
if (originalPath == null) {
@ -54,12 +52,12 @@ export namespace Hovers {
}
}
editorLine = commitLine.line - 1;
editorLine = commitLine.to.line - 1;
// TODO: Doesn't work with dirty files -- pass in editor? or contents?
let hunkLine = await Container.instance.git.getDiffForLine(uri, editorLine, ref, documentRef);
let hunkLine = await Container.instance.git.getDiffForLine(uri, editorLine, ref, ref2);
// If we didn't find a diff & ref is undefined (meaning uncommitted), check for a staged diff
if (hunkLine == null && ref == null) {
if (hunkLine == null && ref == null && ref2 !== GitRevision.uncommittedStaged) {
hunkLine = await Container.instance.git.getDiffForLine(
uri,
editorLine,
@ -72,6 +70,7 @@ export namespace Hovers {
}
const diff = await getDiff();
if (diff == null) return undefined;
let message;
let previous;
@ -142,12 +141,12 @@ export namespace Hovers {
return markdown;
}
export function localChangesMessage(
fromCommit: GitLogCommit | undefined,
export async function localChangesMessage(
fromCommit: GitCommit | undefined,
uri: GitUri,
editorLine: number,
hunk: GitDiffHunk,
): MarkdownString {
): Promise<MarkdownString | undefined> {
const diff = getDiffFromHunk(hunk);
let message;
@ -157,7 +156,8 @@ export namespace Hovers {
previous = '_Working Tree_';
current = '_Unsaved_';
} else {
const file = fromCommit.findFile(uri.fsPath)!;
const file = await fromCommit.findFile(uri.fsPath);
if (file == null) return undefined;
message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs({
lhs: {
@ -189,7 +189,7 @@ export namespace Hovers {
}
export async function detailsMessage(
commit: GitCommit2,
commit: GitCommit,
uri: GitUri,
editorLine: number,
format: string,

+ 2
- 2
src/hovers/lineHoverController.ts View File

@ -135,8 +135,8 @@ export class LineHoverController implements Disposable {
let editorLine = position.line;
const line = editorLine + 1;
const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0];
editorLine = commitLine.originalLine - 1;
const commitLine = commit.lines.find(l => l.to.line === line) ?? commit.lines[0];
editorLine = commitLine.from.line - 1;
const trackedDocument = await this.container.tracker.get(document);
if (trackedDocument == null) return undefined;

+ 3
- 5
src/messages.ts View File

@ -1,6 +1,6 @@
import { ConfigurationTarget, env, MessageItem, Uri, window } from 'vscode';
import { configuration } from './configuration';
import { GitCommit, GitCommit2 } from './git/models';
import { GitCommit } from './git/models';
import { Logger } from './logger';
export const enum SuppressedMessages {
@ -18,10 +18,8 @@ export const enum SuppressedMessages {
}
export class Messages {
static showCommitHasNoPreviousCommitWarningMessage(
commit?: GitCommit | GitCommit2,
): Promise<MessageItem | undefined> {
if (commit === undefined) {
static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise<MessageItem | undefined> {
if (commit == null) {
return Messages.showMessage(
'info',
'There is no previous commit.',

+ 7
- 16
src/premium/github/github.ts View File

@ -518,6 +518,9 @@ export class GitHubApi {
oid
parents(first: 3) { nodes { oid } }
message
additions
changedFiles
deletions
author {
avatarUrl
date
@ -801,6 +804,9 @@ export class GitHubApi {
oid
message
parents(first: 3) { nodes { oid } }
additions
changedFiles
deletions
author {
avatarUrl
date
@ -1201,22 +1207,7 @@ export interface GitHubBlameRange {
startingLine: number;
endingLine: number;
age: number;
commit: {
oid: string;
parents: { nodes: { oid: string }[] };
message: string;
author: {
avatarUrl: string;
date: string;
email: string;
name: string;
};
committer: {
date: string;
email: string;
name: string;
};
};
commit: GitHubCommit;
}
export interface GitHubBranch {

+ 193
- 173
src/premium/github/githubGitProvider.ts View File

@ -34,16 +34,15 @@ import {
import { GitUri } from '../../git/gitUri';
import {
BranchSortOptions,
GitAuthor,
GitBlame,
GitBlameAuthor,
GitBlameLine,
GitBlameLines,
GitBranch,
GitBranchReference,
GitCommit2,
GitCommit,
GitCommitIdentity,
GitCommitLine,
GitCommitType,
GitContributor,
GitDiff,
GitDiffFilter,
@ -53,7 +52,6 @@ import {
GitFileChange,
GitFileIndexStatus,
GitLog,
GitLogCommit,
GitMergeStatus,
GitRebaseStatus,
GitReference,
@ -393,38 +391,40 @@ export class GitHubGitProvider implements GitProvider, Disposable {
file,
);
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitCommit2>();
const authors = new Map<string, GitBlameAuthor>();
const commits = new Map<string, GitCommit>();
const lines: GitCommitLine[] = [];
for (const range of blame.ranges) {
const c = range.commit;
const { viewer = session.account.label } = blame;
const name = viewer != null && c.author.name === viewer ? 'You' : c.author.name;
const authorName = viewer != null && c.author.name === viewer ? 'You' : c.author.name;
const committerName = viewer != null && c.committer.name === viewer ? 'You' : c.committer.name;
let author = authors.get(c.author.name);
let author = authors.get(authorName);
if (author == null) {
author = {
name: c.author.name,
name: authorName,
lineCount: 0,
};
authors.set(name, author);
authors.set(authorName, author);
}
author.lineCount += range.endingLine - range.startingLine + 1;
let commit = commits.get(c.oid);
if (commit == null) {
commit = new GitCommit2(
commit = new GitCommit(
uri.repoPath!,
c.oid,
new GitCommitIdentity(author.name, c.author.email, new Date(c.author.date), c.author.avatarUrl),
new GitCommitIdentity(c.committer.name, c.committer.email, new Date(c.author.date)),
new GitCommitIdentity(authorName, c.author.email, new Date(c.author.date), c.author.avatarUrl),
new GitCommitIdentity(committerName, c.committer.email, new Date(c.author.date)),
c.message.split('\n', 1)[0],
c.parents.nodes[0]?.oid ? [c.parents.nodes[0]?.oid] : [],
c.message,
new GitFileChange(root.toString(), file, GitFileIndexStatus.Modified),
{ changedFiles: c.changedFiles ?? 0, additions: c.additions ?? 0, deletions: c.deletions ?? 0 },
[],
);
@ -434,8 +434,14 @@ export class GitHubGitProvider implements GitProvider, Disposable {
for (let i = range.startingLine; i <= range.endingLine; i++) {
const line: GitCommitLine = {
sha: c.oid,
line: i,
originalLine: i,
from: {
line: i,
count: 1,
},
to: {
line: i,
count: 1,
},
};
commit.lines.push(line);
@ -546,13 +552,13 @@ export class GitHubGitProvider implements GitProvider, Disposable {
const startLine = range.start.line + 1;
const endLine = range.end.line + 1;
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitCommit2>();
const authors = new Map<string, GitBlameAuthor>();
const commits = new Map<string, GitCommit>();
for (const c of blame.commits.values()) {
if (!shas.has(c.sha)) continue;
const commit = c.with({
lines: c.lines.filter(l => l.line >= startLine && l.line <= endLine),
lines: c.lines.filter(l => l.to.line >= startLine && l.to.line <= endLine),
});
commits.set(c.sha, commit);
@ -679,7 +685,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
}
@log()
async getCommit(repoPath: string, ref: string): Promise<GitLogCommit | undefined> {
async getCommit(repoPath: string, ref: string): Promise<GitCommit | undefined> {
if (repoPath == null) return undefined;
const cc = Logger.getCorrelationContext();
@ -691,33 +697,39 @@ export class GitHubGitProvider implements GitProvider, Disposable {
if (commit == null) return undefined;
const { viewer = session.account.label } = commit;
const name = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const authorName = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const committerName = viewer != null && commit.committer.name === viewer ? 'You' : commit.committer.name;
const { files } = commit;
return new GitLogCommit(
GitCommitType.Log,
return new GitCommit(
repoPath,
commit.oid,
name,
commit.author.email,
new Date(commit.author.date),
new Date(commit.committer.date),
commit.message,
'',
files?.map<GitFile>(f => ({
status: fromCommitFileStatus(f.status) ?? GitFileIndexStatus.Modified,
repoPath: repoPath,
fileName: f.filename ?? '',
originalFileName: f.previous_filename,
})) ?? [],
undefined,
undefined,
commit.parents.nodes[0]?.oid,
undefined,
undefined,
new GitCommitIdentity(
authorName,
commit.author.email,
new Date(commit.author.date),
commit.author.avatarUrl,
),
new GitCommitIdentity(committerName, commit.committer.email, new Date(commit.committer.date)),
commit.message.split('\n', 1)[0],
commit.parents.nodes.map(p => p.oid),
undefined,
commit.message,
commit.files?.map(
f =>
new GitFileChange(
repoPath,
f.filename ?? '',
fromCommitFileStatus(f.status) ?? GitFileIndexStatus.Modified,
f.previous_filename,
undefined,
{ additions: f.additions ?? 0, deletions: f.deletions ?? 0, changes: f.changes ?? 0 },
),
) ?? [],
{
changedFiles: commit.changedFiles ?? 0,
additions: commit.additions ?? 0,
deletions: commit.deletions ?? 0,
},
[],
);
} catch (ex) {
Logger.error(ex, cc);
@ -744,8 +756,8 @@ export class GitHubGitProvider implements GitProvider, Disposable {
async getCommitForFile(
repoPath: string | undefined,
uri: Uri,
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range; reverse?: boolean },
): Promise<GitLogCommit | undefined> {
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range },
): Promise<GitCommit | undefined> {
if (repoPath == null) return undefined;
const cc = Logger.getCorrelationContext();
@ -765,37 +777,42 @@ export class GitHubGitProvider implements GitProvider, Disposable {
if (commit == null) return undefined;
const { viewer = session.account.label } = commit;
const name = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const authorName = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const committerName = viewer != null && commit.committer.name === viewer ? 'You' : commit.committer.name;
const files = commit.files?.map(
f =>
new GitFileChange(
repoPath,
f.filename ?? '',
fromCommitFileStatus(f.status) ?? GitFileIndexStatus.Modified,
f.previous_filename,
undefined,
{ additions: f.additions ?? 0, deletions: f.deletions ?? 0, changes: f.changes ?? 0 },
),
);
const foundFile = files?.find(f => f.path === file);
return new GitLogCommit(
GitCommitType.LogFile,
return new GitCommit(
repoPath,
commit.oid,
name,
commit.author.email,
new Date(commit.author.date),
new Date(commit.committer.date),
commit.message,
file,
commit.files?.map<GitFile>(f => ({
status: fromCommitFileStatus(f.status) ?? GitFileIndexStatus.Modified,
repoPath: repoPath,
fileName: f.filename ?? '',
originalFileName: f.previous_filename,
})) ?? [
{
fileName: file,
status: GitFileIndexStatus.Modified,
repoPath: repoPath,
},
],
GitFileIndexStatus.Modified,
undefined,
commit.parents.nodes[0]?.oid,
undefined,
undefined,
new GitCommitIdentity(
authorName,
commit.author.email,
new Date(commit.author.date),
commit.author.avatarUrl,
),
new GitCommitIdentity(committerName, commit.committer.email, new Date(commit.committer.date)),
commit.message.split('\n', 1)[0],
commit.parents.nodes.map(p => p.oid),
undefined,
commit.message,
{ file: foundFile, files: files },
{
changedFiles: commit.changedFiles ?? 0,
additions: commit.additions ?? 0,
deletions: commit.deletions ?? 0,
},
[],
);
} catch (ex) {
Logger.error(ex, cc);
@ -933,7 +950,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
since?: string;
},
): Promise<GitLog | undefined> {
@ -953,42 +969,60 @@ export class GitHubGitProvider implements GitProvider, Disposable {
cursor: options?.cursor ?? options?.since,
});
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitLogCommit>();
const authors = new Map<string, GitBlameAuthor>();
const commits = new Map<string, GitCommit>();
const { viewer = session.account.label } = result;
for (const commit of result.values) {
const name = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const authorName = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const committerName =
viewer != null && commit.committer.name === viewer ? 'You' : commit.committer.name;
let author = authors.get(commit.author.name);
let author = authors.get(authorName);
if (author == null) {
author = {
name: commit.author.name,
name: authorName,
lineCount: 0,
};
authors.set(name, author);
authors.set(authorName, author);
}
let c = commits.get(commit.oid);
if (c == null) {
c = new GitLogCommit(
GitCommitType.Log,
c = new GitCommit(
repoPath,
commit.oid,
name,
commit.author.email,
new Date(commit.author.date),
new Date(commit.committer.date),
new GitCommitIdentity(
authorName,
commit.author.email,
new Date(commit.author.date),
commit.author.avatarUrl,
),
new GitCommitIdentity(committerName, commit.committer.email, new Date(commit.committer.date)),
commit.message.split('\n', 1)[0],
commit.parents.nodes.map(p => p.oid),
commit.message,
'',
commit.files?.map(
f =>
new GitFileChange(
repoPath,
f.filename ?? '',
fromCommitFileStatus(f.status) ?? GitFileIndexStatus.Modified,
f.previous_filename,
undefined,
{
additions: f.additions ?? 0,
deletions: f.deletions ?? 0,
changes: f.changes ?? 0,
},
),
),
{
changedFiles: commit.changedFiles ?? 0,
additions: commit.additions ?? 0,
deletions: commit.deletions ?? 0,
},
[],
undefined,
undefined,
commit.parents.nodes[0]?.oid,
undefined,
undefined,
commit.parents.nodes.map(p => p.oid),
undefined,
);
commits.set(commit.oid, c);
}
@ -996,7 +1030,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
const log: GitLog = {
repoPath: repoPath,
authors: authors,
commits: commits,
sha: ref,
range: undefined,
@ -1029,7 +1062,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
since?: string;
},
): Promise<Set<string> | undefined> {
@ -1048,7 +1080,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
merges?: boolean;
ordering?: string | null;
ref?: string;
reverse?: boolean;
},
): (limit: number | { until: string } | undefined) => Promise<GitLog> {
return async (limit: number | { until: string } | undefined) => {
@ -1090,22 +1121,10 @@ export class GitHubGitProvider implements GitProvider, Disposable {
// If we can't find any more, assume we have everything
if (moreLog == null) return { ...log, hasMore: false };
// Merge authors
const authors = new Map([...log.authors]);
for (const [key, addAuthor] of moreLog.authors) {
const author = authors.get(key);
if (author == null) {
authors.set(key, addAuthor);
} else {
author.lineCount += addAuthor.lineCount;
}
}
const commits = new Map([...log.commits, ...moreLog.commits]);
const mergedLog: GitLog = {
repoPath: log.repoPath,
authors: authors,
commits: commits,
sha: log.sha,
range: undefined,
@ -1156,21 +1175,25 @@ export class GitHubGitProvider implements GitProvider, Disposable {
options = { reverse: false, ...options };
if (options.renames == null) {
options.renames = this.container.config.advanced.fileHistoryFollowsRenames;
}
// Not currently supported
options.renames = false;
options.all = false;
// if (options.renames == null) {
// options.renames = this.container.config.advanced.fileHistoryFollowsRenames;
// }
let key = 'log';
if (options.ref != null) {
key += `:${options.ref}`;
}
if (options.all == null) {
options.all = this.container.config.advanced.fileHistoryShowAllBranches;
}
if (options.all) {
key += ':all';
}
// if (options.all == null) {
// options.all = this.container.config.advanced.fileHistoryShowAllBranches;
// }
// if (options.all) {
// key += ':all';
// }
options.limit = options.limit == null ? this.container.config.advanced.maxListItems || 0 : options.limit;
if (options.limit) {
@ -1193,6 +1216,10 @@ export class GitHubGitProvider implements GitProvider, Disposable {
key += `:skip${options.skip}`;
}
if (options.cursor) {
key += `:cursor=${options.cursor}`;
}
const doc = await this.container.tracker.getOrAdd(GitUri.fromFile(path, repoPath!, options.ref));
if (!options.force && options.range == null) {
if (doc.state != null) {
@ -1221,9 +1248,8 @@ export class GitHubGitProvider implements GitProvider, Disposable {
// Create a copy of the log starting at the requested commit
let skip = true;
let i = 0;
const authors = new Map<string, GitAuthor>();
const commits = new Map(
filterMap<[string, GitLogCommit], [string, GitLogCommit]>(
filterMap<[string, GitCommit], [string, GitCommit]>(
log.commits.entries(),
([ref, c]) => {
if (skip) {
@ -1236,7 +1262,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
return undefined;
}
authors.set(c.author.name, log.authors.get(c.author.name)!);
return [ref, c];
},
),
@ -1248,7 +1273,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
limit: options.limit,
count: commits.size,
commits: commits,
authors: authors,
query: (limit: number | undefined) =>
this.getLogForFile(repoPath, path, { ...opts, limit: limit }),
};
@ -1282,7 +1306,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
private async getLogForFileCore(
repoPath: string | undefined,
fileName: string,
path: string,
document: TrackedDocument<GitDocumentState>,
key: string,
cc: LogCorrelationContext | undefined,
@ -1308,7 +1332,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
if (context == null) return undefined;
const { metadata, github, remotehub, session } = context;
const uri = this.getAbsoluteUri(fileName, repoPath);
const uri = this.getAbsoluteUri(path, repoPath);
const file = this.getRelativePath(uri, remotehub.getProviderRootUri(uri));
// if (range != null && range.start.line > range.end.line) {
@ -1323,48 +1347,62 @@ export class GitHubGitProvider implements GitProvider, Disposable {
cursor: options?.cursor ?? options?.since,
});
const authors = new Map<string, GitAuthor>();
const commits = new Map<string, GitLogCommit>();
const authors = new Map<string, GitBlameAuthor>();
const commits = new Map<string, GitCommit>();
const { viewer = session.account.label } = result;
for (const commit of result.values) {
const name = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const authorName = viewer != null && commit.author.name === viewer ? 'You' : commit.author.name;
const committerName =
viewer != null && commit.committer.name === viewer ? 'You' : commit.committer.name;
let author = authors.get(commit.author.name);
let author = authors.get(authorName);
if (author == null) {
author = {
name: commit.author.name,
name: authorName,
lineCount: 0,
};
authors.set(name, author);
authors.set(authorName, author);
}
let c = commits.get(commit.oid);
if (c == null) {
c = new GitLogCommit(
isFolderGlob(file) ? GitCommitType.Log : GitCommitType.LogFile,
const files = commit.files?.map(
f =>
new GitFileChange(
repoPath,
f.filename ?? '',
fromCommitFileStatus(f.status) ?? GitFileIndexStatus.Modified,
f.previous_filename,
undefined,
{ additions: f.additions ?? 0, deletions: f.deletions ?? 0, changes: f.changes ?? 0 },
),
);
const foundFile = isFolderGlob(file)
? undefined
: files?.find(f => f.path === file) ??
new GitFileChange(repoPath, file, GitFileIndexStatus.Modified);
c = new GitCommit(
repoPath,
commit.oid,
name,
commit.author.email,
new Date(commit.author.date),
new Date(commit.committer.date),
commit.message,
file,
[
{
fileName: file,
status: GitFileIndexStatus.Modified,
repoPath: repoPath,
},
],
GitFileIndexStatus.Modified,
undefined,
commit.parents.nodes[0]?.oid,
undefined,
undefined,
new GitCommitIdentity(
authorName,
commit.author.email,
new Date(commit.author.date),
commit.author.avatarUrl,
),
new GitCommitIdentity(committerName, commit.committer.email, new Date(commit.committer.date)),
commit.message.split('\n', 1)[0],
commit.parents.nodes.map(p => p.oid),
undefined,
commit.message,
{ file: foundFile, files: files },
{
changedFiles: commit.changedFiles ?? 0,
additions: commit.additions ?? 0,
deletions: commit.deletions ?? 0,
},
[],
);
commits.set(commit.oid, c);
}
@ -1372,7 +1410,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
const log: GitLog = {
repoPath: repoPath,
authors: authors,
commits: commits,
sha: ref,
range: undefined,
@ -1380,12 +1417,11 @@ export class GitHubGitProvider implements GitProvider, Disposable {
limit: limit,
hasMore: result.paging?.more ?? false,
cursor: result.paging?.cursor,
query: (limit: number | undefined) =>
this.getLogForFile(repoPath, fileName, { ...options, limit: limit }),
query: (limit: number | undefined) => this.getLogForFile(repoPath, path, { ...options, limit: limit }),
};
if (log.hasMore) {
log.more = this.getLogForFileMoreFn(log, fileName, options);
log.more = this.getLogForFileMoreFn(log, path, options);
}
return log;
@ -1443,22 +1479,10 @@ export class GitHubGitProvider implements GitProvider, Disposable {
// If we can't find any more, assume we have everything
if (moreLog == null) return { ...log, hasMore: false };
// Merge authors
const authors = new Map([...log.authors]);
for (const [key, addAuthor] of moreLog.authors) {
const author = authors.get(key);
if (author == null) {
authors.set(key, addAuthor);
} else {
author.lineCount += addAuthor.lineCount;
}
}
const commits = new Map([...log.commits, ...moreLog.commits]);
const mergedLog: GitLog = {
repoPath: log.repoPath,
authors: authors,
commits: commits,
sha: log.sha,
range: log.range,
@ -1470,13 +1494,11 @@ export class GitHubGitProvider implements GitProvider, Disposable {
};
// if (options.renames) {
// const renamed = Iterables.find(
// const renamed = find(
// moreLog.commits.values(),
// c => Boolean(c.originalFileName) && c.originalFileName !== fileName,
// c => Boolean(c.file?.originalPath) && c.file?.originalPath !== fileName,
// );
// if (renamed != null) {
// fileName = renamed.originalFileName!;
// }
// fileName = renamed?.file?.originalPath ?? fileName;
// }
mergedLog.more = this.getLogForFileMoreFn(mergedLog, fileName, options);
@ -1569,9 +1591,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
if (ref === GitRevision.deletedOrMissing) return undefined;
const commit = await this.getCommitForFile(repoPath, uri, { ref: `${ref ?? 'HEAD'}^` });
if (commit == null) return undefined;
return GitUri.fromCommit(commit);
return commit?.getGitUri();
}
@log()

+ 2
- 2
src/quickpicks/commitPicker.ts View File

@ -1,7 +1,7 @@
import { Disposable, window } from 'vscode';
import { configuration } from '../configuration';
import { Container } from '../container';
import { GitLog, GitLogCommit, GitStash, GitStashCommit } from '../git/models';
import { GitCommit, GitLog, GitStash, GitStashCommit } from '../git/models';
import { KeyboardScope, Keys } from '../keyboard';
import {
CommandQuickPickItem,
@ -24,7 +24,7 @@ export namespace CommitPicker {
onDidPressKey?(key: Keys, item: CommitQuickPickItem): void | Promise<void>;
showOtherReferences?: CommandQuickPickItem[];
},
): Promise<GitLogCommit | undefined> {
): Promise<GitCommit | undefined> {
const quickpick = window.createQuickPick<CommandQuickPickItem | CommitQuickPickItem | DirectiveQuickPickItem>();
quickpick.ignoreFocusOut = getQuickPickIgnoreFocusOut();

+ 28
- 28
src/quickpicks/commitQuickPickItems.ts View File

@ -3,19 +3,19 @@ import { Commands, GitActions, OpenChangedFilesCommandArgs } from '../commands';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { CommitFormatter } from '../git/formatters';
import { GitFile, GitLogCommit, GitStatusFile } from '../git/models';
import { GitCommit, GitFile, GitStatusFile } from '../git/models';
import { Keys } from '../keyboard';
import { Strings } from '../system';
import { basename } from '../system/path';
import { CommandQuickPickItem } from './quickPicksItems';
export class CommitFilesQuickPickItem extends CommandQuickPickItem {
constructor(readonly commit: GitLogCommit, picked: boolean = true, fileName?: string) {
constructor(readonly commit: GitCommit, picked: boolean = true, fileName?: string) {
super(
{
label: commit.getShortMessage(),
label: commit.summary,
description: CommitFormatter.fromTemplate(`\${author}, \${ago} $(git-commit) \${id}`, commit),
detail: `$(files) ${commit.getFormattedDiffStatus({
detail: `$(files) ${commit.formatStats({
expand: true,
separator: ', ',
empty: 'No files changed',
@ -34,9 +34,9 @@ export class CommitFilesQuickPickItem extends CommandQuickPickItem {
}
export class CommitFileQuickPickItem extends CommandQuickPickItem {
constructor(readonly commit: GitLogCommit, readonly file: GitFile, picked?: boolean) {
constructor(readonly commit: GitCommit, readonly file: GitFile, picked?: boolean) {
super({
label: `${Strings.pad(GitFile.getStatusCodicon(file.status), 0, 2)}${basename(file.fileName)}`,
label: `${Strings.pad(GitFile.getStatusCodicon(file.status), 0, 2)}${basename(file.path)}`,
description: GitFile.getFormattedDirectory(file, true),
picked: picked,
});
@ -51,7 +51,7 @@ export class CommitFileQuickPickItem extends CommandQuickPickItem {
override execute(options?: { preserveFocus?: boolean; preview?: boolean }): Promise<void> {
return GitActions.Commit.openChanges(this.file, this.commit, options);
// const fileCommit = this.commit.toFileCommit(this.file)!;
// const fileCommit = await this.commit.getCommitForFile(this.file)!;
// if (fileCommit.previousSha === undefined) {
// void (await findOrOpenEditor(
@ -72,7 +72,7 @@ export class CommitFileQuickPickItem extends CommandQuickPickItem {
export class CommitBrowseRepositoryFromHereCommandQuickPickItem extends CommandQuickPickItem {
constructor(
private readonly commit: GitLogCommit,
private readonly commit: GitCommit,
private readonly executeOptions?: {
before?: boolean;
openInNewWindow: boolean;
@ -88,7 +88,7 @@ export class CommitBrowseRepositoryFromHereCommandQuickPickItem extends CommandQ
}
override execute(_options: { preserveFocus?: boolean; preview?: boolean }): Promise<void> {
return GitActions.browseAtRevision(this.commit.toGitUri(), {
return GitActions.browseAtRevision(this.commit.getGitUri(), {
before: this.executeOptions?.before,
openInNewWindow: this.executeOptions?.openInNewWindow,
});
@ -96,7 +96,7 @@ export class CommitBrowseRepositoryFromHereCommandQuickPickItem extends CommandQ
}
export class CommitCompareWithHEADCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(compare-changes) Compare with HEAD');
}
@ -106,7 +106,7 @@ export class CommitCompareWithHEADCommandQuickPickItem extends CommandQuickPickI
}
export class CommitCompareWithWorkingCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(compare-changes) Compare with Working Tree');
}
@ -116,7 +116,7 @@ export class CommitCompareWithWorkingCommandQuickPickItem extends CommandQuickPi
}
export class CommitCopyIdQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(clippy) Copy SHA');
}
@ -131,7 +131,7 @@ export class CommitCopyIdQuickPickItem extends CommandQuickPickItem {
}
export class CommitCopyMessageQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(clippy) Copy Message');
}
@ -142,13 +142,13 @@ export class CommitCopyMessageQuickPickItem extends CommandQuickPickItem {
override async onDidPressKey(key: Keys): Promise<void> {
await super.onDidPressKey(key);
void window.showInformationMessage(
`${this.commit.isStash ? 'Stash' : 'Commit'} Message copied to the clipboard`,
`${this.commit.stashName ? 'Stash' : 'Commit'} Message copied to the clipboard`,
);
}
}
export class CommitOpenAllChangesCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open All Changes');
}
@ -158,7 +158,7 @@ export class CommitOpenAllChangesCommandQuickPickItem extends CommandQuickPickIt
}
export class CommitOpenAllChangesWithDiffToolCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open All Changes (difftool)');
}
@ -168,7 +168,7 @@ export class CommitOpenAllChangesWithDiffToolCommandQuickPickItem extends Comman
}
export class CommitOpenAllChangesWithWorkingCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open All Changes with Working Tree');
}
@ -178,7 +178,7 @@ export class CommitOpenAllChangesWithWorkingCommandQuickPickItem extends Command
}
export class CommitOpenChangesCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open Changes');
}
@ -188,7 +188,7 @@ export class CommitOpenChangesCommandQuickPickItem extends CommandQuickPickItem
}
export class CommitOpenChangesWithDiffToolCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open Changes (difftool)');
}
@ -198,7 +198,7 @@ export class CommitOpenChangesWithDiffToolCommandQuickPickItem extends CommandQu
}
export class CommitOpenChangesWithWorkingCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open Changes with Working File');
}
@ -208,7 +208,7 @@ export class CommitOpenChangesWithWorkingCommandQuickPickItem extends CommandQui
}
export class CommitOpenDirectoryCompareCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open Directory Compare');
}
@ -218,7 +218,7 @@ export class CommitOpenDirectoryCompareCommandQuickPickItem extends CommandQuick
}
export class CommitOpenDirectoryCompareWithWorkingCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(git-compare) Open Directory Compare with Working Tree');
}
@ -228,7 +228,7 @@ export class CommitOpenDirectoryCompareWithWorkingCommandQuickPickItem extends C
}
export class CommitOpenFilesCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(files) Open Files');
}
@ -238,7 +238,7 @@ export class CommitOpenFilesCommandQuickPickItem extends CommandQuickPickItem {
}
export class CommitOpenFileCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(item ?? '$(file) Open File');
}
@ -248,7 +248,7 @@ export class CommitOpenFileCommandQuickPickItem extends CommandQuickPickItem {
}
export class CommitOpenRevisionsCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, item?: QuickPickItem) {
super(item ?? '$(files) Open Files at Revision');
}
@ -258,7 +258,7 @@ export class CommitOpenRevisionsCommandQuickPickItem extends CommandQuickPickIte
}
export class CommitOpenRevisionCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(item ?? '$(file) Open File at Revision');
}
@ -268,7 +268,7 @@ export class CommitOpenRevisionCommandQuickPickItem extends CommandQuickPickItem
}
export class CommitApplyFileChangesCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(item ?? 'Apply Changes');
}
@ -278,7 +278,7 @@ export class CommitApplyFileChangesCommandQuickPickItem extends CommandQuickPick
}
export class CommitRestoreFileChangesCommandQuickPickItem extends CommandQuickPickItem {
constructor(private readonly commit: GitLogCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
constructor(private readonly commit: GitCommit, private readonly file: string | GitFile, item?: QuickPickItem) {
super(
item ?? {
label: 'Restore',

+ 12
- 13
src/quickpicks/gitQuickPickItems.ts View File

@ -5,12 +5,11 @@ import { Container } from '../container';
import { emojify } from '../emojis';
import {
GitBranch,
GitCommit,
GitContributor,
GitLogCommit,
GitReference,
GitRemoteType,
GitRevision,
GitStashCommit,
GitTag,
Repository,
} from '../git/models';
@ -142,21 +141,21 @@ export class CommitLoadMoreQuickPickItem implements QuickPickItem {
readonly alwaysShow = true;
}
export type CommitQuickPickItem<T extends GitLogCommit = GitLogCommit> = QuickPickItemOfT<T>;
export type CommitQuickPickItem<T extends GitCommit = GitCommit> = QuickPickItemOfT<T>;
export namespace CommitQuickPickItem {
export function create<T extends GitLogCommit = GitLogCommit>(
export function create<T extends GitCommit = GitCommit>(
commit: T,
picked?: boolean,
options: { alwaysShow?: boolean; buttons?: QuickInputButton[]; compact?: boolean; icon?: boolean } = {},
) {
if (GitStashCommit.is(commit)) {
if (GitCommit.isStash(commit)) {
const number = commit.number == null ? '' : `${commit.number}: `;
if (options.compact) {
const item: CommitQuickPickItem<T> = {
label: `${options.icon ? pad('$(archive)', 0, 2) : ''}${number}${commit.getShortMessage()}`,
description: `${commit.formattedDate}${pad(GlyphChars.Dot, 2, 2)}${commit.getFormattedDiffStatus({
label: `${options.icon ? pad('$(archive)', 0, 2) : ''}${number}${commit.summary}`,
description: `${commit.formattedDate}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({
compact: true,
})}`,
alwaysShow: options.alwaysShow,
@ -169,13 +168,13 @@ export namespace CommitQuickPickItem {
}
const item: CommitQuickPickItem<T> = {
label: `${options.icon ? pad('$(archive)', 0, 2) : ''}${number}${commit.getShortMessage()}`,
label: `${options.icon ? pad('$(archive)', 0, 2) : ''}${number}${commit.summary}`,
description: '',
detail: `${GlyphChars.Space.repeat(2)}${commit.formattedDate}${pad(
GlyphChars.Dot,
2,
2,
)}${commit.getFormattedDiffStatus({ compact: true })}`,
)}${commit.formatStats({ compact: true })}`,
alwaysShow: options.alwaysShow,
buttons: options.buttons,
picked: picked,
@ -187,10 +186,10 @@ export namespace CommitQuickPickItem {
if (options.compact) {
const item: CommitQuickPickItem<T> = {
label: `${options.icon ? pad('$(git-commit)', 0, 2) : ''}${commit.getShortMessage()}`,
label: `${options.icon ? pad('$(git-commit)', 0, 2) : ''}${commit.summary}`,
description: `${commit.author.name}, ${commit.formattedDate}${pad('$(git-commit)', 2, 2)}${
commit.shortSha
}${pad(GlyphChars.Dot, 2, 2)}${commit.getFormattedDiffStatus({ compact: true })}`,
}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ compact: true })}`,
alwaysShow: options.alwaysShow,
buttons: options.buttons,
picked: picked,
@ -200,13 +199,13 @@ export namespace CommitQuickPickItem {
}
const item: CommitQuickPickItem<T> = {
label: `${options.icon ? pad('$(git-commit)', 0, 2) : ''}${commit.getShortMessage()}`,
label: `${options.icon ? pad('$(git-commit)', 0, 2) : ''}${commit.summary}`,
description: '',
detail: `${GlyphChars.Space.repeat(2)}${commit.author.name}, ${commit.formattedDate}${pad(
'$(git-commit)',
2,
2,
)}${commit.shortSha}${pad(GlyphChars.Dot, 2, 2)}${commit.getFormattedDiffStatus({
)}${commit.shortSha}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({
compact: true,
})}`,
alwaysShow: options.alwaysShow,

+ 2
- 2
src/quickpicks/quickPicksItems.ts View File

@ -1,7 +1,7 @@
import { commands, QuickPickItem } from 'vscode';
import { Commands, GitActions } from '../commands';
import { Container } from '../container';
import { GitReference, GitRevisionReference, GitStashCommit } from '../git/models';
import { GitCommit, GitReference, GitRevisionReference } from '../git/models';
import { SearchPattern } from '../git/search';
import { Keys } from '../keyboard';
@ -199,7 +199,7 @@ export class RevealInSideBarQuickPickItem extends CommandQuickPickItem {
}
override async execute(options?: { preserveFocus?: boolean; preview?: boolean }): Promise<void> {
if (GitStashCommit.is(this.reference)) {
if (GitCommit.isStash(this.reference)) {
void (await GitActions.Stash.reveal(this.reference, {
select: true,
focus: !(options?.preserveFocus ?? false),

+ 33
- 29
src/statusbar/statusBarController.ts View File

@ -15,7 +15,7 @@ import { configuration, FileAnnotationType, StatusBarCommand } from '../configur
import { GlyphChars, isTextEditor } from '../constants';
import { Container } from '../container';
import { CommitFormatter } from '../git/formatters';
import { GitCommit2, PullRequest } from '../git/models';
import { GitCommit, PullRequest } from '../git/models';
import { Hovers } from '../hovers/hovers';
import { LogCorrelationContext, Logger } from '../logger';
import { debug } from '../system/decorators/log';
@ -172,7 +172,7 @@ export class StatusBarController implements Disposable {
}
@debug({ args: false })
private async updateBlame(editor: TextEditor, commit: GitCommit2, options?: { pr?: PullRequest | null }) {
private async updateBlame(editor: TextEditor, commit: GitCommit, options?: { pr?: PullRequest | null }) {
const cfg = this.container.config.statusBar;
if (!cfg.enabled || this._statusBarBlame == null || !isTextEditor(editor)) return;
@ -270,32 +270,36 @@ export class StatusBarController implements Disposable {
tooltip = 'Click to Toggle File Blame';
break;
case StatusBarCommand.ToggleFileChanges: {
this._statusBarBlame.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({
title: 'Toggle File Changes',
command: Commands.ToggleFileChanges,
arguments: [
commit.uri,
{
type: FileAnnotationType.Changes,
context: { sha: commit.sha, only: false, selection: false },
},
],
});
if (commit.file != null) {
this._statusBarBlame.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({
title: 'Toggle File Changes',
command: Commands.ToggleFileChanges,
arguments: [
commit.file.uri,
{
type: FileAnnotationType.Changes,
context: { sha: commit.sha, only: false, selection: false },
},
],
});
}
tooltip = 'Click to Toggle File Changes';
break;
}
case StatusBarCommand.ToggleFileChangesOnly: {
this._statusBarBlame.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({
title: 'Toggle File Changes',
command: Commands.ToggleFileChanges,
arguments: [
commit.uri,
{
type: FileAnnotationType.Changes,
context: { sha: commit.sha, only: true, selection: false },
},
],
});
if (commit.file != null) {
this._statusBarBlame.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({
title: 'Toggle File Changes',
command: Commands.ToggleFileChanges,
arguments: [
commit.file.uri,
{
type: FileAnnotationType.Changes,
context: { sha: commit.sha, only: true, selection: false },
},
],
});
}
tooltip = 'Click to Toggle File Changes';
break;
}
@ -332,7 +336,7 @@ export class StatusBarController implements Disposable {
}
private async getPullRequest(
commit: GitCommit2,
commit: GitCommit,
{ timeout }: { timeout?: number } = {},
): Promise<PullRequest | PromiseCancelledError<Promise<PullRequest | undefined>> | undefined> {
const remote = await this.container.git.getRichRemoteProvider(commit.repoPath);
@ -348,7 +352,7 @@ export class StatusBarController implements Disposable {
private async updateCommitTooltip(
statusBarItem: StatusBarItem,
commit: GitCommit2,
commit: GitCommit,
actionTooltip: string,
getBranchAndTagTips:
| ((
@ -366,8 +370,8 @@ export class StatusBarController implements Disposable {
const tooltip = await Hovers.detailsMessage(
commit,
commit.toGitUri(),
commit.lines[0].line,
commit.getGitUri(),
commit.lines[0].to.line,
this.container.config.statusBar.tooltipFormat,
this.container.config.defaultDateFormat,
{
@ -386,7 +390,7 @@ export class StatusBarController implements Disposable {
private async waitForPendingPullRequest(
editor: TextEditor,
commit: GitCommit2,
commit: GitCommit,
pr: PullRequest | PromiseCancelledError<Promise<PullRequest | undefined>> | undefined,
cancellationToken: CancellationToken,
timeout: number,

+ 3
- 3
src/system/array.ts View File

@ -206,15 +206,15 @@ export function compactHierarchy(
export function uniqueBy<TKey, TValue>(
source: TValue[],
uniqueKey: (item: TValue) => TKey,
onDeduplicate: (original: TValue, current: TValue) => TValue | void,
) {
onDuplicate: (original: TValue, current: TValue) => TValue | void,
): TValue[] {
const map = source.reduce((uniques, current) => {
const value = uniqueKey(current);
const original = uniques.get(value);
if (original === undefined) {
uniques.set(value, current);
} else {
const updated = onDeduplicate(original, current);
const updated = onDuplicate(original, current);
if (updated !== undefined) {
uniques.set(value, updated);
}

+ 24
- 0
src/system/iterable.ts View File

@ -204,3 +204,27 @@ export function* union(...sources: (Iterable | IterableIterator)[]): It
}
}
}
export function uniqueBy<TKey, TValue>(
source: Iterable<TValue> | IterableIterator<TValue>,
uniqueKey: (item: TValue) => TKey,
onDuplicate: (original: TValue, current: TValue) => TValue | void,
): IterableIterator<TValue> {
const uniques = new Map<TKey, TValue>();
for (const current of source) {
const value = uniqueKey(current);
const original = uniques.get(value);
if (original === undefined) {
uniques.set(value, current);
} else {
const updated = onDuplicate(original, current);
if (updated !== undefined) {
uniques.set(value, updated);
}
}
}
return uniques.values();
}

+ 1
- 1
src/system/promise.ts View File

@ -52,7 +52,7 @@ export function cancellable(
onDidCancel?(resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void): void;
} = {},
): Promise<T> {
if (timeoutOrToken == null) return promise;
if (timeoutOrToken == null || (typeof timeoutOrToken === 'number' && timeoutOrToken <= 0)) return promise;
return new Promise((resolve, reject) => {
let fulfilled = false;

+ 7
- 3
src/trackers/gitLineTracker.ts View File

@ -1,7 +1,7 @@
import { Disposable, TextEditor } from 'vscode';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { GitCommit2, GitLogCommit } from '../git/models';
import { GitCommit } from '../git/models';
import { Logger } from '../logger';
import { debug } from '../system';
import {
@ -16,7 +16,11 @@ import { LinesChangeEvent, LineSelection, LineTracker } from './lineTracker';
export * from './lineTracker';
export class GitLineState {
constructor(public readonly commit: GitCommit2 | undefined, public logCommit?: GitLogCommit) {}
constructor(public readonly commit: GitCommit | undefined) {
if (commit != null && commit.file == null) {
debugger;
}
}
}
export class GitLineTracker extends LineTracker<GitLineState> {
@ -159,7 +163,7 @@ export class GitLineTracker extends LineTracker {
return false;
}
this.setState(blameLine.line.line - 1, new GitLineState(blameLine.commit));
this.setState(blameLine.line.to.line - 1, new GitLineState(blameLine.commit));
} else {
const blame = editor.document.isDirty
? await this.container.git.getBlameForFileContents(trackedDocument.uri, editor.document.getText())

+ 2
- 2
src/views/branchesView.ts View File

@ -19,7 +19,7 @@ import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import {
GitBranchReference,
GitLogCommit,
GitCommit,
GitReference,
GitRevisionReference,
RepositoryChange,
@ -236,7 +236,7 @@ export class BranchesView extends ViewBase
});
}
async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
async findCommit(commit: GitCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
// Get all the branches the commit is on

+ 2
- 2
src/views/commitsView.ts View File

@ -13,7 +13,7 @@ import { ContextKeys, GlyphChars, setContext } from '../constants';
import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import {
GitLogCommit,
GitCommit,
GitReference,
GitRevisionReference,
Repository,
@ -281,7 +281,7 @@ export class CommitsView extends ViewBase {
return true;
}
async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
async findCommit(commit: GitCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
const branch = await this.container.git.getBranch(commit.repoPath);

+ 26
- 15
src/views/nodes/branchTrackingStatusFilesNode.ts View File

@ -2,8 +2,11 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ViewFilesLayout } from '../../configuration';
import { GitUri } from '../../git/gitUri';
import { GitBranch, GitFileWithCommit, GitRevision } from '../../git/models';
import { Arrays, Iterables, Strings } from '../../system';
import { Strings } from '../../system';
import { groupBy, makeHierarchical } from '../../system/array';
import { filter, flatMap, map } from '../../system/iterable';
import { joinPaths, normalizePath } from '../../system/path';
import { sortCompare } from '../../system/string';
import { ViewsWithCommits } from '../viewBase';
import { BranchNode } from './branchNode';
import { BranchTrackingStatus } from './branchTrackingStatusNode';
@ -52,21 +55,29 @@ export class BranchTrackingStatusFilesNode extends ViewNode {
),
});
const files =
log != null
? [
...Iterables.flatMap(log.commits.values(), c =>
c.files.map(s => {
const file: GitFileWithCommit = { ...s, commit: c };
return file;
}),
),
]
: [];
let files: GitFileWithCommit[];
if (log != null) {
await Promise.allSettled(
map(
filter(log.commits.values(), c => c.files == null),
c => c.ensureFullDetails(),
),
);
files = [
...flatMap(
log.commits.values(),
c => c.files?.map<GitFileWithCommit>(f => ({ ...f, commit: c })) ?? [],
),
];
} else {
files = [];
}
files.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
const groups = Arrays.groupBy(files, s => s.fileName);
const groups = groupBy(files, s => s.path);
let children: FileNode[] = Object.values(groups).map(
files =>
@ -80,7 +91,7 @@ export class BranchTrackingStatusFilesNode extends ViewNode {
);
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(
const hierarchy = makeHierarchical(
children,
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
@ -90,7 +101,7 @@ export class BranchTrackingStatusFilesNode extends ViewNode {
const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy, false);
children = root.getChildren() as FileNode[];
} else {
children.sort((a, b) => a.priority - b.priority || Strings.sortCompare(a.label!, b.label!));
children.sort((a, b) => a.priority - b.priority || sortCompare(a.label!, b.label!));
}
return children;

+ 18
- 9
src/views/nodes/commitFileNode.ts View File

@ -1,8 +1,8 @@
import { Command, Selection, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Command, MarkdownString, Selection, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { StatusFileFormatter } from '../../git/formatters';
import { GitUri } from '../../git/gitUri';
import { GitBranch, GitFile, GitLogCommit, GitRevisionReference } from '../../git/models';
import { GitBranch, GitCommit, GitFile, GitRevisionReference } from '../../git/models';
import { dirname, joinPaths } from '../../system/path';
import { FileHistoryView } from '../fileHistoryView';
import { View, ViewsWithCommits } from '../viewBase';
@ -13,7 +13,7 @@ export class CommitFileNode
view: TView,
parent: ViewNode,
public readonly file: GitFile,
public commit: GitLogCommit,
public commit: GitCommit,
private readonly _options: {
branch?: GitBranch;
selection?: Selection;
@ -28,7 +28,7 @@ export class CommitFileNode
}
get fileName(): string {
return this.file.fileName;
return this.file.path;
}
get priority(): number {
@ -44,11 +44,11 @@ export class CommitFileNode
}
async getTreeItem(): Promise<TreeItem> {
if (!this.commit.isFile) {
if (this.commit.file == null) {
// Try to get the commit directly from the multi-file commit
const commit = this.commit.toFileCommit(this.file);
const commit = await this.commit.getCommitForFile(this.file);
if (commit == null) {
const log = await this.view.container.git.getLogForFile(this.repoPath, this.file.fileName, {
const log = await this.view.container.git.getLogForFile(this.repoPath, this.file.path, {
limit: 2,
ref: this.commit.sha,
});
@ -124,7 +124,16 @@ export class CommitFileNode
}
private get tooltip() {
return StatusFileFormatter.fromTemplate(`\${file}\n\${directory}/\n\n\${status}\${ (originalPath)}`, this.file);
const tooltip = StatusFileFormatter.fromTemplate(
`\${file}\${'&nbsp;&nbsp;\u2022&nbsp;&nbsp;'changesDetail}\\\\\n\${directory}\n\n\${status}\${ (originalPath)}`,
this.file,
);
const markdown = new MarkdownString(tooltip, true);
markdown.supportHtml = true;
markdown.isTrusted = true;
return markdown;
}
override getCommand(): Command | undefined {
@ -132,7 +141,7 @@ export class CommitFileNode
if (this.commit.lines.length) {
line = this.commit.lines[0].to.line - 1;
} else {
line = this._options.selection !== undefined ? this._options.selection.active.line : 0;
line = this._options.selection?.active.line ?? 0;
}
const commandArgs: DiffWithPreviousCommandArgs = {

+ 16
- 14
src/views/nodes/commitNode.ts View File

@ -1,9 +1,9 @@
import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { ViewFilesLayout } from '../../configuration';
import { Colors, GlyphChars } from '../../constants';
import { Colors } from '../../constants';
import { CommitFormatter } from '../../git/formatters';
import { GitBranch, GitLogCommit, GitRevisionReference } from '../../git/models';
import { GitBranch, GitCommit, GitRevisionReference } from '../../git/models';
import { Arrays, Strings } from '../../system';
import { joinPaths, normalizePath } from '../../system/path';
import { FileHistoryView } from '../fileHistoryView';
@ -18,23 +18,17 @@ export class CommitNode extends ViewRefNode
constructor(
view: ViewsWithCommits | FileHistoryView,
parent: ViewNode,
public readonly commit: GitLogCommit,
public readonly commit: GitCommit,
private readonly unpublished?: boolean,
public readonly branch?: GitBranch,
private readonly getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined,
private readonly _options: { expand?: boolean } = {},
) {
super(commit.toGitUri(), view, parent);
super(commit.getGitUri(), view, parent);
}
override toClipboard(): string {
let message = this.commit.message;
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
return `${this.commit.shortSha}: ${message}`;
return `${this.commit.shortSha}: ${this.commit.summary}`;
}
get isTip(): boolean {
@ -48,8 +42,9 @@ export class CommitNode extends ViewRefNode
async getChildren(): Promise<ViewNode[]> {
const commit = this.commit;
let children: (PullRequestNode | FileNode)[] = commit.files.map(
s => new CommitFileNode(this.view, this, s, commit.toFileCommit(s)!),
const commits = await commit.getCommitsForFiles();
let children: (PullRequestNode | FileNode)[] = commits.map(
c => new CommitFileNode(this.view, this, c.file!, c),
);
if (this.view.config.files.layout !== ViewFilesLayout.List) {
@ -137,12 +132,19 @@ export class CommitNode extends ViewRefNode
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath);
const remote = await this.view.container.git.getRichRemoteProvider(remotes);
if (this.commit.message == null) {
await this.commit.ensureFullDetails();
}
let autolinkedIssuesOrPullRequests;
let pr;
if (remote?.provider != null) {
[autolinkedIssuesOrPullRequests, pr] = await Promise.all([
this.view.container.autolinks.getIssueOrPullRequestLinks(this.commit.message, remote),
this.view.container.autolinks.getIssueOrPullRequestLinks(
this.commit.message ?? this.commit.summary,
remote,
),
this.view.container.git.getPullRequestForCommit(this.commit.ref, remote.provider),
]);
}

+ 1
- 1
src/views/nodes/compareBranchNode.ts View File

@ -254,7 +254,7 @@ export class CompareBranchNode extends ViewNode
if (workingFiles != null) {
if (files != null) {
for (const wf of workingFiles) {
const index = files.findIndex(f => f.fileName === wf.fileName);
const index = files.findIndex(f => f.path === wf.path);
if (index !== -1) {
files.splice(index, 1, wf);
} else {

+ 2
- 2
src/views/nodes/compareResultsNode.ts View File

@ -235,7 +235,7 @@ export class CompareResultsNode extends ViewNode {
if (workingFiles != null) {
if (files != null) {
for (const wf of workingFiles) {
const index = files.findIndex(f => f.fileName === wf.fileName);
const index = files.findIndex(f => f.path === wf.path);
if (index !== -1) {
files.splice(index, 1, wf);
} else {
@ -265,7 +265,7 @@ export class CompareResultsNode extends ViewNode {
if (workingFiles != null) {
if (files != null) {
for (const wf of workingFiles) {
const index = files.findIndex(f => f.fileName === wf.fileName);
const index = files.findIndex(f => f.path === wf.path);
if (index !== -1) {
files.splice(index, 1, wf);
} else {

+ 14
- 13
src/views/nodes/fileHistoryNode.ts View File

@ -11,11 +11,10 @@ import {
RepositoryFileSystemChangeEvent,
} from '../../git/models';
import { Logger } from '../../logger';
import { uniqueBy } from '../../system/array';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { filterMap, flatMap } from '../../system/iterable';
import { filterMap, flatMap, map, uniqueBy } from '../../system/iterable';
import { basename, joinPaths } from '../../system/path';
import { FileHistoryView } from '../fileHistoryView';
import { CommitNode } from './commitNode';
@ -79,17 +78,19 @@ export class FileHistoryNode extends SubscribeableViewNode impl
if (fileStatuses?.length) {
if (this.folder) {
const commits = uniqueBy(
[...flatMap(fileStatuses, f => f.toPsuedoCommits(currentUser))],
c => c.sha,
(original, c) => void original.files.push(...c.files),
// Combine all the working/staged changes into single pseudo commits
const commits = map(
uniqueBy(
flatMap(fileStatuses, f => f.getPseudoCommits(currentUser)),
c => c.sha,
(original, c) => original.with({ files: { files: [...original.files!, ...c.files!] } }),
),
commit => new CommitNode(this.view, this, commit),
);
if (commits.length) {
children.push(...commits.map(commit => new CommitNode(this.view, this, commit)));
}
children.push(...commits);
} else {
const [file] = fileStatuses;
const commits = file.toPsuedoCommits(currentUser);
const commits = file.getPseudoCommits(currentUser);
if (commits.length) {
children.push(
...commits.map(commit => new FileRevisionAsCommitNode(this.view, this, file, commit)),
@ -104,7 +105,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl
filterMap(log.commits.values(), c =>
this.folder
? new CommitNode(
this.view as any,
this.view,
this,
c,
unpublishedCommits?.has(c.ref),
@ -114,8 +115,8 @@ export class FileHistoryNode extends SubscribeableViewNode impl
expand: false,
},
)
: c.files.length
? new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
: c.file != null
? new FileRevisionAsCommitNode(this.view, this, c.file, c, {
branch: this.branch,
getBranchAndTagTips: getBranchAndTagTips,
unpublished: unpublishedCommits?.has(c.ref),

+ 23
- 22
src/views/nodes/fileRevisionAsCommitNode.ts View File

@ -9,10 +9,10 @@ import {
Uri,
} from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { Colors, GlyphChars } from '../../constants';
import { Colors } from '../../constants';
import { CommitFormatter, StatusFileFormatter } from '../../git/formatters';
import { GitUri } from '../../git/gitUri';
import { GitBranch, GitFile, GitLogCommit, GitRevisionReference } from '../../git/models';
import { GitBranch, GitCommit, GitFile, GitRevisionReference } from '../../git/models';
import { joinPaths } from '../../system/path';
import { FileHistoryView } from '../fileHistoryView';
import { LineHistoryView } from '../lineHistoryView';
@ -26,7 +26,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
view: ViewsWithCommits | FileHistoryView | LineHistoryView,
parent: ViewNode,
public readonly file: GitFile,
public commit: GitLogCommit,
public commit: GitCommit,
private readonly _options: {
branch?: GitBranch;
getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined;
@ -38,17 +38,11 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}
override toClipboard(): string {
let message = this.commit.message;
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
return `${this.commit.shortSha}: ${message}`;
return `${this.commit.shortSha}: ${this.commit.summary}`;
}
get fileName(): string {
return this.file.fileName;
return this.file.path;
}
get isTip(): boolean {
@ -60,7 +54,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}
async getChildren(): Promise<ViewNode[]> {
if (!this.commit.hasConflicts) return [];
if (!this.commit.file?.hasConflicts) return [];
const [mergeStatus, rebaseStatus] = await Promise.all([
this.view.container.git.getMergeStatus(this.commit.repoPath),
@ -75,11 +69,11 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}
async getTreeItem(): Promise<TreeItem> {
if (!this.commit.isFile) {
if (this.commit.file == null) {
// Try to get the commit directly from the multi-file commit
const commit = this.commit.toFileCommit(this.file);
const commit = await this.commit.getCommitForFile(this.file);
if (commit == null) {
const log = await this.view.container.git.getLogForFile(this.repoPath, this.file.fileName, {
const log = await this.view.container.git.getLogForFile(this.repoPath, this.file.path, {
limit: 2,
ref: this.commit.sha,
});
@ -97,7 +91,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
getBranchAndTagTips: (sha: string) => this._options.getBranchAndTagTips?.(sha, { compact: true }),
messageTruncateAtNewLine: true,
}),
this.commit.hasConflicts ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None,
this.commit.file?.hasConflicts ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None,
);
item.contextValue = this.contextValue;
@ -136,7 +130,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}${this._options.unpublished ? '+unpublished' : ''}`;
}
return this.commit.hasConflicts
return this.commit.file?.hasConflicts
? `${ContextValues.File}+conflicted`
: this.commit.isUncommittedStaged
? `${ContextValues.File}+staged`
@ -148,10 +142,10 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
if (this.commit.lines.length) {
line = this.commit.lines[0].to.line - 1;
} else {
line = this._options.selection !== undefined ? this._options.selection.active.line : 0;
line = this._options.selection?.active.line ?? 0;
}
if (this.commit.hasConflicts) {
if (this.commit.file?.hasConflicts) {
return {
title: 'Open Changes',
command: Commands.DiffWith,
@ -200,7 +194,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}
async getConflictBaseUri(): Promise<Uri | undefined> {
if (!this.commit.hasConflicts) return undefined;
if (!this.commit.file?.hasConflicts) return undefined;
const mergeBase = await this.view.container.git.getMergeBase(this.repoPath, 'MERGE_HEAD', 'HEAD');
return GitUri.fromFile(this.file, this.repoPath, mergeBase ?? 'HEAD');
@ -210,19 +204,26 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath);
const remote = await this.view.container.git.getRichRemoteProvider(remotes);
if (this.commit.message == null) {
await this.commit.ensureFullDetails();
}
let autolinkedIssuesOrPullRequests;
let pr;
if (remote?.provider != null) {
[autolinkedIssuesOrPullRequests, pr] = await Promise.all([
this.view.container.autolinks.getIssueOrPullRequestLinks(this.commit.message, remote),
this.view.container.autolinks.getIssueOrPullRequestLinks(
this.commit.message ?? this.commit.summary,
remote,
),
this.view.container.git.getPullRequestForCommit(this.commit.ref, remote.provider),
]);
}
const status = StatusFileFormatter.fromTemplate(`\${status}\${ (originalPath)}`, this.file);
const tooltip = await CommitFormatter.fromTemplateAsync(
`\${'$(git-commit) 'id }\${' via 'pullRequest} \u2022 ${status}\${ \u2022 changesDetail}\${'&nbsp;&nbsp;&nbsp;'tips}\n\n\${avatar} &nbsp;__\${author}__, \${ago} &nbsp; _(\${date})_ \n\n\${message}\${\n\n---\n\nfootnotes}`,
`\${'\`$(git-commit) 'id\`}\${' via 'pullRequest} \u2022 ${status}\${ \u2022 changesDetail}\${'&nbsp;&nbsp;&nbsp;'tips}\n\n\${avatar} &nbsp;__\${author}__, \${ago} &nbsp; _(\${date})_ \n\n\${message}\${\n\n---\n\nfootnotes}`,
this.commit,
{
autolinkedIssuesOrPullRequests: autolinkedIssuesOrPullRequests,

+ 3
- 3
src/views/nodes/helpers.ts View File

@ -1,4 +1,4 @@
import { GitLogCommit } from '../../git/models';
import { GitCommit } from '../../git/models';
import { MessageNode } from './common';
import { ContextValues, ViewNode } from './viewNode';
@ -9,7 +9,7 @@ const markers: [number, string][] = [
[77, 'Over 3 months ago'],
];
export function* insertDateMarkers<T extends ViewNode & { commit: GitLogCommit }>(
export function* insertDateMarkers<T extends ViewNode & { commit: GitCommit }>(
iterable: Iterable<T>,
parent: ViewNode,
skip?: number,
@ -33,7 +33,7 @@ export function* insertDateMarkers
time = date.setDate(date.getDate() - daysAgo);
}
const date = new Date(node.commit.committerDate).setUTCHours(0, 0, 0, 0);
const date = new Date(node.commit.committer.date).setUTCHours(0, 0, 0, 0);
if (date <= time) {
while (index < markers.length - 1) {
[daysAgo] = markers[index + 1];

+ 35
- 104
src/views/nodes/lineHistoryNode.ts View File

@ -2,11 +2,9 @@ import { Disposable, Selection, TreeItem, TreeItemCollapsibleState, window } fro
import type { GitUri } from '../../git/gitUri';
import {
GitBranch,
GitCommitType,
GitFile,
GitFileIndexStatus,
GitLog,
GitLogCommit,
GitRevision,
RepositoryChange,
RepositoryChangeComparisonMode,
@ -71,7 +69,7 @@ export class LineHistoryNode
const range = this.branch != null ? await this.view.container.git.getBranchAheadRange(this.branch) : undefined;
const [log, blame, getBranchAndTagTips, unpublishedCommits] = await Promise.all([
this.getLog(selection),
this.uri.sha == null
this.uri.sha == null || GitRevision.isUncommitted(this.uri.sha)
? this.editorContents
? await this.view.container.git.getBlameForRangeContents(this.uri, selection, this.editorContents)
: await this.view.container.git.getBlameForRange(this.uri, selection)
@ -87,119 +85,52 @@ export class LineHistoryNode
: undefined,
]);
if (this.uri.sha == null) {
// Check for any uncommitted changes in the range
if (blame != null) {
for (const commit of blame.commits.values()) {
if (!commit.isUncommitted) continue;
// Check for any uncommitted changes in the range
if (blame != null) {
for (const commit of blame.commits.values()) {
if (!commit.isUncommitted) continue;
const firstLine = blame.lines[0];
const lastLine = blame.lines[blame.lines.length - 1];
const firstLine = blame.lines[0];
const lastLine = blame.lines[blame.lines.length - 1];
// Since there could be a change in the line numbers, update the selection
const firstActive = selection.active.line === firstLine.line - 1;
selection = new Selection(
(firstActive ? lastLine : firstLine).originalLine - 1,
selection.anchor.character,
(firstActive ? firstLine : lastLine).originalLine - 1,
selection.active.character,
);
// Since there could be a change in the line numbers, update the selection
const firstActive = selection.active.line === firstLine.to.line - 1;
selection = new Selection(
(firstActive ? lastLine : firstLine).from.line - 1,
selection.anchor.character,
(firstActive ? firstLine : lastLine).from.line - 1,
selection.active.character,
);
const status = await this.view.container.git.getStatusForFile(this.uri.repoPath!, this.uri.fsPath);
const status = await this.view.container.git.getStatusForFile(this.uri.repoPath!, this.uri.fsPath);
if (status != null) {
const file: GitFile = {
conflictStatus: status?.conflictStatus,
fileName: commit.file?.path ?? '',
path: commit.file?.path ?? '',
indexStatus: status?.indexStatus,
originalFileName: commit.file?.originalPath,
originalPath: commit.file?.originalPath,
repoPath: this.uri.repoPath!,
status: status?.status ?? GitFileIndexStatus.Modified,
workingTreeStatus: status?.workingTreeStatus,
};
if (status?.workingTreeStatus != null && status?.indexStatus != null) {
let uncommitted = new GitLogCommit(
GitCommitType.LogFile,
this.uri.repoPath!,
GitRevision.uncommittedStaged,
'You',
commit.author.email,
commit.author.date,
commit.committer.date,
commit.message ?? commit.summary,
file.fileName,
[file],
GitFileIndexStatus.Modified,
file.originalFileName,
commit.previousSha,
file.originalFileName ?? file.fileName,
);
children.splice(
0,
0,
new FileRevisionAsCommitNode(this.view, this, file, uncommitted, {
selection: selection,
}),
);
uncommitted = new GitLogCommit(
GitCommitType.LogFile,
this.uri.repoPath!,
GitRevision.uncommitted,
'You',
commit.author.email,
commit.author.date,
commit.committer.date,
commit.message ?? commit.summary,
file.fileName,
[file],
GitFileIndexStatus.Modified,
file.originalFileName,
GitRevision.uncommittedStaged,
file.originalFileName ?? file.fileName,
);
children.splice(
0,
0,
new FileRevisionAsCommitNode(this.view, this, file, uncommitted, {
selection: selection,
}),
);
} else {
const uncommitted = new GitLogCommit(
GitCommitType.LogFile,
this.uri.repoPath!,
status?.workingTreeStatus != null
? GitRevision.uncommitted
: status?.indexStatus != null
? GitRevision.uncommittedStaged
: commit.sha,
'You',
commit.author.email,
commit.author.date,
commit.committer.date,
commit.message ?? commit.summary,
file.fileName,
[file],
GitFileIndexStatus.Modified,
file.originalFileName,
commit.previousSha,
file.originalFileName ?? file.fileName,
);
children.splice(
0,
0,
new FileRevisionAsCommitNode(this.view, this, file, uncommitted, {
selection: selection,
}),
);
const currentUser = await this.view.container.git.getCurrentUser(this.uri.repoPath!);
const pseudoCommits = status?.getPseudoCommits(currentUser);
if (pseudoCommits != null) {
for (const commit of pseudoCommits.reverse()) {
children.splice(
0,
0,
new FileRevisionAsCommitNode(this.view, this, file, commit, {
selection: selection,
}),
);
}
}
break;
}
break;
}
}
@ -207,8 +138,8 @@ export class LineHistoryNode
children.push(
...insertDateMarkers(
filterMap(log.commits.values(), c =>
c.files.length
? new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
c.file != null
? new FileRevisionAsCommitNode(this.view, this, c.file, c, {
branch: this.branch,
getBranchAndTagTips: getBranchAndTagTips,
selection: selection,

+ 4
- 4
src/views/nodes/mergeConflictCurrentChangesNode.ts View File

@ -37,7 +37,7 @@ export class MergeConflictCurrentChangesNode extends ViewNode
: new ThemeIcon('diff');
const markdown = new MarkdownString(
`Current changes to $(file)${GlyphChars.Space}${this.file.fileName} on ${GitReference.toString(
`Current changes to $(file)${GlyphChars.Space}${this.file.path} on ${GitReference.toString(
this.status.current,
)}${
commit != null
@ -70,7 +70,7 @@ export class MergeConflictCurrentChangesNode extends ViewNode
return {
title: 'Open Revision',
command: BuiltInCommands.Open,
arguments: [this.view.container.git.getRevisionUri('HEAD', this.file.fileName, this.status.repoPath)],
arguments: [this.view.container.git.getRevisionUri('HEAD', this.file.path, this.status.repoPath)],
};
}
@ -78,12 +78,12 @@ export class MergeConflictCurrentChangesNode extends ViewNode
lhs: {
sha: this.status.mergeBase,
uri: GitUri.fromFile(this.file, this.status.repoPath, undefined, true),
title: `${this.file.fileName} (merge-base)`,
title: `${this.file.path} (merge-base)`,
},
rhs: {
sha: 'HEAD',
uri: GitUri.fromFile(this.file, this.status.repoPath),
title: `${this.file.fileName} (${GitReference.toString(this.status.current, {
title: `${this.file.path} (${GitReference.toString(this.status.current, {
expand: false,
icon: false,
})})`,

+ 3
- 3
src/views/nodes/mergeConflictFileNode.ts View File

@ -29,7 +29,7 @@ export class MergeConflictFileNode extends ViewNode implements
}
get fileName(): string {
return this.file.fileName;
return this.file.path;
}
get repoPath(): string {
@ -52,7 +52,7 @@ export class MergeConflictFileNode extends ViewNode implements
this.file,
);
// Use the file icon and decorations
item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath);
item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.path, this.repoPath);
item.iconPath = ThemeIcon.File;
item.command = this.getCommand();
@ -114,7 +114,7 @@ export class MergeConflictFileNode extends ViewNode implements
title: 'Open File',
command: BuiltInCommands.Open,
arguments: [
this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath),
this.view.container.git.getAbsoluteUri(this.file.path, this.repoPath),
{
preserveFocus: true,
preview: true,

+ 4
- 8
src/views/nodes/mergeConflictIncomingChangesNode.ts View File

@ -42,7 +42,7 @@ export class MergeConflictIncomingChangesNode extends ViewNode
: new ThemeIcon('diff');
const markdown = new MarkdownString(
`Incoming changes to $(file)${GlyphChars.Space}${this.file.fileName}${
`Incoming changes to $(file)${GlyphChars.Space}${this.file.path}${
this.status.incoming != null
? ` from ${GitReference.toString(this.status.incoming)}${
commit != null
@ -83,11 +83,7 @@ export class MergeConflictIncomingChangesNode extends ViewNode
title: 'Open Revision',
command: BuiltInCommands.Open,
arguments: [
this.view.container.git.getRevisionUri(
this.status.HEAD.ref,
this.file.fileName,
this.status.repoPath,
),
this.view.container.git.getRevisionUri(this.status.HEAD.ref, this.file.path, this.status.repoPath),
],
};
}
@ -96,12 +92,12 @@ export class MergeConflictIncomingChangesNode extends ViewNode
lhs: {
sha: this.status.mergeBase,
uri: GitUri.fromFile(this.file, this.status.repoPath, undefined, true),
title: `${this.file.fileName} (merge-base)`,
title: `${this.file.path} (merge-base)`,
},
rhs: {
sha: this.status.HEAD.ref,
uri: GitUri.fromFile(this.file, this.status.repoPath),
title: `${this.file.fileName} (${
title: `${this.file.path} (${
this.status.incoming != null
? GitReference.toString(this.status.incoming, { expand: false, icon: false })
: 'incoming'

+ 1
- 1
src/views/nodes/pullRequestNode.ts View File

@ -60,7 +60,7 @@ export class PullRequestNode extends ViewNode {
tooltip.supportHtml = true;
tooltip.isTrusted = true;
if (this.branchOrCommit instanceof GitCommit) {
if (GitCommit.is(this.branchOrCommit)) {
tooltip.appendMarkdown(
`Commit \`$(git-commit) ${this.branchOrCommit.shortSha}\` was introduced by $(git-pull-request) PR #${this.pullRequest.id}\n\n`,
);

+ 8
- 22
src/views/nodes/rebaseStatusNode.ts View File

@ -13,14 +13,7 @@ import { ViewFilesLayout } from '../../configuration';
import { BuiltInCommands, GlyphChars } from '../../constants';
import { CommitFormatter } from '../../git/formatters';
import { GitUri } from '../../git/gitUri';
import {
GitBranch,
GitLogCommit,
GitRebaseStatus,
GitReference,
GitRevisionReference,
GitStatus,
} from '../../git/models';
import { GitBranch, GitCommit, GitRebaseStatus, GitReference, GitRevisionReference, GitStatus } from '../../git/models';
import { Arrays, Strings } from '../../system';
import { joinPaths, normalizePath } from '../../system/path';
import { ViewsWithCommits } from '../viewBase';
@ -135,18 +128,12 @@ export class RebaseStatusNode extends ViewNode {
}
export class RebaseCommitNode extends ViewRefNode<ViewsWithCommits, GitRevisionReference> {
constructor(view: ViewsWithCommits, parent: ViewNode, public readonly commit: GitLogCommit) {
super(commit.toGitUri(), view, parent);
constructor(view: ViewsWithCommits, parent: ViewNode, public readonly commit: GitCommit) {
super(commit.getGitUri(), view, parent);
}
override toClipboard(): string {
let message = this.commit.message;
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
return `${this.commit.shortSha}: ${message}`;
return `${this.commit.shortSha}: ${this.commit.summary}`;
}
get ref(): GitRevisionReference {
@ -157,7 +144,7 @@ export class RebaseCommitNode extends ViewRefNode
return CommitFormatter.fromTemplate(
`\${author}\${ (email)} ${
GlyphChars.Dash
} \${id}\${ (tips)}\n\${ago} (\${date})\${\n\nmessage}${this.commit.getFormattedDiffStatus({
} \${id}\${ (tips)}\n\${ago} (\${date})\${\n\nmessage}${this.commit.formatStats({
expand: true,
prefix: '\n\n',
separator: '\n',
@ -170,12 +157,11 @@ export class RebaseCommitNode extends ViewRefNode
);
}
getChildren(): ViewNode[] {
async getChildren(): Promise<ViewNode[]> {
const commit = this.commit;
let children: FileNode[] = commit.files.map(
s => new CommitFileNode(this.view, this, s, commit.toFileCommit(s)!),
);
const commits = await commit.getCommitsForFiles();
let children: FileNode[] = commits.map(c => new CommitFileNode(this.view, this, c.file!, c));
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(

+ 1
- 1
src/views/nodes/resultsFileNode.ts View File

@ -26,7 +26,7 @@ export class ResultsFileNode extends ViewRefFileNode implements FileNode {
}
get fileName(): string {
return this.file.fileName;
return this.file.path;
}
get ref(): GitRevisionReference {

+ 3
- 3
src/views/nodes/resultsFilesNode.ts View File

@ -187,12 +187,12 @@ export class ResultsFilesNode extends ViewNode {
if (mergeBase != null) {
const files = await this.view.container.git.getDiffStatus(this.uri.repoPath!, `${mergeBase}..${ref}`);
if (files != null) {
filterTo = new Set<string>(files.map(f => f.fileName));
filterTo = new Set<string>(files.map(f => f.path));
}
} else {
const commit = await this.view.container.git.getCommit(this.uri.repoPath!, ref || 'HEAD');
if (commit?.files != null) {
filterTo = new Set<string>(commit.files.map(f => f.fileName));
filterTo = new Set<string>(commit.files.map(f => f.path));
}
}
@ -200,7 +200,7 @@ export class ResultsFilesNode extends ViewNode {
results.filtered = {
filter: filter,
files: results.files!.filter(f => filterTo!.has(f.fileName)),
files: results.files!.filter(f => filterTo!.has(f.path)),
};
}
}

+ 2
- 2
src/views/nodes/stashFileNode.ts View File

@ -1,11 +1,11 @@
import { GitFile, GitLogCommit } from '../../git/models';
import { GitFile, GitStashCommit } from '../../git/models';
import { RepositoriesView } from '../repositoriesView';
import { StashesView } from '../stashesView';
import { CommitFileNode } from './commitFileNode';
import { ContextValues, ViewNode } from './viewNode';
export class StashFileNode extends CommitFileNode<StashesView | RepositoriesView> {
constructor(view: StashesView | RepositoriesView, parent: ViewNode, file: GitFile, commit: GitLogCommit) {
constructor(view: StashesView | RepositoriesView, parent: ViewNode, file: GitFile, commit: GitStashCommit) {
super(view, parent, file, commit);
}

+ 4
- 7
src/views/nodes/stashNode.ts View File

@ -15,7 +15,7 @@ export class StashNode extends ViewRefNode
}
constructor(view: StashesView | RepositoriesView, parent: ViewNode, public readonly commit: GitStashCommit) {
super(commit.toGitUri(), view, parent);
super(commit.getGitUri(), view, parent);
}
override toClipboard(): string {
@ -31,12 +31,9 @@ export class StashNode extends ViewRefNode
}
async getChildren(): Promise<ViewNode[]> {
// Ensure we have checked for untracked files
await this.commit.checkForUntrackedFiles();
let children: FileNode[] = this.commit.files.map(
s => new StashFileNode(this.view, this, s, this.commit.toFileCommit(s)!),
);
// Ensure we have checked for untracked files (inside the getCommitsForFiles call)
const commits = await this.commit.getCommitsForFiles();
let children: FileNode[] = commits.map(c => new StashFileNode(this.view, this, c.file!, c as GitStashCommit));
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(

+ 7
- 7
src/views/nodes/statusFileNode.ts View File

@ -2,7 +2,7 @@ import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithCommandArgs, DiffWithPreviousCommandArgs } from '../../commands';
import { StatusFileFormatter } from '../../git/formatters/statusFormatter';
import { GitUri } from '../../git/gitUri';
import { GitFile, GitLogCommit } from '../../git/models';
import { GitCommit, GitFile } from '../../git/models';
import { Strings } from '../../system';
import { dirname, joinPaths } from '../../system/path';
import { ViewsWithCommits } from '../viewBase';
@ -11,14 +11,14 @@ import { FileNode } from './folderNode';
import { ContextValues, ViewNode } from './viewNode';
export class StatusFileNode extends ViewNode<ViewsWithCommits> implements FileNode {
public readonly commits: GitLogCommit[];
public readonly commits: GitCommit[];
public readonly file: GitFile;
public readonly repoPath: string;
private readonly _hasStagedChanges: boolean;
private readonly _hasUnstagedChanges: boolean;
constructor(view: ViewsWithCommits, parent: ViewNode, repoPath: string, file: GitFile, commits: GitLogCommit[]) {
constructor(view: ViewsWithCommits, parent: ViewNode, repoPath: string, file: GitFile, commits: GitCommit[]) {
let hasStagedChanges = false;
let hasUnstagedChanges = false;
let ref = undefined;
@ -58,7 +58,7 @@ export class StatusFileNode extends ViewNode implements FileNo
}
get fileName(): string {
return this.file.fileName;
return this.file.path;
}
getChildren(): ViewNode[] {
@ -86,7 +86,7 @@ export class StatusFileNode extends ViewNode implements FileNo
}
// Use the file icon and decorations
item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath);
item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.path, this.repoPath);
item.iconPath = ThemeIcon.File;
item.command = this.getCommand();
@ -103,7 +103,7 @@ export class StatusFileNode extends ViewNode implements FileNo
}
// Use the file icon and decorations
item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath);
item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.path, this.repoPath);
item.iconPath = ThemeIcon.File;
} else {
item.contextValue = ContextValues.StatusFileCommits;
@ -241,7 +241,7 @@ export class StatusFileNode extends ViewNode implements FileNo
}
const commit = this.commits[this.commits.length - 1];
const file = commit.findFile(this.file.fileName)!;
const file = commit.files?.find(f => f.path === this.file.path) ?? this.file;
const commandArgs: DiffWithCommandArgs = {
lhs: {
sha: `${commit.sha}^`,

+ 27
- 57
src/views/nodes/statusFilesNode.ts View File

@ -1,18 +1,11 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ViewFilesLayout } from '../../configuration';
import { GitUri } from '../../git/gitUri';
import {
GitCommitType,
GitFileWithCommit,
GitLog,
GitLogCommit,
GitRevision,
GitStatus,
GitStatusFile,
GitTrackingState,
} from '../../git/models';
import { Arrays, Iterables, Strings } from '../../system';
import { GitCommit, GitFileWithCommit, GitLog, GitStatus, GitStatusFile, GitTrackingState } from '../../git/models';
import { groupBy, makeHierarchical } from '../../system/array';
import { filter, flatMap, map } from '../../system/iterable';
import { joinPaths, normalizePath } from '../../system/path';
import { pluralize, sortCompare } from '../../system/string';
import { RepositoriesView } from '../repositoriesView';
import { FileNode, FolderNode } from './folderNode';
import { RepositoryNode } from './repositoryNode';
@ -57,12 +50,17 @@ export class StatusFilesNode extends ViewNode {
if (this.range != null) {
log = await this.view.container.git.getLog(repoPath, { limit: 0, ref: this.range });
if (log != null) {
await Promise.allSettled(
map(
filter(log.commits.values(), c => c.files == null),
c => c.ensureFullDetails(),
),
);
files = [
...Iterables.flatMap(log.commits.values(), c =>
c.files.map(s => {
const file: GitFileWithCommit = { ...s, commit: c };
return file;
}),
...flatMap(
log.commits.values(),
c => c.files?.map<GitFileWithCommit>(f => ({ ...f, commit: c })) ?? [],
),
];
}
@ -72,28 +70,15 @@ export class StatusFilesNode extends ViewNode {
files.splice(
0,
0,
...Iterables.flatMap(this.status.files, s => {
if (s.workingTreeStatus != null && s.indexStatus != null) {
// Decrements the date to guarantee this entry will be sorted after the previous entry (most recent first)
const older = new Date();
older.setMilliseconds(older.getMilliseconds() - 1);
return [
this.toStatusFile(s, GitRevision.uncommitted, GitRevision.uncommittedStaged),
this.toStatusFile(s, GitRevision.uncommittedStaged, 'HEAD', older),
];
} else if (s.indexStatus != null) {
return [this.toStatusFile(s, GitRevision.uncommittedStaged, 'HEAD')];
}
return [this.toStatusFile(s, GitRevision.uncommitted, 'HEAD')];
}),
...flatMap(this.status.files, f =>
map(f.getPseudoCommits(undefined), c => this.getFileWithPseudoCommit(f, c)),
),
);
}
files.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
const groups = Arrays.groupBy(files, s => s.fileName);
const groups = groupBy(files, s => s.path);
let children: FileNode[] = Object.values(groups).map(
files =>
@ -107,7 +92,7 @@ export class StatusFilesNode extends ViewNode {
);
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(
const hierarchy = makeHierarchical(
children,
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
@ -117,7 +102,7 @@ export class StatusFilesNode extends ViewNode {
const root = new FolderNode(this.view, this, repoPath, '', hierarchy, true);
children = root.getChildren() as FileNode[];
} else {
children.sort((a, b) => a.priority - b.priority || Strings.sortCompare(a.label!, b.label!));
children.sort((a, b) => a.priority - b.priority || sortCompare(a.label!, b.label!));
}
return children;
@ -137,10 +122,10 @@ export class StatusFilesNode extends ViewNode {
if (aheadFiles != null) {
const uniques = new Set();
for (const f of this.status.files) {
uniques.add(f.fileName);
uniques.add(f.path);
}
for (const f of aheadFiles) {
uniques.add(f.fileName);
uniques.add(f.path);
}
files = uniques.size;
@ -159,7 +144,7 @@ export class StatusFilesNode extends ViewNode {
}
}
const label = files === -1 ? '?? files changed' : `${Strings.pluralize('file', files)} changed`;
const label = files === -1 ? '?? files changed' : `${pluralize('file', files)} changed`;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ContextValues.StatusFiles;
@ -171,30 +156,15 @@ export class StatusFilesNode extends ViewNode {
return item;
}
private toStatusFile(file: GitStatusFile, ref: string, previousRef: string, date?: Date): GitFileWithCommit {
private getFileWithPseudoCommit(file: GitStatusFile, commit: GitCommit): GitFileWithCommit {
return {
status: file.status,
repoPath: file.repoPath,
indexStatus: file.indexStatus,
workingTreeStatus: file.workingTreeStatus,
fileName: file.fileName,
originalFileName: file.originalFileName,
commit: new GitLogCommit(
GitCommitType.LogFile,
file.repoPath,
ref,
'You',
undefined,
date ?? new Date(),
date ?? new Date(),
'',
file.fileName,
[file],
file.status,
file.originalFileName,
previousRef,
file.fileName,
),
path: file.path,
originalPath: file.originalPath,
commit: commit,
};
}
}

+ 2
- 2
src/views/remotesView.ts View File

@ -14,7 +14,7 @@ import { GitUri } from '../git/gitUri';
import {
GitBranch,
GitBranchReference,
GitLogCommit,
GitCommit,
GitReference,
GitRemote,
GitRevisionReference,
@ -227,7 +227,7 @@ export class RemotesView extends ViewBase {
});
}
async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
async findCommit(commit: GitCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
// Get all the remote branches the commit is on

+ 2
- 2
src/views/repositoriesView.ts View File

@ -20,8 +20,8 @@ import { Container } from '../container';
import {
GitBranch,
GitBranchReference,
GitCommit,
GitContributor,
GitLogCommit,
GitReference,
GitRemote,
GitRevisionReference,
@ -326,7 +326,7 @@ export class RepositoriesView extends ViewBase
});
}
async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
async findCommit(commit: GitCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
// Get all the branches the commit is on

+ 6
- 6
src/views/viewCommands.ts View File

@ -627,7 +627,7 @@ export class ViewCommands {
return;
}
void (await this.container.git.stageFile(node.repoPath, node.file.fileName));
void (await this.container.git.stageFile(node.repoPath, node.file.path));
void node.triggerChange();
}
@ -704,7 +704,7 @@ export class ViewCommands {
return;
}
void (await this.container.git.unStageFile(node.repoPath, node.file.fileName));
void (await this.container.git.unStageFile(node.repoPath, node.file.path));
void node.triggerChange();
}
@ -998,7 +998,7 @@ export class ViewCommands {
preview: true,
},
});
} else if (node instanceof FileRevisionAsCommitNode && node.commit.hasConflicts) {
} else if (node instanceof FileRevisionAsCommitNode && node.commit.file?.hasConflicts) {
const baseUri = await node.getConflictBaseUri();
if (baseUri != null) {
return executeEditorCommand<DiffWithWorkingCommandArgs>(Commands.DiffWithWorking, undefined, {
@ -1092,10 +1092,10 @@ export class ViewCommands {
uri = Container.instance.git.getRevisionUri(node.uri);
} else {
uri =
node.commit.status === 'D'
node.commit.file?.status === 'D'
? Container.instance.git.getRevisionUri(
node.commit.previousSha!,
node.commit.previousUri.fsPath,
node.commit.previousSha,
node.commit.file.path,
node.commit.repoPath,
)
: Container.instance.git.getRevisionUri(node.uri);

+ 1
- 1
src/webviews/rebaseEditor.ts View File

@ -583,7 +583,7 @@ async function parseRebaseTodo(
author: name,
date: commit.formatDate(container.config.defaultDateFormat),
dateFromNow: commit.formatDateFromNow(),
message: commit.message,
message: commit.message ?? commit.summary,
});
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save