diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 63b46a3..deccd3d 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 { GitCommit } from '../git/models'; +import { GitCommit2 } from '../git/models'; import { Strings } from '../system'; import { toRgba } from '../webviews/apps/shared/colors'; @@ -141,7 +141,7 @@ export class Annotations { } static gutter( - commit: GitCommit, + commit: GitCommit2, format: string, dateFormatOrFormatOptions: string | null | CommitFormatOptions, renderOptions: RenderOptions, @@ -227,7 +227,7 @@ export class Annotations { } static trailing( - commit: GitCommit, + commit: GitCommit2, // uri: GitUri, // editorLine: number, format: string, diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index ccbcab2..8159a12 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, GitBlameCommit, GitCommit } from '../git/models'; +import { GitBlame, GitCommit2 } from '../git/models'; import { Hovers } from '../hovers/hovers'; import { log } from '../system'; import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker'; @@ -171,19 +171,19 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase ); } - private async getDetailsHoverMessage(commit: GitBlameCommit, 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: 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; + // } + // } let editorLine = this.editor.selection.active.line; const line = editorLine + 1; @@ -191,7 +191,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase editorLine = commitLine.originalLine - 1; return Hovers.detailsMessage( - logCommit ?? commit, + commit, await GitUri.fromUri(document.uri), editorLine, this.container.config.hovers.detailsMarkdownFormat, diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index e0c2146..305ecee 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, GitBlameCommit } from '../git/models'; +import { GitBlame, GitCommit2 } 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: GitBlameCommit | undefined; + let commit: GitCommit2 | undefined; let compacted = false; let gutter: DecorationOptions | undefined; let previousSha: string | undefined; @@ -150,7 +150,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { decorationOptions.push(gutter); - if (avatars && commit.email != null) { + if (avatars && commit.author.email != null) { await this.applyAvatarDecoration(commit, gutter, gravatarDefault, avatarDecorationsMap!); } @@ -208,12 +208,12 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { } private async applyAvatarDecoration( - commit: GitBlameCommit, + commit: GitCommit2, gutter: DecorationOptions, gravatarDefault: GravatarDefaultStyle, map: Map, ) { - let avatarDecoration = map.get(commit.email!); + let avatarDecoration = map.get(commit.author.email ?? ''); if (avatarDecoration == null) { const url = (await commit.getAvatarUri({ defaultStyle: gravatarDefault, size: 16 })).toString(true); avatarDecoration = { @@ -224,7 +224,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { url, )});background-size:16px 16px;margin-left: 0 !important`, }; - map.set(commit.email!, avatarDecoration); + map.set(commit.author.email ?? '', avatarDecoration); } gutter.renderOptions!.after = avatarDecoration; diff --git a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts index 838a187..d479dc3 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 { GitBlameCommit } from '../git/models'; +import { GitCommit2 } from '../git/models'; import { Logger } from '../logger'; import { log, Stopwatch } from '../system'; import { GitDocumentState } from '../trackers/gitDocumentTracker'; @@ -32,7 +32,7 @@ export class GutterHeatmapBlameAnnotationProvider extends BlameAnnotationProvide >(); const computedHeatmap = await this.getComputedHeatmap(blame); - let commit: GitBlameCommit | undefined; + let commit: GitCommit2 | undefined; for (const l of blame.lines) { // editor lines are 0-based const editorLine = l.line - 1; diff --git a/src/annotations/lineAnnotationController.ts b/src/annotations/lineAnnotationController.ts index 41e79a5..b4911b4 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 { GitBlameCommit, PullRequest } from '../git/models'; +import { GitCommit2, 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, GitBlameCommit][], + lines: [number, GitCommit2][], { 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/avatars.ts b/src/avatars.ts index 1dda4f1..6c1168b 100644 --- a/src/avatars.ts +++ b/src/avatars.ts @@ -78,7 +78,7 @@ export function getAvatarUri( // Double the size to avoid blurring on the retina screen size *= 2; - if (email == null || email.length === 0) { + if (!email) { const avatar = createOrUpdateAvatar( `${missingGravatarHash}:${size}`, undefined, diff --git a/src/codelens/codeLensProvider.ts b/src/codelens/codeLensProvider.ts index 964e560..8da4fca 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, GitCommit } from '../git/models'; +import { GitBlame, GitBlameLines, GitCommit2 } from '../git/models'; import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { is, once } from '../system/function'; @@ -488,7 +488,7 @@ export class GitCodeLensProvider implements CodeLensProvider { const blame = lens.getBlame(); if (blame === undefined) return lens; - const recentCommit: GitCommit = first(blame.commits.values()); + const recentCommit = first(blame.commits.values()); // TODO@eamodio This is FAR too expensive, but this accounts for commits that delete lines -- is there another way? // if (lens.uri != null) { // const commit = await this.container.git.getCommitForFile(lens.uri.repoPath, lens.uri.fsPath, { @@ -503,7 +503,7 @@ export class GitCodeLensProvider implements CodeLensProvider { // } // } - let title = `${recentCommit.author}, ${ + let title = `${recentCommit.author.name}, ${ lens.dateFormat == null ? recentCommit.formattedDate : recentCommit.formatDate(lens.dateFormat) }`; if (this.container.config.debug) { @@ -594,7 +594,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return this.applyCommandWithNoClickAction(title, lens); } - const commit = find(blame.commits.values(), c => c.author === author) ?? first(blame.commits.values()); + const commit = find(blame.commits.values(), c => c.author.name === author) ?? first(blame.commits.values()); switch (lens.desiredCommand) { case CodeLensCommand.CopyRemoteCommitUrl: @@ -635,7 +635,7 @@ export class GitCodeLensProvider implements CodeLensProvider { private applyDiffWithPreviousCommand( title: string, lens: T, - commit: GitCommit | undefined, + commit: GitCommit2 | 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: GitCommit, + commit: GitCommit2, clipboard: boolean = false, ): T { lens.command = command<[OpenOnRemoteCommandArgs]>({ @@ -677,7 +677,7 @@ export class GitCodeLensProvider implements CodeLensProvider { private applyCopyOrOpenFileOnRemoteCommand( title: string, lens: T, - commit: GitCommit, + commit: GitCommit2, clipboard: boolean = false, ): T { lens.command = command<[OpenOnRemoteCommandArgs]>({ @@ -687,7 +687,7 @@ export class GitCodeLensProvider implements CodeLensProvider { { resource: { type: RemoteResourceType.Revision, - fileName: commit.fileName, + fileName: commit.file?.path ?? '', sha: commit.sha, }, repoPath: commit.repoPath, @@ -701,7 +701,7 @@ export class GitCodeLensProvider implements CodeLensProvider { private applyRevealCommitInViewCommand( title: string, lens: T, - commit: GitCommit | undefined, + commit: GitCommit2 | 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?: GitCommit, + commit?: GitCommit2, ): T { let refs; if (commit === undefined) { @@ -746,7 +746,7 @@ export class GitCodeLensProvider implements CodeLensProvider { private applyShowQuickCommitDetailsCommand( title: string, lens: T, - commit: GitCommit | undefined, + commit: GitCommit2 | 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: GitCommit | undefined, + commit: GitCommit2 | 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: GitCommit, + commit: GitCommit2, only?: boolean, ): T { lens.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({ diff --git a/src/commands/common.ts b/src/commands/common.ts index 00fde43..f19fc45 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -20,6 +20,7 @@ import { GitUri } from '../git/gitUri'; import { GitBranch, GitCommit, + GitCommit2, GitContributor, GitFile, GitReference, @@ -324,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 }).commit); + return GitCommit.is((context.node as ViewNode & { commit: GitCommit | GitCommit2 }).commit); } export function isCommandContextViewNodeHasContributor( diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index 0758999..e894022 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, GitRevision } from '../git/models'; +import { GitCommit, GitCommit2, 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; + commit?: GitCommit | GitCommit2; line?: number; showOptions?: TextDocumentShowOptions; @@ -31,6 +31,9 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { args.line = editor?.selection.active.line ?? 0; } + let lhsSha: string; + let lhsUri: Uri; + if (args.commit == null || args.commit.isUncommitted) { const blameline = args.line; if (blameline < 0) return; @@ -47,28 +50,37 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { args.commit = blame.commit; - // If the line is uncommitted, change the previous commit + // If the line is uncommitted, use previous commit (or index if the file is staged) if (args.commit.isUncommitted) { const status = await this.container.git.getStatusForFile(gitUri.repoPath!, gitUri.fsPath); - args.commit = args.commit.with({ - sha: status?.indexStatus != null ? GitRevision.uncommittedStaged : args.commit.previousSha!, - fileName: args.commit.previousFileName!, - originalFileName: null, - previousSha: null, - previousFileName: null, - }); - // editor lines are 0-based - args.line = blame.line.line - 1; + if (status?.indexStatus != null) { + lhsSha = GitRevision.uncommittedStaged; + lhsUri = this.container.git.getAbsoluteUri( + status.originalFileName || status.fileName, + args.commit.repoPath, + ); + } else { + lhsSha = args.commit.file!.previousSha ?? GitRevision.deletedOrMissing; + lhsUri = args.commit.file!.previousUri; + } + } else { + lhsSha = args.commit.sha; + lhsUri = args.commit.file!.uri; } + // editor lines are 0-based + args.line = blame.line.line - 1; } catch (ex) { Logger.error(ex, 'DiffLineWithWorkingCommand', `getBlameForLine(${blameline})`); void Messages.showGenericErrorMessage('Unable to open compare'); return; } + } else { + lhsSha = args.commit.sha; + lhsUri = args.commit.file?.uri ?? gitUri; } - const workingUri = await args.commit.getWorkingUri(); + const workingUri = await args.commit.file?.getWorkingUri(); if (workingUri == null) { void window.showWarningMessage('Unable to open compare. File has been deleted from the working tree'); @@ -78,8 +90,8 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { void (await executeCommand(Commands.DiffWith, { repoPath: args.commit.repoPath, lhs: { - sha: args.commit.sha, - uri: args.commit.uri, + sha: lhsSha, + uri: lhsUri, }, rhs: { sha: '', diff --git a/src/commands/diffWith.ts b/src/commands/diffWith.ts index d565d11..ae46d07 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, GitRevision } from '../git/models'; +import { GitCommit, GitCommit2, GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { basename } from '../system/path'; @@ -25,10 +25,10 @@ export interface DiffWithCommandArgs { @command() export class DiffWithCommand extends Command { static getMarkdownCommandArgs(args: DiffWithCommandArgs): string; - static getMarkdownCommandArgs(commit: GitCommit, line?: number): string; - static getMarkdownCommandArgs(argsOrCommit: DiffWithCommandArgs | GitCommit, line?: number): string { - let args: DiffWithCommandArgs | GitCommit; - if (GitCommit.is(argsOrCommit)) { + 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)) { const commit = argsOrCommit; if (commit.isUncommitted) { @@ -45,11 +45,16 @@ export class DiffWithCommand extends Command { line: line, }; } else { + if (commit.file == null) { + debugger; + throw new Error('Commit has no file'); + } + args = { repoPath: commit.repoPath, lhs: { - sha: commit.previousSha != null ? commit.previousSha : GitRevision.deletedOrMissing, - uri: commit.previousUri, + sha: commit.file.previousSha ?? GitRevision.deletedOrMissing, + uri: commit.file.previousUri, }, rhs: { sha: commit.sha, diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index 49f140e..6b861ad 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, GitRevision } from '../git/models'; +import { GitCommit, GitCommit2, 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?: GitCommit; + commit?: GitCommit2 | GitCommit; inDiffRightEditor?: boolean; uri?: Uri; @@ -58,7 +58,7 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand { repoPath: args.commit.repoPath, lhs: { sha: `${args.commit.sha}^`, - uri: args.commit.originalUri, + uri: args.commit.originalUri ?? args.commit.uri, }, rhs: { sha: args.commit.sha || '', diff --git a/src/commands/openCommitOnRemote.ts b/src/commands/openCommitOnRemote.ts index f81e84d..7f49537 100644 --- a/src/commands/openCommitOnRemote.ts +++ b/src/commands/openCommitOnRemote.ts @@ -1,6 +1,7 @@ import { TextEditor, Uri, window } from 'vscode'; import type { Container } from '../container'; import { GitUri } from '../git/gitUri'; +import { GitRevision } from '../git/models'; import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { Messages } from '../messages'; @@ -81,18 +82,10 @@ export class OpenCommitOnRemoteCommand extends ActiveEditorCommand { return; } - let commit = blame.commit; - // If the line is uncommitted, find the previous commit - if (commit.isUncommitted) { - commit = commit.with({ - sha: commit.previousSha, - fileName: commit.previousFileName, - previousSha: null, - previousFileName: null, - }); - } - - args.sha = commit.sha; + // If the line is uncommitted, use previous commit + args.sha = blame.commit.isUncommitted + ? blame.commit.file!.previousSha ?? GitRevision.deletedOrMissing + : blame.commit.sha; } void (await executeCommand(Commands.OpenOnRemote, { diff --git a/src/commands/showQuickCommit.ts b/src/commands/showQuickCommit.ts index eb63992..bc71c26 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, GitLog, GitLogCommit } from '../git/models'; +import { GitCommit, GitCommit2, GitLog, GitLogCommit } 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?: GitCommit | GitLogCommit; + commit?: GitCommit2 | GitCommit | GitLogCommit; repoLog?: GitLog; revealInView?: boolean; } @@ -115,7 +115,7 @@ export class ShowQuickCommitCommand extends ActiveEditorCachedCommand { } try { - if (args.commit == null || args.commit.isFile) { + if (args.commit == null || args.commit.file != null) { if (args.repoLog != null) { args.commit = args.repoLog.commits.get(args.sha); // If we can't find the commit, kill the repoLog @@ -148,7 +148,7 @@ export class ShowQuickCommitCommand extends ActiveEditorCachedCommand { void (await executeGitCommand({ command: 'show', state: { - repo: repoPath!, + repo: repoPath, reference: args.commit as GitLogCommit, }, })); diff --git a/src/commands/showQuickCommitFile.ts b/src/commands/showQuickCommitFile.ts index f626f3e..97eb15f 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 { GitBlameCommit, GitCommit, GitLog, GitLogCommit } from '../git/models'; +import { GitCommit2, GitLog, GitLogCommit } 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?: GitCommit | GitLogCommit; + commit?: GitCommit2 | GitLogCommit; fileLog?: GitLog; revisionUri?: string; } @@ -49,7 +49,7 @@ export class ShowQuickCommitFileCommand extends ActiveEditorCachedCommand { args.sha = context.node.uri.sha; if (isCommandContextViewNodeHasCommit(context)) { - args.commit = context.node.commit; + args.commit = context.node.commit as any; } } @@ -103,21 +103,21 @@ export class ShowQuickCommitFileCommand extends ActiveEditorCachedCommand { } try { - if (args.commit === undefined || !args.commit.isFile) { - if (args.fileLog !== undefined) { + if (args.commit == null /*|| args.commit.file != null*/) { + if (args.fileLog != null) { args.commit = args.fileLog.commits.get(args.sha); // If we can't find the commit, kill the fileLog - if (args.commit === undefined) { + if (args.commit == null) { args.fileLog = undefined; } } - if (args.fileLog === undefined) { - const repoPath = args.commit === undefined ? gitUri.repoPath : args.commit.repoPath; + if (args.fileLog == null) { + const repoPath = args.commit?.repoPath ?? gitUri.repoPath; args.commit = await this.container.git.getCommitForFile(repoPath, gitUri, { ref: args.sha, }); - if (args.commit === undefined) { + if (args.commit == null) { void Messages.showCommitNotFoundWarningMessage('Unable to show commit file details'); return; @@ -125,25 +125,31 @@ export class ShowQuickCommitFileCommand extends ActiveEditorCachedCommand { } } - if (args.commit === undefined) { + if (args.commit == null) { void Messages.showCommitNotFoundWarningMessage('Unable to show commit file details'); return; } + const path = args.commit?.file?.path ?? gitUri.fsPath; + if (GitCommit2.is(args.commit)) { + if (args.commit.files == null) { + await args.commit.ensureFullDetails(); + } + } + // const shortSha = GitRevision.shorten(args.sha); - const fileName = args.commit.fileName; - if (args.commit instanceof GitBlameCommit) { - args.commit = (await this.container.git.getCommit(args.commit.repoPath, args.commit.ref))!; - } + // if (args.commit instanceof GitBlameCommit) { + // args.commit = (await this.container.git.getCommit(args.commit.repoPath, args.commit.ref))!; + // } void (await executeGitCommand({ command: 'show', state: { repo: args.commit.repoPath, - reference: args.commit as GitLogCommit, - fileName: fileName, + reference: args.commit, + fileName: path, }, })); diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index cddd2ae..21541a6 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -42,11 +42,11 @@ import { BranchSortOptions, GitAuthor, GitBlame, - GitBlameCommit, GitBlameLine, GitBlameLines, GitBranch, GitBranchReference, + GitCommit2, GitCommitType, GitContributor, GitDiff, @@ -1083,7 +1083,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const commit = blame.commits.get(blameLine.sha); if (commit == null) return undefined; - const author = blame.authors.get(commit.author)!; + const author = blame.authors.get(commit.author.name)!; return { author: { ...author, lineCount: commit.lines.length }, commit: commit, @@ -1134,7 +1134,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const commit = blame.commits.get(blameLine.sha); if (commit == null) return undefined; - const author = blame.authors.get(commit.author)!; + const author = blame.authors.get(commit.author.name)!; return { author: { ...author, lineCount: commit.lines.length }, commit: commit, @@ -1197,7 +1197,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const endLine = range.end.line + 1; const authors = new Map(); - const commits = new Map(); + const commits = new Map(); for (const c of blame.commits.values()) { if (!shas.has(c.sha)) continue; @@ -1206,10 +1206,10 @@ export class LocalGitProvider implements GitProvider, Disposable { }); commits.set(c.sha, commit); - let author = authors.get(commit.author); + let author = authors.get(commit.author.name); if (author == null) { author = { - name: commit.author, + name: commit.author.name, lineCount: 0, }; authors.set(author.name, author); @@ -2261,7 +2261,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return undefined; } - authors.set(c.author, log.authors.get(c.author)!); + authors.set(c.author.name, log.authors.get(c.author.name)!); return [ref, c]; }, ), @@ -2839,12 +2839,12 @@ export class LocalGitProvider implements GitProvider, Disposable { // If line is committed, diff with line ref with previous else { ref = blameLine.commit.sha; - path = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? path); + path = blameLine.commit.file?.path ?? blameLine.commit.file?.originalPath ?? path; uri = this.getAbsoluteUri(path, repoPath); editorLine = blameLine.line.originalLine - 1; - if (skip === 0 && blameLine.commit.previousSha) { - previous = GitUri.fromFile(path, repoPath, blameLine.commit.previousSha); + if (skip === 0 && blameLine.commit.file?.previousSha) { + previous = GitUri.fromFile(path, repoPath, blameLine.commit.file.previousSha); } } } else { @@ -2868,12 +2868,12 @@ export class LocalGitProvider implements GitProvider, Disposable { // Diff with line ref with previous ref = blameLine.commit.sha; - path = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? path); + path = blameLine.commit.file?.path ?? blameLine.commit.file?.originalPath ?? path; uri = this.getAbsoluteUri(path, repoPath); editorLine = blameLine.line.originalLine - 1; - if (skip === 0 && blameLine.commit.previousSha) { - previous = GitUri.fromFile(path, repoPath, blameLine.commit.previousSha); + 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 c14e0b7..439701b 100644 --- a/src/git/formatters/commitFormatter.ts +++ b/src/git/formatters/commitFormatter.ts @@ -19,12 +19,18 @@ import { Iterables, Strings } from '../../system'; import { PromiseCancelledError } from '../../system/promise'; import { ContactPresence } from '../../vsls/vsls'; import type { GitUri } from '../gitUri'; -import { GitCommit, GitLogCommit, GitRemote, GitRevision, IssueOrPullRequest, PullRequest } from '../models'; +import { + GitCommit, + GitCommit2, + GitLogCommit, + GitRemote, + GitRevision, + IssueOrPullRequest, + PullRequest, +} from '../models'; import { RemoteProvider } from '../remotes/provider'; import { FormatOptions, Formatter } from './formatter'; -const emptyStr = ''; - export interface CommitFormatOptions extends FormatOptions { autolinkedIssuesOrPullRequests?: Map; avatarSize?: number; @@ -77,29 +83,29 @@ export interface CommitFormatOptions extends FormatOptions { }; } -export class CommitFormatter extends Formatter { +export class CommitFormatter extends Formatter { private get _authorDate() { - return this._item.formatAuthorDate(this._options.dateFormat); + return this._item.author.formatDate(this._options.dateFormat); } private get _authorDateAgo() { - return this._item.formatAuthorDateFromNow(); + return this._item.author.fromNow(); } private get _authorDateAgoShort() { - return this._item.formatCommitterDateFromNow(true); + return this._item.author.fromNow(true); } private get _committerDate() { - return this._item.formatCommitterDate(this._options.dateFormat); + return this._item.committer.formatDate(this._options.dateFormat); } private get _committerDateAgo() { - return this._item.formatCommitterDateFromNow(); + return this._item.committer.fromNow(); } private get _committerDateAgoShort() { - return this._item.formatCommitterDateFromNow(true); + return this._item.committer.fromNow(true); } private get _date() { @@ -116,16 +122,16 @@ export class CommitFormatter extends Formatter { private get _pullRequestDate() { const { pullRequestOrRemote: pr } = this._options; - if (pr == null || !PullRequest.is(pr)) return emptyStr; + if (pr == null || !PullRequest.is(pr)) return ''; - return pr.formatDate(this._options.dateFormat) ?? emptyStr; + return pr.formatDate(this._options.dateFormat) ?? ''; } private get _pullRequestDateAgo() { const { pullRequestOrRemote: pr } = this._options; - if (pr == null || !PullRequest.is(pr)) return emptyStr; + if (pr == null || !PullRequest.is(pr)) return ''; - return pr.formatDateFromNow() ?? emptyStr; + return pr.formatDateFromNow() ?? ''; } private get _pullRequestDateOrAgo() { @@ -157,12 +163,11 @@ export class CommitFormatter extends Formatter { } get author(): string { - const author = this._padOrTruncate(this._item.author, this._options.tokenOptions.author); - if (!this._options.markdown) { - return author; - } + const { name, email } = this._item.author; + const author = this._padOrTruncate(name, this._options.tokenOptions.author); + if (!this._options.markdown) return author; - return `[${author}](mailto:${this._item.email} "Email ${this._item.author} (${this._item.email})")`; + return `[${author}](mailto:${email} "Email ${name} (${email})")`; } get authorAgo(): string { @@ -192,25 +197,26 @@ export class CommitFormatter extends Formatter { } get authorNotYou(): string { - if (this._item.author === 'You') return this._padOrTruncate(emptyStr, this._options.tokenOptions.authorNotYou); + const { name, email } = this._item.author; + if (name === 'You') return this._padOrTruncate('', this._options.tokenOptions.authorNotYou); - const author = this._padOrTruncate(this._item.author, this._options.tokenOptions.authorNotYou); - if (!this._options.markdown) { - return author; - } + const author = this._padOrTruncate(name, this._options.tokenOptions.authorNotYou); + if (!this._options.markdown) return author; - return `[${author}](mailto:${this._item.email} "Email ${this._item.author} (${this._item.email})")`; + return `[${author}](mailto:${email} "Email ${name} (${email})")`; } get avatar(): string | Promise { if (!this._options.markdown || !Container.instance.config.hovers.avatars) { - return this._padOrTruncate(emptyStr, this._options.tokenOptions.avatar); + return this._padOrTruncate('', this._options.tokenOptions.avatar); } + const { name } = this._item.author; + const presence = this._options.presence; if (presence != null) { - const title = `${this._item.author} ${this._item.author === 'You' ? 'are' : 'is'} ${ - presence.status === 'dnd' ? 'in ' : emptyStr + const title = `${name} ${name === 'You' ? 'are' : 'is'} ${ + presence.status === 'dnd' ? 'in ' : '' }${presence.statusText.toLocaleLowerCase()}`; const avatarMarkdownPromise = this._getAvatarMarkdown(title, this._options.avatarSize); @@ -222,7 +228,7 @@ export class CommitFormatter extends Formatter { ); } - return this._getAvatarMarkdown(this._item.author, this._options.avatarSize); + return this._getAvatarMarkdown(name, this._options.avatarSize); } private async _getAvatarMarkdown(title: string, size?: number) { @@ -243,31 +249,27 @@ export class CommitFormatter extends Formatter { get changes(): string { return this._padOrTruncate( - GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus() : emptyStr, + GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus() : '', this._options.tokenOptions.changes, ); } get changesDetail(): string { return this._padOrTruncate( - GitLogCommit.is(this._item) - ? this._item.getFormattedDiffStatus({ expand: true, separator: ', ' }) - : emptyStr, + GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus({ expand: true, separator: ', ' }) : '', this._options.tokenOptions.changesDetail, ); } get changesShort(): string { return this._padOrTruncate( - GitLogCommit.is(this._item) - ? this._item.getFormattedDiffStatus({ compact: true, separator: emptyStr }) - : emptyStr, + GitLogCommit.is(this._item) ? this._item.getFormattedDiffStatus({ compact: true, separator: '' }) : '', this._options.tokenOptions.changesShort, ); } get commands(): string { - if (!this._options.markdown) return this._padOrTruncate(emptyStr, this._options.tokenOptions.commands); + if (!this._options.markdown) return this._padOrTruncate('', this._options.tokenOptions.commands); let commands; if (this._item.isUncommitted) { @@ -284,11 +286,11 @@ export class CommitFormatter extends Formatter { commands += `  [$(chevron-left)$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs({ lhs: { - sha: diffUris.previous.sha ?? emptyStr, + sha: diffUris.previous.sha ?? '', uri: diffUris.previous.documentUri(), }, rhs: { - sha: diffUris.current.sha ?? emptyStr, + sha: diffUris.current.sha ?? '', uri: diffUris.current.documentUri(), }, repoPath: this._item.repoPath, @@ -370,6 +372,8 @@ export class CommitFormatter extends Formatter { } if (Container.instance.actionRunners.count('hover.commands') > 0) { + const { name, email } = this._item.author; + commands += `${separator}[$(organization) Team${GlyphChars.SpaceThinnest}${ GlyphChars.Ellipsis }](${getMarkdownActionCommand('hover.commands', { @@ -377,8 +381,8 @@ export class CommitFormatter extends Formatter { commit: { sha: this._item.sha, author: { - name: this._item.author, - email: this._item.email, + name: name, + email: email, presence: this._options.presence, }, }, @@ -430,13 +434,14 @@ export class CommitFormatter extends Formatter { } get email(): string { - return this._padOrTruncate(this._item.email ?? emptyStr, this._options.tokenOptions.email); + const { email } = this._item.author; + return this._padOrTruncate(email ?? '', this._options.tokenOptions.email); } get footnotes(): string { return this._padOrTruncate( this._options.footnotes == null || this._options.footnotes.size === 0 - ? emptyStr + ? '' : Iterables.join( Iterables.map(this._options.footnotes, ([i, footnote]) => this._options.markdown ? footnote : `${Strings.getSuperscript(i)} ${footnote}`, @@ -448,7 +453,7 @@ export class CommitFormatter extends Formatter { } get id(): string { - const sha = this._padOrTruncate(this._item.shortSha ?? emptyStr, this._options.tokenOptions.id); + const sha = this._padOrTruncate(this._item.shortSha ?? '', this._options.tokenOptions.id); if (this._options.markdown && this._options.unpublished) { return `${sha} (unpublished)`; } @@ -471,7 +476,7 @@ export class CommitFormatter extends Formatter { ); } - let message = this._item.message; + let message = this._item.message ?? this._item.summary; if (this._options.messageTruncateAtNewLine) { const index = message.indexOf('\n'); if (index !== -1) { @@ -501,7 +506,7 @@ export class CommitFormatter extends Formatter { get pullRequest(): string { const { pullRequestOrRemote: pr } = this._options; - if (pr == null) return this._padOrTruncate(emptyStr, this._options.tokenOptions.pullRequest); + if (pr == null) return this._padOrTruncate('', this._options.tokenOptions.pullRequest); let text; if (PullRequest.is(pr)) { @@ -543,9 +548,9 @@ export class CommitFormatter extends Formatter { } else if (pr instanceof PromiseCancelledError) { text = this._options.markdown ? `[PR $(loading~spin)](command:${Commands.RefreshHover} "Searching for a Pull Request (if any) that introduced this commit...")` - : this._options?.pullRequestPendingMessage ?? emptyStr; + : this._options?.pullRequestPendingMessage ?? ''; } else { - return this._padOrTruncate(emptyStr, this._options.tokenOptions.pullRequest); + return this._padOrTruncate('', this._options.tokenOptions.pullRequest); } return this._padOrTruncate(text, this._options.tokenOptions.pullRequest); @@ -566,13 +571,13 @@ export class CommitFormatter extends Formatter { get pullRequestState(): string { const { pullRequestOrRemote: pr } = this._options; return this._padOrTruncate( - pr == null || !PullRequest.is(pr) ? emptyStr : pr.state ?? emptyStr, + pr == null || !PullRequest.is(pr) ? '' : pr.state ?? '', this._options.tokenOptions.pullRequestState, ); } get sha(): string { - return this._padOrTruncate(this._item.shortSha ?? emptyStr, this._options.tokenOptions.sha); + return this._padOrTruncate(this._item.shortSha ?? '', this._options.tokenOptions.sha); } get tips(): string { @@ -583,19 +588,19 @@ export class CommitFormatter extends Formatter { .map(t => `  ${t}  `) .join(GlyphChars.Space.repeat(3)); } - return this._padOrTruncate(branchAndTagTips ?? emptyStr, this._options.tokenOptions.tips); + return this._padOrTruncate(branchAndTagTips ?? '', this._options.tokenOptions.tips); } - static fromTemplate(template: string, commit: GitCommit, dateFormat: string | null): string; - static fromTemplate(template: string, commit: GitCommit, options?: CommitFormatOptions): string; + static fromTemplate(template: string, commit: GitCommit | GitCommit2, dateFormat: string | null): string; + static fromTemplate(template: string, commit: GitCommit | GitCommit2, options?: CommitFormatOptions): string; static fromTemplate( template: string, - commit: GitCommit, + commit: GitCommit | GitCommit2, dateFormatOrOptions?: string | null | CommitFormatOptions, ): string; static fromTemplate( template: string, - commit: GitCommit, + commit: GitCommit | GitCommit2, dateFormatOrOptions?: string | null | CommitFormatOptions, ): string { if (dateFormatOrOptions == null || typeof dateFormatOrOptions === 'string') { @@ -618,16 +623,24 @@ export class CommitFormatter extends Formatter { return super.fromTemplateCore(this, template, commit, dateFormatOrOptions); } - static fromTemplateAsync(template: string, commit: GitCommit, dateFormat: string | null): Promise; - static fromTemplateAsync(template: string, commit: GitCommit, options?: CommitFormatOptions): Promise; static fromTemplateAsync( template: string, - commit: GitCommit, + commit: GitCommit | GitCommit2, + dateFormat: string | null, + ): Promise; + static fromTemplateAsync( + template: string, + commit: GitCommit | GitCommit2, + options?: CommitFormatOptions, + ): Promise; + static fromTemplateAsync( + template: string, + commit: GitCommit | GitCommit2, dateFormatOrOptions?: string | null | CommitFormatOptions, ): Promise; static fromTemplateAsync( template: string, - commit: GitCommit, + commit: GitCommit | GitCommit2, dateFormatOrOptions?: string | null | CommitFormatOptions, ): Promise { if (CommitFormatter.has(template, 'footnotes')) { diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 1d7921d..1035d66 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -41,6 +41,7 @@ import { BranchDateFormatting, BranchSortOptions, CommitDateFormatting, + CommitShaFormatting, GitBlame, GitBlameLine, GitBlameLines, @@ -155,6 +156,7 @@ export class GitProviderService implements Disposable { BranchDateFormatting.reset(); CommitDateFormatting.reset(); + CommitShaFormatting.reset(); PullRequestDateFormatting.reset(); this.updateContext(); @@ -184,6 +186,10 @@ export class GitProviderService implements Disposable { PullRequestDateFormatting.reset(); } + if (configuration.changed(e, 'advanced.abbreviatedShaLength')) { + CommitShaFormatting.reset(); + } + if (configuration.changed(e, 'views.contributors.showAllBranches')) { this.resetCaches('contributors'); } diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 6a89779..229c0a0 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, GitFile, GitRevision } from './models'; +import { GitCommit, GitCommit2, GitFile, GitRevision } from './models'; export interface GitCommitish { fileName?: string; @@ -230,7 +230,7 @@ export class GitUri extends (Uri as any as UriEx) { return Container.instance.git.getAbsoluteUri(this.fsPath, this.repoPath); } - static fromCommit(commit: GitCommit, previous: boolean = false) { + static fromCommit(commit: GitCommit | GitCommit2, previous: boolean = false) { if (!previous) return new GitUri(commit.uri, commit); return new GitUri(commit.previousUri, { diff --git a/src/git/models.ts b/src/git/models.ts index d29252a..188fd1b 100644 --- a/src/git/models.ts +++ b/src/git/models.ts @@ -1,6 +1,5 @@ export * from './models/author'; export * from './models/blame'; -export * from './models/blameCommit'; export * from './models/branch'; export * from './models/commit'; export * from './models/contributor'; diff --git a/src/git/models/blame.ts b/src/git/models/blame.ts index ed77105..ccdfe17 100644 --- a/src/git/models/blame.ts +++ b/src/git/models/blame.ts @@ -1,16 +1,15 @@ -import { GitBlameCommit } from './blameCommit'; -import { GitAuthor, GitCommitLine } from './commit'; +import { GitAuthor, GitCommit2, GitCommitLine } from './commit'; export interface GitBlame { readonly repoPath: string; readonly authors: Map; - readonly commits: Map; + readonly commits: Map; readonly lines: GitCommitLine[]; } export interface GitBlameLine { readonly author?: GitAuthor; - readonly commit: GitBlameCommit; + readonly commit: GitCommit2; readonly line: GitCommitLine; } @@ -20,6 +19,6 @@ export interface GitBlameLines extends GitBlame { export interface GitBlameCommitLines { readonly author: GitAuthor; - readonly commit: GitBlameCommit; + readonly commit: GitCommit2; readonly lines: GitCommitLine[]; } diff --git a/src/git/models/blameCommit.ts b/src/git/models/blameCommit.ts deleted file mode 100644 index be1f1d3..0000000 --- a/src/git/models/blameCommit.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { GitCommit, GitCommitLine, GitCommitType } from './commit'; - -export class GitBlameCommit extends GitCommit { - static override is(commit: any): commit is GitBlameCommit { - return ( - commit instanceof GitBlameCommit - //|| (commit.repoPath !== undefined && commit.sha !== undefined && commit.type === GitCommitType.Blame) - ); - } - - constructor( - repoPath: string, - sha: string, - author: string, - email: string | undefined, - authorDate: Date, - committerDate: Date, - message: string, - fileName: string, - originalFileName: string | undefined, - previousSha: string | undefined, - previousFileName: string | undefined, - public readonly lines: GitCommitLine[], - ) { - super( - GitCommitType.Blame, - repoPath, - sha, - author, - email, - authorDate, - committerDate, - message, - fileName, - originalFileName, - previousSha, - previousFileName, - ); - } - - with(changes: { - sha?: string; - fileName?: string; - originalFileName?: string | null; - previousFileName?: string | null; - previousSha?: string | null; - lines?: GitCommitLine[] | null; - }): GitBlameCommit { - return new GitBlameCommit( - this.repoPath, - changes.sha ?? this.sha, - this.author, - this.email, - this.authorDate, - this.committerDate, - this.message, - changes.fileName ?? this.fileName, - this.getChangedValue(changes.originalFileName, this.originalFileName), - this.getChangedValue(changes.previousSha, this.previousSha), - this.getChangedValue(changes.previousFileName, this.previousFileName), - this.getChangedValue(changes.lines, changes.sha ?? changes.fileName ? [] : this.lines) ?? [], - ); - } -} diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index 1b26d3b..de2b1db 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -6,7 +6,14 @@ import { formatDate, fromNow } from '../../system/date'; import { memoize } from '../../system/decorators/memoize'; import { CommitFormatter } from '../formatters'; import { GitUri } from '../gitUri'; -import { GitReference, GitRevision, GitRevisionReference, PullRequest } from '../models'; +import { + GitFileIndexStatus, + GitFileStatus, + GitReference, + GitRevision, + GitRevisionReference, + PullRequest, +} from '../models'; export interface GitAuthor { name: string; @@ -41,7 +48,308 @@ export const CommitDateFormatting = { }, }; +export const CommitShaFormatting = { + length: undefined! as number, + + reset: () => { + // Don't allow shas to be shortened to less than 5 characters + CommitShaFormatting.length = Math.max(5, Container.instance.config.advanced.abbreviatedShaLength); + }, +}; + +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); + } + + @memoize() + getWorkingUri(): Promise { + return Container.instance.git.getWorkingUri(this.repoPath, this.uri); + } +} + +const stashNumberRegex = /stash@{(\d+)}/; + +export class GitCommit2 implements GitRevisionReference { + static is(commit: any): commit is GitCommit2 { + return commit instanceof GitCommit2; + } + + static hasFullDetails(commit: GitCommit2): commit is GitCommit2 & SomeNonNullable { + return commit.message != null && commit.files != null && commit.parents.length !== 0; + } + + static isOfRefType(commit: GitReference | undefined) { + return commit?.refType === 'revision' || commit?.refType === 'stash'; + } + + readonly lines: GitCommitLine[]; + readonly ref: string; + readonly refType: GitRevisionReference['refType']; + readonly shortSha: string; + readonly stashName: string | undefined; + readonly stashNumber: number | undefined; + + constructor( + public readonly repoPath: string, + public readonly sha: string, + public readonly author: GitCommitIdentity, + public readonly committer: GitCommitIdentity, + public readonly summary: string, + public readonly parents: string[], + message?: string | undefined, + files?: GitFileChange | GitFileChange[] | undefined, + lines?: GitCommitLine | GitCommitLine[] | undefined, + stashName?: string | undefined, + ) { + this.ref = this.sha; + this.refType = 'revision'; + this.shortSha = this.sha.substring(0, CommitShaFormatting.length); + + if (message != null) { + this._message = message; + } + + if (files != null) { + if (Array.isArray(files)) { + this._files = files; + } else { + this._file = files; + } + } + + if (lines != null) { + if (Array.isArray(lines)) { + this.lines = lines; + } else { + this.lines = [lines]; + } + } else { + this.lines = []; + } + + if (stashName) { + this.stashName = stashName || undefined; + this.stashNumber = Number(stashNumberRegex.exec(stashName)?.[1]); + } + } + + get date(): Date { + return CommitDateFormatting.dateSource === DateSource.Committed ? this.committer.date : this.author.date; + } + + private _file: GitFileChange | undefined; + get file(): GitFileChange | undefined { + return this._file; + } + + private _files: GitFileChange[] | undefined; + get files(): GitFileChange[] | undefined { + return this._files; + } + + get formattedDate(): string { + return CommitDateFormatting.dateStyle === DateStyle.Absolute + ? this.formatDate(CommitDateFormatting.dateFormat) + : 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); + } + + @memoize() + get isUncommittedStaged(): boolean { + 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); + } + + /** @deprecated use `file.originalUri` */ + get originalUri(): Uri | undefined { + return this.file?.originalUri; + } + + /** @deprecated use `file.getWorkingUri` */ + getWorkingUri(): Promise { + return Promise.resolve(this.file?.getWorkingUri()); + } + + /** @deprecated use `file.previousUri` */ + get previousUri(): Uri /*| undefined*/ { + return this.file?.previousUri ?? Container.instance.git.getAbsoluteUri(this.repoPath, this.repoPath); + } + + /** @deprecated use `file.previousSha` */ + get previousSha(): string | undefined { + return this.file?.previousSha; + } + + async ensureFullDetails(): Promise { + if (this.isUncommitted || GitCommit2.hasFullDetails(this)) return; + + const commit = await Container.instance.git.getCommit(this.repoPath, this.sha); + if (commit == null) return; + + 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)); + } + + formatDate(format?: string | null) { + return CommitDateFormatting.dateSource === DateSource.Committed + ? this.committer.formatDate(format) + : this.author.formatDate(format); + } + + formatDateFromNow(short?: boolean) { + return CommitDateFormatting.dateSource === DateSource.Committed + ? this.committer.fromNow(short) + : this.author.fromNow(short); + } + + // TODO@eamodio deal with memoization, since we don't want the timeout to apply + @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); + } + + getAvatarUri(options?: { defaultStyle?: GravatarDefaultStyle; size?: number }): Uri | Promise { + return this.author.getAvatarUri(this, options); + } + + @memoize((u, e, r) => `${u.toString()}|${e}|${r ?? ''}`) + getPreviousLineDiffUris(uri: Uri, editorLine: number, ref: string | undefined) { + return this.file?.path + ? Container.instance.git.getPreviousLineDiffUris(this.repoPath, uri, editorLine, ref) + : Promise.resolve(undefined); + } + + @memoize() + toGitUri(previous: boolean = false): GitUri { + return GitUri.fromCommit(this, previous); + } + + with(changes: { + sha?: string; + parents?: string[]; + files?: GitFileChange | GitFileChange[] | null; + lines?: GitCommitLine[]; + }): GitCommit2 { + return new GitCommit2( + this.repoPath, + changes.sha ?? this.sha, + this.author, + this.committer, + this.summary, + this.getChangedValue(changes.parents, this.parents) ?? [], + this.message, + this.getChangedValue(changes.files, this.files), + this.getChangedValue(changes.lines, this.lines), + this.stashName, + ); + } + + protected getChangedValue(change: T | null | undefined, original: T | undefined): T | undefined { + if (change === undefined) return original; + return change !== null ? change : undefined; + } +} + 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; } @@ -56,8 +364,8 @@ export abstract class GitCommit implements GitRevisionReference { public readonly type: GitCommitType, public readonly repoPath: string, public readonly sha: string, - public readonly author: string, - public readonly email: string | undefined, + public readonly authorName: string, + public readonly authorEmail: string | undefined, public readonly authorDate: Date, public readonly committerDate: Date, public readonly message: string, @@ -209,7 +517,7 @@ export abstract class GitCommit implements GitRevisionReference { } getAvatarUri(options?: { defaultStyle?: GravatarDefaultStyle; size?: number }): Uri | Promise { - return getAvatarUri(this.email, this, options); + return getAvatarUri(this.authorEmail, this, options); } @memoize() diff --git a/src/git/models/file.ts b/src/git/models/file.ts index 405badb..3852d88 100644 --- a/src/git/models/file.ts +++ b/src/git/models/file.ts @@ -32,7 +32,7 @@ export const enum GitFileWorkingTreeStatus { } export interface GitFile { - status: GitFileConflictStatus | GitFileIndexStatus | GitFileWorkingTreeStatus; + status: GitFileStatus; readonly repoPath?: string; readonly conflictStatus?: GitFileConflictStatus; readonly indexStatus?: GitFileIndexStatus; diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index b65edef..1eaa1a5 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -3,8 +3,8 @@ import { Container } from '../../container'; import { memoize, Strings } from '../../system'; import { GitUri } from '../gitUri'; import { GitReference } from '../models'; -import { GitCommit, GitCommitType } from './commit'; -import { GitFile, GitFileStatus } from './file'; +import { GitCommit, GitCommitType, GitFileChange } from './commit'; +import { GitFile, GitFileIndexStatus, GitFileStatus } from './file'; const emptyStats = Object.freeze({ added: 0, @@ -24,6 +24,10 @@ export interface GitLogCommitLine { } 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'; } @@ -39,6 +43,7 @@ export class GitLogCommit extends GitCommit { nextSha?: string; nextFileName?: string; + readonly lines: GitLogCommitLine[]; constructor( type: GitCommitType, @@ -62,7 +67,7 @@ export class GitLogCommit extends GitCommit { } | undefined, public readonly parentShas?: string[], - public readonly line?: GitLogCommitLine, + lines?: GitLogCommitLine[], ) { super( type, @@ -78,6 +83,19 @@ export class GitLogCommit extends GitCommit { 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() @@ -222,8 +240,8 @@ export class GitLogCommit extends GitCommit { changes.type ?? this.type, this.repoPath, this.getChangedValue(changes.sha, this.sha)!, - changes.author ?? this.author, - changes.email ?? this.email, + changes.author ?? this.authorName, + changes.email ?? this.authorEmail, changes.authorDate ?? this.authorDate, changes.committedDate ?? this.committerDate, changes.message ?? this.message, @@ -235,7 +253,7 @@ export class GitLogCommit extends GitCommit { this.getChangedValue(changes.previousFileName, this.previousFileName), this._fileStats, this.parentShas, - this.line, + this.lines, ); } } diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index a775353..1457552 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -1,9 +1,17 @@ -import { debug, Strings } from '../../system'; +import { debug } from '../../system/decorators/log'; import { normalizePath, relative } from '../../system/path'; -import { GitAuthor, GitBlame, GitBlameCommit, GitCommitLine, GitRevision, GitUser } from '../models'; - -const emptyStr = ''; -const slash = '/'; +import { getLines } from '../../system/string'; +import { + GitAuthor, + GitBlame, + GitCommit2, + GitCommitIdentity, + GitCommitLine, + GitFileChange, + GitFileIndexStatus, + GitRevision, + GitUser, +} from '../models'; interface BlameEntry { sha: string; @@ -17,8 +25,10 @@ interface BlameEntry { authorTimeZone?: string; authorEmail?: string; + committer: string; committerDate?: string; committerTimeZone?: string; + committerEmail?: string; previousSha?: string; previousFileName?: string; @@ -39,7 +49,7 @@ export class GitBlameParser { if (!data) return undefined; const authors = new Map(); - const commits = new Map(); + const commits = new Map(); const lines: GitCommitLine[] = []; let relativeFileName; @@ -50,13 +60,14 @@ export class GitBlameParser { let first = true; - for (line of Strings.lines(data)) { + for (line of getLines(data)) { lineParts = line.split(' '); if (lineParts.length < 2) continue; if (entry === undefined) { entry = { author: undefined!, + committer: undefined!, sha: lineParts[0], originalLine: parseInt(lineParts[1], 10), line: parseInt(lineParts[2], 10), @@ -102,6 +113,33 @@ export class GitBlameParser { entry.authorTimeZone = lineParts[1]; break; + case 'committer': + if (GitRevision.isUncommitted(entry.sha)) { + entry.committer = 'You'; + } else { + entry.committer = lineParts.slice(1).join(' ').trim(); + } + break; + + case 'committer-mail': { + if (GitRevision.isUncommitted(entry.sha)) { + entry.committerEmail = currentUser !== undefined ? currentUser.email : undefined; + continue; + } + + entry.committerEmail = lineParts.slice(1).join(' ').trim(); + const start = entry.committerEmail.indexOf('<'); + if (start >= 0) { + const end = entry.committerEmail.indexOf('>', start); + if (end > start) { + entry.committerEmail = entry.committerEmail.substring(start + 1, end); + } else { + entry.committerEmail = entry.committerEmail.substring(start + 1); + } + } + + break; + } case 'committer-time': entry.committerDate = lineParts[1]; break; @@ -125,10 +163,7 @@ export class GitBlameParser { if (first && repoPath === 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.fileName}` : entry.fileName, ''), ); relativeFileName = normalizePath(relative(repoPath, fileName)); } else { @@ -147,10 +182,10 @@ export class GitBlameParser { } for (const [, c] of commits) { - if (c.author === undefined) return undefined; + if (!c.author.name) continue; - const author = authors.get(c.author); - if (author === undefined) return undefined; + const author = authors.get(c.author.name); + if (author == undefined) return undefined; author.lineCount += c.lines.length; } @@ -170,28 +205,28 @@ export class GitBlameParser { entry: BlameEntry, repoPath: string | undefined, relativeFileName: string, - commits: Map, + commits: Map, authors: Map, lines: GitCommitLine[], currentUser: { name?: string; email?: string } | undefined, ) { let commit = commits.get(entry.sha); - if (commit === undefined) { - if (entry.author !== undefined) { + 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.authorEmail) + (currentUser.email == null || currentUser.email === entry.authorEmail) ) { entry.author = 'You'; } let author = authors.get(entry.author); - if (author === undefined) { + if (author == null) { author = { name: entry.author, lineCount: 0, @@ -200,20 +235,27 @@ export class GitBlameParser { } } - commit = new GitBlameCommit( + commit = new GitCommit2( repoPath!, entry.sha, - entry.author, - entry.authorEmail, - new Date((entry.authorDate as any) * 1000), - new Date((entry.committerDate as any) * 1000), + 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.summary!, - relativeFileName, - entry.previousFileName !== undefined && entry.previousFileName !== entry.fileName - ? entry.previousFileName - : undefined, - entry.previousSha, - entry.previousSha && entry.previousFileName, + [], + undefined, + new GitFileChange( + repoPath!, + relativeFileName, + GitFileIndexStatus.Modified, + entry.previousFileName && entry.previousFileName !== entry.fileName + ? entry.previousFileName + : undefined, + entry.previousSha, + ), [], ); @@ -225,13 +267,10 @@ export class GitBlameParser { sha: entry.sha, line: entry.line + i, originalLine: entry.originalLine + i, + previousSha: commit.file?.previousSha, }; - if (commit.previousSha) { - line.previousSha = commit.previousSha; - } - - commit.lines.push(line); + commit.lines?.push(line); lines[line.line - 1] = line; } } diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index ce43465..52dd11d 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -81,7 +81,7 @@ export class GitDiffParser { let hasRemoved; let removed = 0; - for (const l of Strings.lines(hunk.diff)) { + for (const l of Strings.getLines(hunk.diff)) { switch (l[0]) { case '+': hasAddedOrChanged = true; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 5db6883..765da34 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -1,6 +1,7 @@ import { Range } from 'vscode'; -import { Arrays, debug, Strings } from '../../system'; +import { Arrays, debug } from '../../system'; import { normalizePath, relative } from '../../system/path'; +import { getLines } from '../../system/string'; import { GitAuthor, GitCommitType, @@ -105,7 +106,7 @@ export class GitLogParser { let i = 0; let first = true; - const lines = Strings.lines(`${data}`); + const lines = getLines(`${data}`); // Skip the first line since it will always be let next = lines.next(); if (next.done) return undefined; @@ -455,7 +456,7 @@ export class GitLogParser { undefined, entry.fileStats, entry.parentShas, - entry.line, + entry.line != null ? [entry.line] : [], ); commits.set(entry.ref!, commit); diff --git a/src/git/parsers/stashParser.ts b/src/git/parsers/stashParser.ts index f670ca3..138e029 100644 --- a/src/git/parsers/stashParser.ts +++ b/src/git/parsers/stashParser.ts @@ -40,7 +40,7 @@ export class GitStashParser { static parse(data: string, repoPath: string): GitStash | undefined { if (!data) return undefined; - const lines = Strings.lines(`${data}`); + const lines = Strings.getLines(`${data}`); // Skip the first line since it will always be let next = lines.next(); if (next.done) return undefined; diff --git a/src/hovers/hovers.ts b/src/hovers/hovers.ts index 0b572cc..824d0c5 100644 --- a/src/hovers/hovers.ts +++ b/src/hovers/hovers.ts @@ -6,8 +6,7 @@ import { Container } from '../container'; import { CommitFormatter } from '../git/formatters'; import { GitUri } from '../git/gitUri'; import { - GitBlameCommit, - GitCommit, + GitCommit2, GitDiffHunk, GitDiffHunkLine, GitLogCommit, @@ -16,12 +15,13 @@ import { PullRequest, } from '../git/models'; import { Logger, LogLevel } from '../logger'; -import { Iterables, Strings } from '../system'; +import { count } from '../system/iterable'; import { PromiseCancelledError } from '../system/promise'; +import { getDurationMilliseconds } from '../system/string'; export namespace Hovers { export async function changesMessage( - commit: GitBlameCommit | GitLogCommit, + commit: GitCommit2, uri: GitUri, editorLine: number, document: TextDocument, @@ -29,7 +29,7 @@ export namespace Hovers { const documentRef = uri.sha; async function getDiff() { - if (!GitBlameCommit.is(commit)) return undefined; + if (commit.file == null) return undefined; // TODO: Figure out how to optimize this let ref; @@ -38,7 +38,7 @@ export namespace Hovers { ref = documentRef; } } else { - ref = commit.previousSha; + ref = commit.file.previousSha; if (ref == null) { return `\`\`\`diff\n+ ${document.lineAt(editorLine).text}\n\`\`\``; } @@ -47,10 +47,10 @@ export namespace Hovers { const line = editorLine + 1; const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0]; - let originalFileName = commit.originalFileName; - if (originalFileName == null) { - if (uri.fsPath !== commit.uri.fsPath) { - originalFileName = commit.fileName; + let originalPath = commit.file.originalPath; + if (originalPath == null) { + if (uri.fsPath !== commit.file.uri.fsPath) { + originalPath = commit.file.path; } } @@ -96,9 +96,7 @@ export namespace Hovers { previous = diffUris.previous.sha == null || diffUris.previous.isUncommitted ? `  _${GitRevision.shorten(diffUris.previous.sha, { - strings: { - working: 'Working Tree', - }, + strings: { working: 'Working Tree' }, })}_  ${GlyphChars.ArrowLeftRightLong}  ` : `  [$(git-commit) ${GitRevision.shorten( diffUris.previous.sha || '', @@ -122,10 +120,11 @@ export namespace Hovers { editorLine, )} "Open Changes")`; - if (commit.previousSha) { - previous = `  [$(git-commit) ${ - commit.previousShortSha - }](${ShowQuickCommitCommand.getMarkdownCommandArgs(commit.previousSha)} "Show Commit")  ${ + const previousSha = commit.file?.previousSha; + if (previousSha) { + previous = `  [$(git-commit) ${GitRevision.shorten( + previousSha, + )}](${ShowQuickCommitCommand.getMarkdownCommandArgs(previousSha)} "Show Commit")  ${ GlyphChars.ArrowLeftRightLong } `; } @@ -190,7 +189,7 @@ export namespace Hovers { } export async function detailsMessage( - commit: GitCommit, + commit: GitCommit2, uri: GitUri, editorLine: number, format: string, @@ -212,13 +211,21 @@ export namespace Hovers { dateFormat = 'MMMM Do, YYYY h:mma'; } + let message = commit.message ?? commit.summary; + if (commit.message == null && !commit.isUncommitted) { + await commit.ensureFullDetails(); + message = commit.message ?? commit.summary; + + if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString(); + } + const remotes = await Container.instance.git.getRemotesWithProviders(commit.repoPath, { sort: true }); if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString(); const [previousLineDiffUris, autolinkedIssuesOrPullRequests, pr, presence] = await Promise.all([ commit.isUncommitted ? commit.getPreviousLineDiffUris(uri, editorLine, uri.sha) : undefined, - getAutoLinkedIssuesOrPullRequests(commit.message, remotes), + getAutoLinkedIssuesOrPullRequests(message, remotes), options?.pullRequests?.pr ?? getPullRequestForCommit(commit.ref, remotes, { pullRequests: @@ -232,7 +239,7 @@ export namespace Hovers { 'pullRequestState', ), }), - Container.instance.vsls.maybeGetPresence(commit.email), + Container.instance.vsls.maybeGetPresence(commit.author.email), ]); if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString(); @@ -284,14 +291,14 @@ export namespace Hovers { !Container.instance.config.hovers.autolinks.enhanced || !CommitFormatter.has(Container.instance.config.hovers.detailsMarkdownFormat, 'message') ) { - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return undefined; } const remote = await Container.instance.git.getRichRemoteProvider(remotes); if (remote?.provider == null) { - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return undefined; } @@ -306,15 +313,15 @@ export namespace Hovers { if (autolinks != null && Logger.enabled(LogLevel.Debug)) { // If there are any issues/PRs that timed out, log it - const count = Iterables.count(autolinks.values(), pr => pr instanceof PromiseCancelledError); - if (count !== 0) { + const prCount = count(autolinks.values(), pr => pr instanceof PromiseCancelledError); + if (prCount !== 0) { Logger.debug( cc, `timed out ${ GlyphChars.Dash - } ${count} issue/pull request queries took too long (over ${timeout} ms) ${ + } ${prCount} issue/pull request queries took too long (over ${timeout} ms) ${ GlyphChars.Dot - } ${Strings.getDurationMilliseconds(start)} ms`, + } ${getDurationMilliseconds(start)} ms`, ); // const pending = [ @@ -336,11 +343,11 @@ export namespace Hovers { } } - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return autolinks; } catch (ex) { - Logger.error(ex, cc, `failed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.error(ex, cc, `failed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return undefined; } @@ -359,14 +366,14 @@ export namespace Hovers { const start = hrtime(); if (!options?.pullRequests) { - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return undefined; } const remote = await Container.instance.git.getRichRemoteProvider(remotes, { includeDisconnected: true }); if (remote?.provider == null) { - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return undefined; } @@ -374,7 +381,7 @@ export namespace Hovers { const { provider } = remote; const connected = provider.maybeConnected ?? (await provider.isConnected()); if (!connected) { - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return remote; } @@ -382,17 +389,17 @@ export namespace Hovers { try { const pr = await Container.instance.git.getPullRequestForCommit(ref, provider, { timeout: 250 }); - Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `completed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return pr; } catch (ex) { if (ex instanceof PromiseCancelledError) { - Logger.debug(cc, `timed out ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.debug(cc, `timed out ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return ex; } - Logger.error(ex, cc, `failed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); + Logger.error(ex, cc, `failed ${GlyphChars.Dot} ${getDurationMilliseconds(start)} ms`); return undefined; } diff --git a/src/hovers/lineHoverController.ts b/src/hovers/lineHoverController.ts index 7692127..808d2b9 100644 --- a/src/hovers/lineHoverController.ts +++ b/src/hovers/lineHoverController.ts @@ -116,22 +116,22 @@ export class LineHoverController implements Disposable { ); if (!wholeLine && range.start.character !== position.character) return undefined; - // Get the full commit message -- since blame only returns the summary - let logCommit = lineState?.logCommit; - if (logCommit == null && !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.previousSha = commit.previousSha; - logCommit.previousFileName = commit.previousFileName; - - if (lineState != null) { - lineState.logCommit = logCommit; - } - } - } + // // Get the full commit message -- since blame only returns the summary + // let logCommit = lineState?.logCommit; + // if (logCommit == null && !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.previousSha = commit.previousSha; + // logCommit.previousFileName = commit.previousFileName; + + // if (lineState != null) { + // lineState.logCommit = logCommit; + // } + // } + // } let editorLine = position.line; const line = editorLine + 1; @@ -142,7 +142,7 @@ export class LineHoverController implements Disposable { if (trackedDocument == null) return undefined; const message = await Hovers.detailsMessage( - logCommit ?? commit, + commit, trackedDocument.uri, editorLine, this.container.config.hovers.detailsMarkdownFormat, diff --git a/src/messages.ts b/src/messages.ts index fdb065f..e5c2f5b 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 } from './git/models'; +import { GitCommit, GitCommit2 } from './git/models'; import { Logger } from './logger'; export const enum SuppressedMessages { @@ -18,7 +18,9 @@ export const enum SuppressedMessages { } export class Messages { - static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise { + static showCommitHasNoPreviousCommitWarningMessage( + commit?: GitCommit | GitCommit2, + ): Promise { if (commit === undefined) { return Messages.showMessage( 'info', @@ -28,7 +30,7 @@ export class Messages { } return Messages.showMessage( 'info', - `Commit ${commit.shortSha} (${commit.author}, ${commit.formattedDate}) has no previous commit.`, + `Commit ${commit.shortSha} (${commit.author.name}, ${commit.formattedDate}) has no previous commit.`, SuppressedMessages.CommitHasNoPreviousCommitWarning, ); } diff --git a/src/premium/github/github.ts b/src/premium/github/github.ts index 53ff399..fd56247 100644 --- a/src/premium/github/github.ts +++ b/src/premium/github/github.ts @@ -524,7 +524,11 @@ export class GitHubApi { email name } - committer { date } + committer { + date + email + name + } } } } @@ -645,20 +649,24 @@ export class GitHubApi { const result = rsp?.data; if (result == null) return undefined; + const { commit } = result; return { oid: result.sha, parents: { nodes: result.parents.map(p => ({ oid: p.sha })) }, - message: result.commit.message, + message: commit.message, additions: result.stats?.additions, changedFiles: result.files?.length, deletions: result.stats?.deletions, author: { - date: result.commit.author?.date ?? result.commit.committer?.date ?? new Date().toString(), - email: result.commit.author?.email ?? undefined, - name: result.commit.author?.name ?? '', + avatarUrl: result.author?.avatar_url ?? undefined, + date: commit.author?.date ?? new Date().toString(), + email: commit.author?.email ?? undefined, + name: commit.author?.name ?? '', }, committer: { - date: result.commit.committer?.date ?? result.commit.author?.date ?? new Date().toString(), + date: commit.committer?.date ?? new Date().toString(), + email: commit.committer?.email ?? undefined, + name: commit.committer?.name ?? '', }, files: result.files, }; @@ -720,11 +728,16 @@ export class GitHubApi { changedFiles deletions author { + avatarUrl + date + email + name + } + committer { date email name } - committer { date } } } } @@ -783,23 +796,28 @@ export class GitHubApi { endCursor hasNextPage } - nodes { ... on Commit { ...commit } } + nodes { + ... on Commit { + oid + message + parents(first: 3) { nodes { oid } } + author { + avatarUrl + date + email + name + } + committer { + date + email + name + } + } + } } } } } -} - -fragment commit on Commit { - oid - message - parents(first: 100) { nodes { oid } } - author { - date - email - name - } - committer { date } }`; const rsp = await this.graphql(token, query, { @@ -1195,6 +1213,8 @@ export interface GitHubBlameRange { }; committer: { date: string; + email: string; + name: string; }; }; } @@ -1216,12 +1236,8 @@ export interface GitHubCommit { additions: number | undefined; changedFiles: number | undefined; deletions: number | undefined; - author: { - date: string; - email: string | undefined; - name: string; - }; - committer: { date: string }; + author: { avatarUrl: string | undefined; date: string; email: string | undefined; name: string }; + committer: { date: string; email: string | undefined; name: string }; files?: Endpoints['GET /repos/{owner}/{repo}/commits/{ref}']['response']['data']['files']; } diff --git a/src/premium/github/githubGitProvider.ts b/src/premium/github/githubGitProvider.ts index 05b20ad..f25f3ae 100644 --- a/src/premium/github/githubGitProvider.ts +++ b/src/premium/github/githubGitProvider.ts @@ -36,11 +36,12 @@ import { BranchSortOptions, GitAuthor, GitBlame, - GitBlameCommit, GitBlameLine, GitBlameLines, GitBranch, GitBranchReference, + GitCommit2, + GitCommitIdentity, GitCommitLine, GitCommitType, GitContributor, @@ -49,6 +50,7 @@ import { GitDiffHunkLine, GitDiffShortStat, GitFile, + GitFileChange, GitFileIndexStatus, GitLog, GitLogCommit, @@ -376,7 +378,8 @@ export class GitHubGitProvider implements GitProvider, Disposable { if (context == null) return undefined; const { metadata, github, remotehub, session } = context; - const file = this.getRelativePath(uri, remotehub.getProviderRootUri(uri)); + const root = remotehub.getVirtualUri(remotehub.getProviderRootUri(uri)); + const file = this.getRelativePath(uri, root); // const sha = await this.resolveReferenceCore(uri.repoPath!, metadata, uri.sha); // if (sha == null) return undefined; @@ -391,17 +394,19 @@ export class GitHubGitProvider implements GitProvider, Disposable { ); const authors = new Map(); - const commits = 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 && range.commit.author.name === viewer ? 'You' : range.commit.author.name; + const name = viewer != null && c.author.name === viewer ? 'You' : c.author.name; - let author = authors.get(range.commit.author.name); + let author = authors.get(c.author.name); if (author == null) { author = { - name: range.commit.author.name, + name: c.author.name, lineCount: 0, }; authors.set(name, author); @@ -409,29 +414,26 @@ export class GitHubGitProvider implements GitProvider, Disposable { author.lineCount += range.endingLine - range.startingLine + 1; - let commit = commits.get(range.commit.oid); + let commit = commits.get(c.oid); if (commit == null) { - commit = new GitBlameCommit( + commit = new GitCommit2( uri.repoPath!, - range.commit.oid, - author.name, - range.commit.author.email, - new Date(range.commit.author.date), - new Date(range.commit.committer.date), - range.commit.message, - file, - undefined, - range.commit.parents.nodes[0]?.oid, - undefined, + 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)), + 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), [], ); - commits.set(range.commit.oid, commit); + commits.set(c.oid, commit); } for (let i = range.startingLine; i <= range.endingLine; i++) { const line: GitCommitLine = { - sha: range.commit.oid, + sha: c.oid, line: i, originalLine: i, }; @@ -495,7 +497,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { const commit = blame.commits.get(blameLine.sha); if (commit == null) return undefined; - const author = blame.authors.get(commit.author)!; + const author = blame.authors.get(commit.author.name)!; return { author: { ...author, lineCount: commit.lines.length }, commit: commit, @@ -545,7 +547,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { const endLine = range.end.line + 1; const authors = new Map(); - const commits = new Map(); + const commits = new Map(); for (const c of blame.commits.values()) { if (!shas.has(c.sha)) continue; @@ -554,10 +556,10 @@ export class GitHubGitProvider implements GitProvider, Disposable { }); commits.set(c.sha, commit); - let author = authors.get(commit.author); + let author = authors.get(commit.author.name); if (author == null) { author = { - name: commit.author, + name: commit.author.name, lineCount: 0, }; authors.set(author.name, author); @@ -1234,7 +1236,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { return undefined; } - authors.set(c.author, log.authors.get(c.author)!); + authors.set(c.author.name, log.authors.get(c.author.name)!); return [ref, c]; }, ), diff --git a/src/quickpicks/gitQuickPickItems.ts b/src/quickpicks/gitQuickPickItems.ts index 282627e..4b3d3a1 100644 --- a/src/quickpicks/gitQuickPickItems.ts +++ b/src/quickpicks/gitQuickPickItems.ts @@ -188,7 +188,7 @@ export namespace CommitQuickPickItem { if (options.compact) { const item: CommitQuickPickItem = { label: `${options.icon ? pad('$(git-commit)', 0, 2) : ''}${commit.getShortMessage()}`, - description: `${commit.author}, ${commit.formattedDate}${pad('$(git-commit)', 2, 2)}${ + description: `${commit.author.name}, ${commit.formattedDate}${pad('$(git-commit)', 2, 2)}${ commit.shortSha }${pad(GlyphChars.Dot, 2, 2)}${commit.getFormattedDiffStatus({ compact: true })}`, alwaysShow: options.alwaysShow, @@ -202,7 +202,7 @@ export namespace CommitQuickPickItem { const item: CommitQuickPickItem = { label: `${options.icon ? pad('$(git-commit)', 0, 2) : ''}${commit.getShortMessage()}`, description: '', - detail: `${GlyphChars.Space.repeat(2)}${commit.author}, ${commit.formattedDate}${pad( + detail: `${GlyphChars.Space.repeat(2)}${commit.author.name}, ${commit.formattedDate}${pad( '$(git-commit)', 2, 2, diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index a600172..80ca170 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 { GitBlameCommit, PullRequest } from '../git/models'; +import { GitCommit2, 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: GitBlameCommit, options?: { pr?: PullRequest | null }) { + private async updateBlame(editor: TextEditor, commit: GitCommit2, options?: { pr?: PullRequest | null }) { const cfg = this.container.config.statusBar; if (!cfg.enabled || this._statusBarBlame == null || !isTextEditor(editor)) return; @@ -332,7 +332,7 @@ export class StatusBarController implements Disposable { } private async getPullRequest( - commit: GitBlameCommit, + commit: GitCommit2, { timeout }: { timeout?: number } = {}, ): Promise> | undefined> { const remote = await this.container.git.getRichRemoteProvider(commit.repoPath); @@ -348,7 +348,7 @@ export class StatusBarController implements Disposable { private async updateCommitTooltip( statusBarItem: StatusBarItem, - commit: GitBlameCommit, + commit: GitCommit2, actionTooltip: string, getBranchAndTagTips: | (( @@ -386,7 +386,7 @@ export class StatusBarController implements Disposable { private async waitForPendingPullRequest( editor: TextEditor, - commit: GitBlameCommit, + commit: GitCommit2, pr: PullRequest | PromiseCancelledError> | undefined, cancellationToken: CancellationToken, timeout: number, diff --git a/src/system/string.ts b/src/system/string.ts index 809e010..3c8dbf0 100644 --- a/src/system/string.ts +++ b/src/system/string.ts @@ -137,6 +137,19 @@ export function getDurationMilliseconds(start: [number, number]) { return secs * 1000 + Math.floor(nanosecs / 1000000); } +export function* getLines(s: string, char: string = '\n'): IterableIterator { + let i = 0; + while (i < s.length) { + let j = s.indexOf(char, i); + if (j === -1) { + j = s.length; + } + + yield s.substring(i, j); + i = j + 1; + } +} + const superscripts = ['\u00B9', '\u00B2', '\u00B3', '\u2074', '\u2075', '\u2076', '\u2077', '\u2078', '\u2079']; export function getSuperscript(num: number) { @@ -249,19 +262,6 @@ export function isUpperAsciiLetter(code: number): boolean { return code >= CharCode.A && code <= CharCode.Z; } -export function* lines(s: string, char: string = '\n'): IterableIterator { - let i = 0; - while (i < s.length) { - let j = s.indexOf(char, i); - if (j === -1) { - j = s.length; - } - - yield s.substring(i, j); - i = j + 1; - } -} - export function md5(s: string, encoding: 'base64' | 'hex' = 'base64'): string { return _md5(s, encoding); } diff --git a/src/trackers/gitLineTracker.ts b/src/trackers/gitLineTracker.ts index 1323caf..7d5079e 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 { GitBlameCommit, GitLogCommit } from '../git/models'; +import { GitCommit2, GitLogCommit } from '../git/models'; import { Logger } from '../logger'; import { debug } from '../system'; import { @@ -16,7 +16,7 @@ import { LinesChangeEvent, LineSelection, LineTracker } from './lineTracker'; export * from './lineTracker'; export class GitLineState { - constructor(public readonly commit: GitBlameCommit | undefined, public logCommit?: GitLogCommit) {} + constructor(public readonly commit: GitCommit2 | undefined, public logCommit?: GitLogCommit) {} } export class GitLineTracker extends LineTracker { diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index 03d0ed9..ba385a1 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -129,8 +129,8 @@ export class CommitFileNode c.ref.startsWith(onto)) : undefined; if (ontoCommit != null) { - if (!authors.has(ontoCommit.author)) { - authors.set(ontoCommit.author, { - author: ontoCommit.author, + const { name, email } = ontoCommit.author; + if (!authors.has(name)) { + authors.set(name, { + author: name, avatarUrl: ( await ontoCommit.getAvatarUri({ defaultStyle: container.config.defaultGravatarsStyle }) ).toString(true), - email: ontoCommit.email, + email: email, }); } commits.push({ ref: ontoCommit.ref, - author: ontoCommit.author, + author: name, date: ontoCommit.formatDate(container.config.defaultDateFormat), dateFromNow: ontoCommit.formatDateFromNow(), message: ontoCommit.message || 'root', @@ -566,19 +567,20 @@ async function parseRebaseTodo( onto = ''; } - if (!authors.has(commit.author)) { - authors.set(commit.author, { - author: commit.author, + const { name, email } = commit.author; + if (!authors.has(name)) { + authors.set(name, { + author: name, avatarUrl: ( await commit.getAvatarUri({ defaultStyle: container.config.defaultGravatarsStyle }) ).toString(true), - email: commit.email, + email: email, }); } commits.push({ ref: commit.ref, - author: commit.author, + author: name, date: commit.formatDate(container.config.defaultDateFormat), dateFromNow: commit.formatDateFromNow(), message: commit.message, diff --git a/src/webviews/webviewBase.ts b/src/webviews/webviewBase.ts index 10030a7..8e7ff85 100644 --- a/src/webviews/webviewBase.ts +++ b/src/webviews/webviewBase.ts @@ -16,7 +16,14 @@ import { Commands } from '../commands'; import { configuration } from '../configuration'; import { Container } from '../container'; import { CommitFormatter } from '../git/formatters'; -import { GitBlameCommit, PullRequest, PullRequestState } from '../git/models'; +import { + GitCommit2, + GitCommitIdentity, + GitFileChange, + GitFileIndexStatus, + PullRequest, + PullRequestState, +} from '../git/models'; import { Logger } from '../logger'; import { DidChangeConfigurationNotificationType, @@ -195,18 +202,19 @@ export abstract class WebviewBase implements Disposable { onIpcCommand(PreviewConfigurationCommandType, e, async params => { switch (params.type) { case 'commit': { - const commit = new GitBlameCommit( + const commit = new GitCommit2( '~/code/eamodio/vscode-gitlens-demo', 'fe26af408293cba5b4bfd77306e1ac9ff7ccaef8', - 'You', - 'eamodio@gmail.com', - new Date('2016-11-12T20:41:00.000Z'), - new Date('2020-11-01T06:57:21.000Z'), + new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2016-11-12T20:41:00.000Z')), + new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2020-11-01T06:57:21.000Z')), + 'Supercharged', + ['3ac1d3f51d7cf5f438cc69f25f6740536ad80fef'], 'Supercharged', - 'code.ts', - undefined, - '3ac1d3f51d7cf5f438cc69f25f6740536ad80fef', - 'code.ts', + new GitFileChange( + '~/code/eamodio/vscode-gitlens-demo', + 'code.ts', + GitFileIndexStatus.Modified, + ), [], );