215 lines
6.6 KiB

'use strict';
import * as paths from 'path';
import { Command, Selection, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { Colors, GlyphChars } from '../../constants';
import { Container } from '../../container';
import { FileHistoryView } from '../fileHistoryView';
import {
CommitFormatter,
GitBranch,
GitFile,
GitLogCommit,
GitRevisionReference,
StatusFileFormatter,
} from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { LineHistoryView } from '../lineHistoryView';
import { MergeConflictCurrentChangesNode } from './mergeConflictCurrentChangesNode';
import { MergeConflictIncomingChangesNode } from './mergeConflictIncomingChangesNode';
import { ViewsWithCommits } from '../viewBase';
import { ContextValues, ViewNode, ViewRefFileNode } from './viewNode';
export class FileRevisionAsCommitNode extends ViewRefFileNode<ViewsWithCommits | FileHistoryView | LineHistoryView> {
constructor(
view: ViewsWithCommits | FileHistoryView | LineHistoryView,
parent: ViewNode,
public readonly file: GitFile,
public commit: GitLogCommit,
private readonly _options: {
branch?: GitBranch;
selection?: Selection;
unpublished?: boolean;
} = {},
) {
super(GitUri.fromFile(file, commit.repoPath, commit.sha), view, parent);
}
toClipboard(): string {
let message = this.commit.message;
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
return `${this.commit.shortSha}: ${message}`;
}
get fileName(): string {
return this.file.fileName;
}
get isTip(): boolean {
return (this._options.branch?.current && this._options.branch.sha === this.commit.ref) ?? false;
}
get ref(): GitRevisionReference {
return this.commit;
}
async getChildren(): Promise<ViewNode[]> {
if (!this.commit.hasConflicts) return [];
const [mergeStatus, rebaseStatus] = await Promise.all([
Container.git.getMergeStatus(this.commit.repoPath),
Container.git.getRebaseStatus(this.commit.repoPath),
]);
if (mergeStatus == null && rebaseStatus == null) return [];
return [
new MergeConflictCurrentChangesNode(this.view, this, (mergeStatus ?? rebaseStatus)!, this.file),
new MergeConflictIncomingChangesNode(this.view, this, (mergeStatus ?? rebaseStatus)!, this.file),
];
}
async getTreeItem(): Promise<TreeItem> {
if (!this.commit.isFile) {
// See if we can get the commit directly from the multi-file commit
const commit = this.commit.toFileCommit(this.file);
if (commit == null) {
const log = await Container.git.getLogForFile(this.repoPath, this.file.fileName, {
limit: 2,
ref: this.commit.sha,
});
if (log != null) {
this.commit = log.commits.get(this.commit.sha) ?? this.commit;
}
} else {
this.commit = commit;
}
}
const item = new TreeItem(
CommitFormatter.fromTemplate(this.view.config.formats.commits.label, this.commit, {
dateFormat: Container.config.defaultDateFormat,
messageTruncateAtNewLine: true,
}),
this.commit.hasConflicts ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None,
);
item.contextValue = this.contextValue;
item.description = CommitFormatter.fromTemplate(this.view.config.formats.commits.description, this.commit, {
dateFormat: Container.config.defaultDateFormat,
messageTruncateAtNewLine: true,
});
// eslint-disable-next-line no-template-curly-in-string
const status = StatusFileFormatter.fromTemplate('${status}${ (originalPath)}', this.file); // lgtm [js/template-syntax-in-string-literal]
item.tooltip = CommitFormatter.fromTemplate(
this.commit.isUncommitted
? `\${author} ${GlyphChars.Dash} \${id}\n${status}\n\${ago} (\${date})`
: `\${author}\${ (email)} ${GlyphChars.Dash} \${id}${
this._options.unpublished ? ' (unpublished)' : ''
}\n${status}\n\${ago} (\${date})\${\n\nmessage}${this.commit.getFormattedDiffStatus({
expand: true,
prefix: '\n\n',
separator: '\n',
})}\${\n\n${GlyphChars.Dash.repeat(2)}\nfootnotes}`,
this.commit,
{
dateFormat: Container.config.defaultDateFormat,
// messageAutolinks: true,
messageIndent: 4,
},
);
if (!this.commit.isUncommitted && this.view.config.avatars) {
item.iconPath = this._options.unpublished
? new ThemeIcon('arrow-up', new ThemeColor(Colors.UnpublishedCommitIconColor))
: await this.commit.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle });
}
if (item.iconPath == null) {
const icon = GitFile.getStatusIcon(this.file.status);
item.iconPath = {
dark: Container.context.asAbsolutePath(paths.join('images', 'dark', icon)),
light: Container.context.asAbsolutePath(paths.join('images', 'light', icon)),
};
}
item.command = this.getCommand();
return item;
}
protected get contextValue(): string {
if (!this.commit.isUncommitted) {
return `${ContextValues.File}+committed${this._options.branch?.current ? '+current' : ''}${
this.isTip ? '+HEAD' : ''
}${this._options.unpublished ? '+unpublished' : ''}`;
}
return this.commit.hasConflicts
? `${ContextValues.File}+conflicted`
: this.commit.isUncommittedStaged
? `${ContextValues.File}+staged`
: `${ContextValues.File}+unstaged`;
}
getCommand(): Command | undefined {
let line;
if (this.commit.line !== undefined) {
line = this.commit.line.to.line - 1;
} else {
line = this._options.selection !== undefined ? this._options.selection.active.line : 0;
}
if (this.commit.hasConflicts) {
return {
title: 'Open Changes',
command: Commands.DiffWith,
arguments: [
{
lhs: {
sha: 'MERGE_HEAD',
uri: GitUri.fromFile(this.file, this.repoPath, undefined, true),
},
rhs: {
sha: 'HEAD',
uri: GitUri.fromFile(this.file, this.repoPath),
},
repoPath: this.repoPath,
line: 0,
showOptions: {
preserveFocus: false,
preview: false,
},
},
],
};
}
const commandArgs: DiffWithPreviousCommandArgs = {
commit: this.commit,
uri: GitUri.fromFile(this.file, this.commit.repoPath),
line: line,
showOptions: {
preserveFocus: true,
preview: true,
},
};
return {
title: 'Open Changes with Previous Revision',
command: Commands.DiffWithPrevious,
arguments: [undefined, commandArgs],
};
}
async getConflictBaseUri(): Promise<Uri | undefined> {
if (!this.commit.hasConflicts) return undefined;
const mergeBase = await Container.git.getMergeBase(this.repoPath, 'MERGE_HEAD', 'HEAD');
return GitUri.fromFile(this.file, this.repoPath, mergeBase ?? 'HEAD');
}
}