diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index deccd3d..63b46a3 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -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, diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 8159a12..025c32b 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -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, diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index 305ecee..23e8be5 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -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(); const avatarDecorationsMap = avatars ? new Map() : 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, diff --git a/src/annotations/gutterChangesAnnotationProvider.ts b/src/annotations/gutterChangesAnnotationProvider.ts index 220d568..6059398 100644 --- a/src/annotations/gutterChangesAnnotationProvider.ts +++ b/src/annotations/gutterChangesAnnotationProvider.ts @@ -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 { - 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 { 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= 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, diff --git a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts index d479dc3..925a5d9 100644 --- a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts +++ b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts @@ -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; diff --git a/src/annotations/lineAnnotationController.ts b/src/annotations/lineAnnotationController.ts index b4911b4..dd24b1f 100644 --- a/src/annotations/lineAnnotationController.ts +++ b/src/annotations/lineAnnotationController.ts @@ -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(selections, selection => { + ...filterMap(selections, selection => { const state = this.container.lineTracker.getState(selection.active); if (state?.commit == null) { Logger.debug(cc, `Line ${selection.active} returned no commit`); diff --git a/src/codelens/codeLensProvider.ts b/src/codelens/codeLensProvider.ts index 8da4fca..4b45b6b 100644 --- a/src/codelens/codeLensProvider.ts +++ b/src/codelens/codeLensProvider.ts @@ -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( 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( 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( 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( 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( 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( 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( title: string, lens: T, - commit: GitCommit2, + commit: GitCommit, only?: boolean, ): T { lens.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({ diff --git a/src/commands/common.ts b/src/commands/common.ts index f19fc45..a76d149 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -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( +export function isCommandContextViewNodeHasCommit( 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( diff --git a/src/commands/copyMessageToClipboard.ts b/src/commands/copyMessageToClipboard.ts index 9ac305e..5901b4a 100644 --- a/src/commands/copyMessageToClipboard.ts +++ b/src/commands/copyMessageToClipboard.ts @@ -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)); diff --git a/src/commands/copyShaToClipboard.ts b/src/commands/copyShaToClipboard.ts index 946d2de..5f24ca2 100644 --- a/src/commands/copyShaToClipboard.ts +++ b/src/commands/copyShaToClipboard.ts @@ -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; diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index df5343a..0dcc5e8 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -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( diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index e894022..3b0c719 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -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'); diff --git a/src/commands/diffWith.ts b/src/commands/diffWith.ts index ae46d07..d2e3a8f 100644 --- a/src/commands/diffWith.ts +++ b/src/commands/diffWith.ts @@ -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, }; diff --git a/src/commands/diffWithNext.ts b/src/commands/diffWithNext.ts index 4a736d1..273adcb 100644 --- a/src/commands/diffWithNext.ts +++ b/src/commands/diffWithNext.ts @@ -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!, diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index 6b861ad..8bab0e1 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -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(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); } diff --git a/src/commands/diffWithRevisionFrom.ts b/src/commands/diffWithRevisionFrom.ts index 00c9350..09dcd13 100644 --- a/src/commands/diffWithRevisionFrom.ts +++ b/src/commands/diffWithRevisionFrom.ts @@ -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)})`; } } diff --git a/src/commands/externalDiff.ts b/src/commands/externalDiff.ts index 66eaef5..37e44cd 100644 --- a/src/commands/externalDiff.ts +++ b/src/commands/externalDiff.ts @@ -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 = [ diff --git a/src/commands/git/branch.ts b/src/commands/git/branch.ts index 6e364dc..4a4243a 100644 --- a/src/commands/git/branch.ts +++ b/src/commands/git/branch.ts @@ -60,9 +60,39 @@ interface RenameState { type State = CreateState | DeleteState | RenameState; type BranchStepState = SomeNonNullable, 'subcommand'>; + type CreateStepState = BranchStepState>; +function assertStateStepCreate(state: PartialStepState): asserts state is CreateStepState { + if (state.repo instanceof Repository && state.subcommand === 'create') return; + + debugger; + throw new Error('Missing repository'); +} + type DeleteStepState = BranchStepState>; +function assertStateStepDelete(state: PartialStepState): asserts state is DeleteStepState { + if (state.repo instanceof Repository && state.subcommand === 'delete') return; + + debugger; + throw new Error('Missing repository'); +} + type RenameStepState = BranchStepState>; +function assertStateStepRename(state: PartialStepState): 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 { + if (Array.isArray(state.references)) return; + + debugger; + throw new Error('Missing branches'); +} const subcommandToTitleMap = new Map([ ['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, - context, - ); + assertStateStepDeleteBranches(state); + const result = yield* this.deleteCommandConfirmStep(state, context); if (result === StepResult.Break) continue; state.flags = result; diff --git a/src/commands/git/log.ts b/src/commands/git/log.ts index 865363b..24e1f18 100644 --- a/src/commands/git/log.ts +++ b/src/commands/git/log.ts @@ -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 = ExcludeSome, 'repo', string>; +type RepositoryStepState = SomeNonNullable< + ExcludeSome, 'repo', string>, + 'repo' +>; +function assertStateStepRepository(state: PartialStepState): 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, }, }, diff --git a/src/commands/git/search.ts b/src/commands/git/search.ts index 8c7c686..f93ddc1 100644 --- a/src/commands/git/search.ts +++ b/src/commands/git/search.ts @@ -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 | undefined; title: string; diff --git a/src/commands/git/show.ts b/src/commands/git/show.ts index 8a67c69..bcab9d9 100644 --- a/src/commands/git/show.ts +++ b/src/commands/git/show.ts @@ -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 { +interface State { repo: string | Repository; reference: Ref; fileName: string; @@ -32,7 +31,32 @@ export interface ShowGitCommandArgs { state?: Partial; } -type ShowStepState = ExcludeSome, 'repo', string>; +type RepositoryStepState = SomeNonNullable< + ExcludeSome, 'repo', string>, + 'repo' +>; +function assertStateStepRepository(state: PartialStepState): asserts state is RepositoryStepState { + if (state.repo instanceof Repository) return; + + debugger; + throw new Error('Missing repository'); +} + +type CommitStepState = SomeNonNullable>, 'reference'>; +function assertsStateStepCommit(state: RepositoryStepState): asserts state is CommitStepState { + if (GitCommit.is(state.reference)) return; + + debugger; + throw new Error('Missing reference'); +} + +type FileNameStepState = SomeNonNullable; +function assertsStateStepFileName(state: CommitStepState): asserts state is FileNameStepState { + if (state.fileName) return; + + debugger; + throw new Error('Missing filename'); +} export class ShowGitCommand extends QuickCommand { 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(), - commits: new Map(), + commits: new Map(), 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>, - 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>, - 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>, - context, - ); + assertsStateStepFileName(state); + + const result = yield* showCommitOrStashFileStep(state, context); if (result === StepResult.Break) continue; if (result instanceof CommitFilesQuickPickItem) { diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index 16d9d38..b6c7674 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -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, { diff --git a/src/commands/gitCommands.actions.ts b/src/commands/gitCommands.actions.ts index 83cb110..0ea6b04 100644 --- a/src/commands/gitCommands.actions.ts +++ b/src/commands/gitCommands.actions.ts @@ -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 { + 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; + export async function openAllChanges(commit: GitCommit, options?: TextDocumentShowOptions): Promise; export async function openAllChanges( files: GitFile[], refs: { repoPath: string; ref1: string; ref2: string }, options?: TextDocumentShowOptions, ): Promise; 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; + export async function openAllChangesWithDiffTool(commit: GitCommit): Promise; export async function openAllChangesWithDiffTool( files: GitFile[], ref: { repoPath: string; ref: string }, ): Promise; 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; export async function openAllChangesWithWorking( @@ -281,14 +293,18 @@ export namespace GitActions { options?: TextDocumentShowOptions, ): Promise; 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; export async function openChanges( @@ -328,13 +344,13 @@ export namespace GitActions { ): Promise; 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; export function openChangesWithDiffTool( @@ -379,13 +395,13 @@ export namespace GitActions { ): Promise; 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; export async function openChangesWithWorking( @@ -415,13 +431,13 @@ export namespace GitActions { ): Promise; 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 { 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 { return openDirectoryCompare(ref.repoPath, ref.ref, undefined); } @@ -502,28 +518,28 @@ export namespace GitActions { ): Promise; export async function openFileAtRevision( file: string | GitFile, - commit: GitLogCommit, + commit: GitCommit, options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number }, ): Promise; export async function openFileAtRevision( fileOrRevisionUri: string | GitFile | Uri, - commitOrOptions?: GitLogCommit | TextDocumentShowOptions, + commitOrOptions?: GitCommit | TextDocumentShowOptions, options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number }, ): Promise { 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; + export async function openFiles(commit: GitCommit): Promise; export async function openFiles(files: GitFile[], repoPath: string, ref: string): Promise; export async function openFiles( - commitOrFiles: GitLogCommit | GitFile[], + commitOrFiles: GitCommit | GitFile[], repoPath?: string, ref?: string, ): Promise { 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; + export async function openFilesAtRevision(commit: GitCommit): Promise; export async function openFilesAtRevision( files: GitFile[], repoPath: string, @@ -599,17 +619,21 @@ export namespace GitActions { ref2: string, ): Promise; export async function openFilesAtRevision( - commitOrFiles: GitLogCommit | GitFile[], + commitOrFiles: GitCommit | GitFile[], repoPath?: string, ref1?: string, ref2?: string, ): Promise { 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, })); } diff --git a/src/commands/openFileAtRevision.ts b/src/commands/openFileAtRevision.ts index 28133eb..5a3fef5 100644 --- a/src/commands/openFileAtRevision.ts +++ b/src/commands/openFileAtRevision.ts @@ -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, diff --git a/src/commands/openFileAtRevisionFrom.ts b/src/commands/openFileAtRevisionFrom.ts index a80177e..65209c8 100644 --- a/src/commands/openFileAtRevisionFrom.ts +++ b/src/commands/openFileAtRevisionFrom.ts @@ -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; diff --git a/src/commands/openOnRemote.ts b/src/commands/openOnRemote.ts index aa57888..e3c96f7 100644 --- a/src/commands/openOnRemote.ts +++ b/src/commands/openOnRemote.ts @@ -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( diff --git a/src/commands/openRevisionFile.ts b/src/commands/openRevisionFile.ts index b51f970..d971dea 100644 --- a/src/commands/openRevisionFile.ts +++ b/src/commands/openRevisionFile.ts @@ -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); diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index 0302ee1..47e58da 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -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[]>) => void; + onDidClick: (items: Readonly[]>) => void; }; titleContext?: string; }, -): AsyncStepResultGenerator { +): AsyncStepResultGenerator { 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>( - (i): i is CommitQuickPickItem => !CommandQuickPickItem.is(i), + const items = quickpick.activeItems.filter>( + (i): i is CommitQuickPickItem => !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>( - (i): i is CommitQuickPickItem => !CommandQuickPickItem.is(i), + const items = quickpick.activeItems.filter>( + (i): i is CommitQuickPickItem => !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[] | 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 { + if (state.reference.files == null) { + debugger; + } + const step: QuickPickStep = 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[] | undefined; let isStash = false; - if (GitStashCommit.is(state.reference)) { + if (GitCommit.is(state.reference)) { isStash = true; items.push(new RevealInSideBarQuickPickItem(state.reference)); diff --git a/src/commands/showQuickCommit.ts b/src/commands/showQuickCommit.ts index bc71c26..012b418 100644 --- a/src/commands/showQuickCommit.ts +++ b/src/commands/showQuickCommit.ts @@ -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) { diff --git a/src/commands/showQuickCommitFile.ts b/src/commands/showQuickCommitFile.ts index 97eb15f..cec78df 100644 --- a/src/commands/showQuickCommitFile.ts +++ b/src/commands/showQuickCommitFile.ts @@ -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, diff --git a/src/commands/stashApply.ts b/src/commands/stashApply.ts index f80125a..6daa7f8 100644 --- a/src/commands/stashApply.ts +++ b/src/commands/stashApply.ts @@ -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(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 }; diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index bb666ce..bc4e6e0 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -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( diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 30f93a8..e0b387e 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -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; } - 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; } - 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(); - const commits = new Map(); + const authors = new Map(); + const commits = new Map(); 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 { + async getCommit(repoPath: string, ref: string): Promise { 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 { + options?: { ref?: string; firstIfNotFound?: boolean; range?: Range }, + ): Promise { 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 { @@ -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 | 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 { 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(); 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 { - 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; } @@ -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); diff --git a/src/git/formatters/commitFormatter.ts b/src/git/formatters/commitFormatter.ts index 439701b..5d35ca7 100644 --- a/src/git/formatters/commitFormatter.ts +++ b/src/git/formatters/commitFormatter.ts @@ -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 { +export class CommitFormatter extends Formatter { private get _authorDate() { return this._item.author.formatDate(this._options.dateFormat); } @@ -249,21 +242,21 @@ export class CommitFormatter extends Formatter - 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('openPullRequest', { repoPath: this._item.repoPath, @@ -519,7 +513,7 @@ export class CommitFormatter extends Formatter; + static fromTemplateAsync(template: string, commit: GitCommit, options?: CommitFormatOptions): Promise; static fromTemplateAsync( template: string, - commit: GitCommit | GitCommit2, - dateFormat: string | null, - ): Promise; - static fromTemplateAsync( - template: string, - commit: GitCommit | GitCommit2, - options?: CommitFormatOptions, - ): Promise; - static fromTemplateAsync( - template: string, - commit: GitCommit | GitCommit2, + commit: GitCommit, dateFormatOrOptions?: string | null | CommitFormatOptions, ): Promise; static fromTemplateAsync( template: string, - commit: GitCommit | GitCommit2, + commit: GitCommit, dateFormatOrOptions?: string | null | CommitFormatOptions, ): Promise { if (CommitFormatter.has(template, 'footnotes')) { diff --git a/src/git/formatters/statusFormatter.ts b/src/git/formatters/statusFormatter.ts index aaea118..1f6f7f3 100644 --- a/src/git/formatters/statusFormatter.ts +++ b/src/git/formatters/statusFormatter.ts @@ -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( diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 8f21011..f57f9eb 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -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>; getChangedFilesCount(repoPath: string, ref?: string): Promise; - getCommit(repoPath: string, ref: string): Promise; + getCommit(repoPath: string, ref: string): Promise; 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; + ): Promise; getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise; 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; @@ -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 | undefined>; diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 1035d66..94202c8 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -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 { + getCommit(repoPath: string | Uri, ref: string): Promise { 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 { + options?: { ref?: string; firstIfNotFound?: boolean; range?: Range }, + ): Promise { 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 { @@ -1212,7 +1211,6 @@ export class GitProviderService implements Disposable { merges?: boolean; ordering?: string | null; ref?: string; - reverse?: boolean; since?: string; }, ): Promise | undefined> { diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 229c0a0..47ab665 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -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) { diff --git a/src/git/models.ts b/src/git/models.ts index 188fd1b..1f997d9 100644 --- a/src/git/models.ts +++ b/src/git/models.ts @@ -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'; diff --git a/src/git/models/blame.ts b/src/git/models/blame.ts index ccdfe17..05642eb 100644 --- a/src/git/models/blame.ts +++ b/src/git/models/blame.ts @@ -1,15 +1,20 @@ -import { GitAuthor, GitCommit2, GitCommitLine } from './commit'; +import { GitCommit, GitCommitLine } from './commit'; export interface GitBlame { readonly repoPath: string; - readonly authors: Map; - readonly commits: Map; + readonly authors: Map; + readonly commits: Map; 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[]; } diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 864ba91..55844d5 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -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'; diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index de2b1db..a095874 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -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(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 { - 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 { - 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 { - return commit.message != null && commit.files != null && commit.parents.length !== 0; + static hasFullDetails(commit: GitCommit): commit is GitCommit & SomeNonNullable { + 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 { - 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 { - 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 { + 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 | undefined; async getAssociatedPullRequest(options?: { timeout?: number }): Promise { - 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 { + 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 { return this.author.getAvatarUri(this, options); } + async getCommitForFile(file: string | GitFile): Promise { + 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 { + 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((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 { - 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( - (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 { - if (!this.isFile) return Promise.resolve(undefined); - - return Container.instance.git.getWorkingUri(this.repoPath, this.uri); - } - - @memoize(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(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(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 { - return getAvatarUri(this.authorEmail, this, options); - } + getAvatarUri( + commit: GitCommit, + options?: { defaultStyle?: GravatarDefaultStyle; size?: number }, + ): Uri | Promise { + 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(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; } diff --git a/src/git/models/file.ts b/src/git/models/file.ts index 3852d88..7d2c71c 100644 --- a/src/git/models/file.ts +++ b/src/git/models/file.ts @@ -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 { + 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; + } +} diff --git a/src/git/models/log.ts b/src/git/models/log.ts index 5993ea5..e0496a6 100644 --- a/src/git/models/log.ts +++ b/src/git/models/log.ts @@ -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; - readonly commits: Map; + readonly commits: Map; readonly sha: string | undefined; readonly range: Range | undefined; diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts deleted file mode 100644 index 1eaa1a5..0000000 --- a/src/git/models/logCommit.ts +++ /dev/null @@ -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, - ); - } -} diff --git a/src/git/models/merge.ts b/src/git/models/merge.ts index 663778f..3756c94 100644 --- a/src/git/models/merge.ts +++ b/src/git/models/merge.ts @@ -1,4 +1,4 @@ -import { GitBranchReference, GitRevisionReference } from '../models'; +import { GitBranchReference, GitRevisionReference } from './reference'; export interface GitMergeStatus { type: 'merge'; diff --git a/src/git/models/rebase.ts b/src/git/models/rebase.ts index ff7e914..714ddb6 100644 --- a/src/git/models/rebase.ts +++ b/src/git/models/rebase.ts @@ -1,4 +1,4 @@ -import { GitBranchReference, GitRevisionReference } from '../models'; +import { GitBranchReference, GitRevisionReference } from './reference'; export interface GitRebaseStatus { type: 'rebase'; diff --git a/src/git/models/reference.ts b/src/git/models/reference.ts index e377523..64a4fa5 100644 --- a/src/git/models/reference.ts +++ b/src/git/models/reference.ts @@ -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 { diff --git a/src/git/models/reflog.ts b/src/git/models/reflog.ts index 607ceba..8dad1f5 100644 --- a/src/git/models/reflog.ts +++ b/src/git/models/reflog.ts @@ -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; diff --git a/src/git/models/remote.ts b/src/git/models/remote.ts index 42cf2bf..ec5f653 100644 --- a/src/git/models/remote.ts +++ b/src/git/models/remote.ts @@ -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.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), ); } diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index fc1092b..4197d99 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -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 { + getCommit(ref: string): Promise { return this.container.git.getCommit(this.path, ref); } diff --git a/src/git/models/stash.ts b/src/git/models/stash.ts index b194d84..4e65f56 100644 --- a/src/git/models/stash.ts +++ b/src/git/models/stash.ts @@ -1,4 +1,4 @@ -import { GitStashCommit } from './stashCommit'; +import { GitStashCommit } from './commit'; export interface GitStash { readonly repoPath: string; diff --git a/src/git/models/stashCommit.ts b/src/git/models/stashCommit.ts deleted file mode 100644 index 4262e54..0000000 --- a/src/git/models/stashCommit.ts +++ /dev/null @@ -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) ?? [], - ); - } -} diff --git a/src/git/models/status.ts b/src/git/models/status.ts index d432855..ee9622c 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -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(change: T | null | undefined, original: T | undefined): T | undefined { - if (change === undefined) return original; - return change !== null ? change : undefined; - } } diff --git a/src/git/models/tag.ts b/src/git/models/tag.ts index 2a18668..54e33be 100644 --- a/src/git/models/tag.ts +++ b/src/git/models/tag.ts @@ -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, diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index 1457552..0d2b1a5 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -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(); - const commits = new Map(); + const authors = new Map(); + const commits = new Map(); 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, - authors: Map, + repoPath: string, + commits: Map, + authors: Map, 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; } } } diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index 52dd11d..ce36bff 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -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), diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index a62af01..7467d4b 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -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 = { @@ -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(); - const commits = new Map(); + const commits = new Map(); 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, - authors: Map, - recentCommit: GitLogCommit | undefined, + commits: Map, 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, diff --git a/src/git/parsers/stashParser.ts b/src/git/parsers/stashParser.ts index 138e029..9725e2c 100644 --- a/src/git/parsers/stashParser.ts +++ b/src/git/parsers/stashParser.ts @@ -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}`); + const lines = getLines(`${data}`); // Skip the first line since it will always be let next = lines.next(); if (next.done) return undefined; @@ -51,7 +58,7 @@ export class GitStashParser { const commits = new Map(); - 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) { 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); diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index ca3eb2d..906867b 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -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; diff --git a/src/hovers/hovers.ts b/src/hovers/hovers.ts index 824d0c5..8661eb2 100644 --- a/src/hovers/hovers.ts +++ b/src/hovers/hovers.ts @@ -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 { 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, diff --git a/src/hovers/lineHoverController.ts b/src/hovers/lineHoverController.ts index 808d2b9..702a235 100644 --- a/src/hovers/lineHoverController.ts +++ b/src/hovers/lineHoverController.ts @@ -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; diff --git a/src/messages.ts b/src/messages.ts index e5c2f5b..bf0220b 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -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 { - if (commit === undefined) { + static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise { + if (commit == null) { return Messages.showMessage( 'info', 'There is no previous commit.', diff --git a/src/premium/github/github.ts b/src/premium/github/github.ts index fd56247..c4e9527 100644 --- a/src/premium/github/github.ts +++ b/src/premium/github/github.ts @@ -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 { diff --git a/src/premium/github/githubGitProvider.ts b/src/premium/github/githubGitProvider.ts index f25f3ae..2deb865 100644 --- a/src/premium/github/githubGitProvider.ts +++ b/src/premium/github/githubGitProvider.ts @@ -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(); - const commits = new Map(); + const authors = new Map(); + const commits = new Map(); 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(); - const commits = new Map(); + const authors = new Map(); + const commits = new Map(); 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 { + async getCommit(repoPath: string, ref: string): Promise { 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(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 { + options?: { ref?: string; firstIfNotFound?: boolean; range?: Range }, + ): Promise { 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(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 { @@ -953,42 +969,60 @@ export class GitHubGitProvider implements GitProvider, Disposable { cursor: options?.cursor ?? options?.since, }); - const authors = new Map(); - const commits = new Map(); + const authors = new Map(); + const commits = new Map(); 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 | 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 { 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(); 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, 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(); - const commits = new Map(); + const authors = new Map(); + const commits = new Map(); 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() diff --git a/src/quickpicks/commitPicker.ts b/src/quickpicks/commitPicker.ts index bbee46d..3438aa8 100644 --- a/src/quickpicks/commitPicker.ts +++ b/src/quickpicks/commitPicker.ts @@ -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; showOtherReferences?: CommandQuickPickItem[]; }, - ): Promise { + ): Promise { const quickpick = window.createQuickPick(); quickpick.ignoreFocusOut = getQuickPickIgnoreFocusOut(); diff --git a/src/quickpicks/commitQuickPickItems.ts b/src/quickpicks/commitQuickPickItems.ts index b186184..9d3ec30 100644 --- a/src/quickpicks/commitQuickPickItems.ts +++ b/src/quickpicks/commitQuickPickItems.ts @@ -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 { 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 { - 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 { 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', diff --git a/src/quickpicks/gitQuickPickItems.ts b/src/quickpicks/gitQuickPickItems.ts index 4b3d3a1..9e46acc 100644 --- a/src/quickpicks/gitQuickPickItems.ts +++ b/src/quickpicks/gitQuickPickItems.ts @@ -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 = QuickPickItemOfT; +export type CommitQuickPickItem = QuickPickItemOfT; export namespace CommitQuickPickItem { - export function create( + export function create( 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 = { - 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 = { - 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 = { - 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 = { - 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, diff --git a/src/quickpicks/quickPicksItems.ts b/src/quickpicks/quickPicksItems.ts index 9994b8b..67a1b0d 100644 --- a/src/quickpicks/quickPicksItems.ts +++ b/src/quickpicks/quickPicksItems.ts @@ -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 { - if (GitStashCommit.is(this.reference)) { + if (GitCommit.isStash(this.reference)) { void (await GitActions.Stash.reveal(this.reference, { select: true, focus: !(options?.preserveFocus ?? false), diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index 80ca170..79fa775 100644 --- a/src/statusbar/statusBarController.ts +++ b/src/statusbar/statusBarController.ts @@ -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> | 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> | undefined, cancellationToken: CancellationToken, timeout: number, diff --git a/src/system/array.ts b/src/system/array.ts index 0c39736..fea52bf 100644 --- a/src/system/array.ts +++ b/src/system/array.ts @@ -206,15 +206,15 @@ export function compactHierarchy( export function uniqueBy( 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); } diff --git a/src/system/iterable.ts b/src/system/iterable.ts index 3bc8ebe..4b7ee70 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -204,3 +204,27 @@ export function* union(...sources: (Iterable | IterableIterator)[]): It } } } + +export function uniqueBy( + source: Iterable | IterableIterator, + uniqueKey: (item: TValue) => TKey, + onDuplicate: (original: TValue, current: TValue) => TValue | void, +): IterableIterator { + const uniques = new Map(); + + 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(); +} diff --git a/src/system/promise.ts b/src/system/promise.ts index 7634702..a6ea49a 100644 --- a/src/system/promise.ts +++ b/src/system/promise.ts @@ -52,7 +52,7 @@ export function cancellable( onDidCancel?(resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void): void; } = {}, ): Promise { - if (timeoutOrToken == null) return promise; + if (timeoutOrToken == null || (typeof timeoutOrToken === 'number' && timeoutOrToken <= 0)) return promise; return new Promise((resolve, reject) => { let fulfilled = false; diff --git a/src/trackers/gitLineTracker.ts b/src/trackers/gitLineTracker.ts index 7d5079e..334e826 100644 --- a/src/trackers/gitLineTracker.ts +++ b/src/trackers/gitLineTracker.ts @@ -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 { @@ -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()) diff --git a/src/views/branchesView.ts b/src/views/branchesView.ts index 1855a06..00f4582 100644 --- a/src/views/branchesView.ts +++ b/src/views/branchesView.ts @@ -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 diff --git a/src/views/commitsView.ts b/src/views/commitsView.ts index cebb388..79f0733 100644 --- a/src/views/commitsView.ts +++ b/src/views/commitsView.ts @@ -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); diff --git a/src/views/nodes/branchTrackingStatusFilesNode.ts b/src/views/nodes/branchTrackingStatusFilesNode.ts index 07188a7..8cddfaa 100644 --- a/src/views/nodes/branchTrackingStatusFilesNode.ts +++ b/src/views/nodes/branchTrackingStatusFilesNode.ts @@ -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(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; diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index ba385a1..e894bfc 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -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 { - 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 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 { 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 f.fileName === wf.fileName); + const index = files.findIndex(f => f.path === wf.path); if (index !== -1) { files.splice(index, 1, wf); } else { diff --git a/src/views/nodes/compareResultsNode.ts b/src/views/nodes/compareResultsNode.ts index 6a2ee7d..442fbdf 100644 --- a/src/views/nodes/compareResultsNode.ts +++ b/src/views/nodes/compareResultsNode.ts @@ -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 { diff --git a/src/views/nodes/fileHistoryNode.ts b/src/views/nodes/fileHistoryNode.ts index fdd1255..2ae7c70 100644 --- a/src/views/nodes/fileHistoryNode.ts +++ b/src/views/nodes/fileHistoryNode.ts @@ -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), diff --git a/src/views/nodes/fileRevisionAsCommitNode.ts b/src/views/nodes/fileRevisionAsCommitNode.ts index 8e2d37a..12e9df7 100644 --- a/src/views/nodes/fileRevisionAsCommitNode.ts +++ b/src/views/nodes/fileRevisionAsCommitNode.ts @@ -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 string | undefined; @@ -38,17 +38,11 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode { - 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 { - 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 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 { - 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( +export function* insertDateMarkers( iterable: Iterable, parent: ViewNode, skip?: number, @@ -33,7 +33,7 @@ export function* insertDateMarkers - 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, diff --git a/src/views/nodes/mergeConflictCurrentChangesNode.ts b/src/views/nodes/mergeConflictCurrentChangesNode.ts index 59561c3..fd7c36d 100644 --- a/src/views/nodes/mergeConflictCurrentChangesNode.ts +++ b/src/views/nodes/mergeConflictCurrentChangesNode.ts @@ -37,7 +37,7 @@ export class MergeConflictCurrentChangesNode 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, diff --git a/src/views/nodes/mergeConflictIncomingChangesNode.ts b/src/views/nodes/mergeConflictIncomingChangesNode.ts index 83dc7dd..6207f40 100644 --- a/src/views/nodes/mergeConflictIncomingChangesNode.ts +++ b/src/views/nodes/mergeConflictIncomingChangesNode.ts @@ -42,7 +42,7 @@ export class MergeConflictIncomingChangesNode 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`, ); diff --git a/src/views/nodes/rebaseStatusNode.ts b/src/views/nodes/rebaseStatusNode.ts index e66ad61..df943d6 100644 --- a/src/views/nodes/rebaseStatusNode.ts +++ b/src/views/nodes/rebaseStatusNode.ts @@ -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 { - 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 { 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( diff --git a/src/views/nodes/resultsFileNode.ts b/src/views/nodes/resultsFileNode.ts index 872f6d2..75a7a8b 100644 --- a/src/views/nodes/resultsFileNode.ts +++ b/src/views/nodes/resultsFileNode.ts @@ -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 { diff --git a/src/views/nodes/resultsFilesNode.ts b/src/views/nodes/resultsFilesNode.ts index 25894aa..401ec89 100644 --- a/src/views/nodes/resultsFilesNode.ts +++ b/src/views/nodes/resultsFilesNode.ts @@ -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(files.map(f => f.fileName)); + filterTo = new Set(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(commit.files.map(f => f.fileName)); + filterTo = new Set(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)), }; } } diff --git a/src/views/nodes/stashFileNode.ts b/src/views/nodes/stashFileNode.ts index 81fce71..036862a 100644 --- a/src/views/nodes/stashFileNode.ts +++ b/src/views/nodes/stashFileNode.ts @@ -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 { - 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); } diff --git a/src/views/nodes/stashNode.ts b/src/views/nodes/stashNode.ts index 9803792..20ffe4f 100644 --- a/src/views/nodes/stashNode.ts +++ b/src/views/nodes/stashNode.ts @@ -15,7 +15,7 @@ export class StashNode extends ViewRefNode { - // 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( diff --git a/src/views/nodes/statusFileNode.ts b/src/views/nodes/statusFileNode.ts index d9d1e92..59af497 100644 --- a/src/views/nodes/statusFileNode.ts +++ b/src/views/nodes/statusFileNode.ts @@ -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 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}^`, diff --git a/src/views/nodes/statusFilesNode.ts b/src/views/nodes/statusFilesNode.ts index 0fae623..77a9a88 100644 --- a/src/views/nodes/statusFilesNode.ts +++ b/src/views/nodes/statusFilesNode.ts @@ -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(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, }; } } diff --git a/src/views/remotesView.ts b/src/views/remotesView.ts index a4476db..5d7916d 100644 --- a/src/views/remotesView.ts +++ b/src/views/remotesView.ts @@ -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 diff --git a/src/views/repositoriesView.ts b/src/views/repositoriesView.ts index 7c469c2..f7e4774 100644 --- a/src/views/repositoriesView.ts +++ b/src/views/repositoriesView.ts @@ -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(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); diff --git a/src/webviews/rebaseEditor.ts b/src/webviews/rebaseEditor.ts index 5119212..8adbe7e 100644 --- a/src/webviews/rebaseEditor.ts +++ b/src/webviews/rebaseEditor.ts @@ -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, }); } diff --git a/src/webviews/webviewBase.ts b/src/webviews/webviewBase.ts index 8e7ff85..02d4a32 100644 --- a/src/webviews/webviewBase.ts +++ b/src/webviews/webviewBase.ts @@ -17,7 +17,7 @@ import { configuration } from '../configuration'; import { Container } from '../container'; import { CommitFormatter } from '../git/formatters'; import { - GitCommit2, + GitCommit, GitCommitIdentity, GitFileChange, GitFileIndexStatus, @@ -202,7 +202,7 @@ export abstract class WebviewBase implements Disposable { onIpcCommand(PreviewConfigurationCommandType, e, async params => { switch (params.type) { case 'commit': { - const commit = new GitCommit2( + const commit = new GitCommit( '~/code/eamodio/vscode-gitlens-demo', 'fe26af408293cba5b4bfd77306e1ac9ff7ccaef8', new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2016-11-12T20:41:00.000Z')), @@ -215,6 +215,7 @@ export abstract class WebviewBase implements Disposable { 'code.ts', GitFileIndexStatus.Modified, ), + undefined, [], );