浏览代码

Adds folder history support

main
Eric Amodio 3 年前
父节点
当前提交
fd08cf9deb
共有 23 个文件被更改,包括 336 次插入85 次删除
  1. +4
    -0
      CHANGELOG.md
  2. +64
    -9
      package.json
  3. +3
    -1
      src/annotations/gutterChangesAnnotationProvider.ts
  4. +3
    -1
      src/commands/common.ts
  5. +12
    -2
      src/commands/showQuickFileHistory.ts
  6. +1
    -0
      src/config.ts
  7. +10
    -1
      src/git/git.ts
  8. +20
    -5
      src/git/gitService.ts
  9. +1
    -0
      src/git/models/models.ts
  10. +2
    -4
      src/git/models/status.ts
  11. +6
    -0
      src/git/models/user.ts
  12. +2
    -2
      src/git/parsers/blameParser.ts
  13. +2
    -1
      src/git/parsers/logParser.ts
  14. +43
    -24
      src/system/array.ts
  15. +5
    -0
      src/views/fileHistoryView.ts
  16. +2
    -1
      src/views/nodes/commitFileNode.ts
  17. +4
    -3
      src/views/nodes/commitNode.ts
  18. +61
    -17
      src/views/nodes/fileHistoryNode.ts
  19. +17
    -4
      src/views/nodes/fileHistoryTrackerNode.ts
  20. +3
    -2
      src/views/nodes/folderNode.ts
  21. +18
    -2
      src/views/nodes/lineHistoryTrackerNode.ts
  22. +16
    -4
      src/views/viewCommands.ts
  23. +37
    -2
      src/webviews/apps/settings/partials/views.file-history.html

+ 4
- 0
CHANGELOG.md 查看文件

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased]
### Added
- Adds _Open Folder History_ command to folders in the _Explorer_ view to show the folder's history in the _File History_ view
### Fixed
- Fixes [1411](https://github.com/eamodio/vscode-gitlens/issues/1411) - Click on branch compare node does not expand the tree

+ 64
- 9
package.json 查看文件

@ -128,6 +128,8 @@
"onCommand:gitlens.showCommitInView",
"onCommand:gitlens.showCommitsInView",
"onCommand:gitlens.showFileHistoryInView",
"onCommand:gitlens.openFileHistory",
"onCommand:gitlens.openFolderHistory",
"onCommand:gitlens.showQuickCommitDetails",
"onCommand:gitlens.showQuickCommitFileDetails",
"onCommand:gitlens.showQuickRevisionDetails",
@ -2010,6 +2012,34 @@
"markdownDescription": "Specifies whether to show avatar images instead of status icons in the _File History_ view",
"scope": "window"
},
"gitlens.views.fileHistory.files.compact": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to compact (flatten) unnecessary file nesting in the _File History_ view. Only applies to folder history and when `#gitlens.views.fileHistory.files.layout#` is set to `tree` or `auto`",
"scope": "window"
},
"gitlens.views.fileHistory.files.layout": {
"type": "string",
"default": "auto",
"enum": [
"auto",
"list",
"tree"
],
"enumDescriptions": [
"Automatically switches between displaying files as a `tree` or `list` based on the `#gitlens.views.fileHistory.files.threshold#` value and the number of files at each nesting level",
"Displays files as a list",
"Displays files as a tree"
],
"markdownDescription": "Specifies how the _File History_ view will display files when showing the history of a folder",
"scope": "window"
},
"gitlens.views.fileHistory.files.threshold": {
"type": "number",
"default": 5,
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _File History_ view. Only applies to folder history and when `#gitlens.views.fileHistory.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.lineHistory.avatars": {
"type": "boolean",
"default": true,
@ -3372,6 +3402,16 @@
"category": "GitLens"
},
{
"command": "gitlens.openFileHistory",
"title": "Open File History",
"category": "GitLens"
},
{
"command": "gitlens.openFolderHistory",
"title": "Open Folder History",
"category": "GitLens"
},
{
"command": "gitlens.showQuickCommitDetails",
"title": "Show Commit",
"category": "GitLens"
@ -4508,13 +4548,15 @@
"command": "gitlens.views.fileHistory.setCursorFollowingOn",
"title": "Toggle History by: File",
"category": "GitLens",
"icon": "$(file)"
"icon": "$(file)",
"enablement": "gitlens:views:fileHistory:editorFollowing"
},
{
"command": "gitlens.views.fileHistory.setCursorFollowingOff",
"title": "Toggle History by: Selected Line(s)",
"category": "GitLens",
"icon": "$(list-selection)"
"icon": "$(list-selection)",
"enablement": "gitlens:views:fileHistory:editorFollowing || gitlens:views:fileHistory:cursorFollowing"
},
{
"command": "gitlens.views.fileHistory.setEditorFollowingOn",
@ -4530,13 +4572,13 @@
},
{
"command": "gitlens.views.fileHistory.setRenameFollowingOn",
"title": "Toggle Renames: Not Following",
"title": "Toggle Follow Renames: Off",
"category": "GitLens",
"enablement": "!config.gitlens.advanced.fileHistoryShowAllBranches"
},
{
"command": "gitlens.views.fileHistory.setRenameFollowingOff",
"title": "Toggle Renames: Following",
"title": "Toggle Follow Renames: On",
"category": "GitLens",
"enablement": "!config.gitlens.advanced.fileHistoryShowAllBranches"
},
@ -5274,9 +5316,17 @@
},
{
"command": "gitlens.showFileHistoryInView",
"when": "false"
},
{
"command": "gitlens.openFileHistory",
"when": "gitlens:activeFileStatus =~ /tracked/"
},
{
"command": "gitlens.openFolderHistory",
"when": "false"
},
{
"command": "gitlens.showQuickCommitDetails",
"when": "false"
},
@ -6354,7 +6404,7 @@
"alt": "gitlens.copyRemoteFileUrlFrom"
},
{
"command": "gitlens.showFileHistoryInView",
"command": "gitlens.openFileHistory",
"when": "gitlens:activeFileStatus =~ /tracked/ && config.gitlens.menus.editor.history",
"group": "2_gitlens@5"
},
@ -6531,7 +6581,7 @@
"alt": "gitlens.copyRemoteFileUrlFrom"
},
{
"command": "gitlens.showFileHistoryInView",
"command": "gitlens.openFileHistory",
"when": "gitlens:enabled && config.gitlens.menus.editorTab.history",
"group": "2_gitlens@4"
},
@ -6560,7 +6610,12 @@
"alt": "gitlens.copyRemoteFileUrlFrom"
},
{
"command": "gitlens.showFileHistoryInView",
"command": "gitlens.openFolderHistory",
"when": "!explorerResourceIsRoot && explorerResourceIsFolder && gitlens:enabled && config.gitlens.menus.explorer.history",
"group": "4_timeline@2"
},
{
"command": "gitlens.openFileHistory",
"when": "!explorerResourceIsRoot && !explorerResourceIsFolder && gitlens:enabled && config.gitlens.menus.explorer.history",
"group": "4_timeline@2"
},
@ -6650,7 +6705,7 @@
"group": "2_gitlens@1"
},
{
"command": "gitlens.showFileHistoryInView",
"command": "gitlens.openFileHistory",
"when": "gitlens:enabled && scmProvider == git && scmResourceGroup =~ /^(workingTree|index|merge)$/ && config.gitlens.menus.scmItem.history",
"group": "4_timeline@2"
},
@ -7760,7 +7815,7 @@
"alt": "gitlens.copyRemoteFileUrlToClipboard"
},
{
"command": "gitlens.showFileHistoryInView",
"command": "gitlens.openFileHistory",
"when": "view != gitlens.views.fileHistory && viewItem =~ /gitlens:file\\b/",
"group": "2_gitlens_quickopen@6"
},

+ 3
- 1
src/annotations/gutterChangesAnnotationProvider.ts 查看文件

@ -100,7 +100,9 @@ export class GutterChangesAnnotationProvider extends AnnotationProviderBase
this.trackedDocument.uri.repoPath!,
this.trackedDocument.uri.fsPath,
);
const commits = await status?.toPsuedoCommits();
const commits = status?.toPsuedoCommits(
await Container.git.getCurrentUser(this.trackedDocument.uri.repoPath!),
);
if (commits?.length) {
commit = await Container.git.getCommitForFile(
this.trackedDocument.uri.repoPath,

+ 3
- 1
src/commands/common.ts 查看文件

@ -82,11 +82,13 @@ export enum Commands {
OpenChangedFiles = 'gitlens.openChangedFiles',
OpenCommitOnRemote = 'gitlens.openCommitOnRemote',
OpenComparisonOnRemote = 'gitlens.openComparisonOnRemote',
OpenFileHistory = 'gitlens.openFileHistory',
OpenFileFromRemote = 'gitlens.openFileFromRemote',
OpenFileOnRemote = 'gitlens.openFileOnRemote',
OpenFileOnRemoteFrom = 'gitlens.openFileOnRemoteFrom',
OpenFileAtRevision = 'gitlens.openFileRevision',
OpenFileAtRevisionFrom = 'gitlens.openFileRevisionFrom',
OpenFolderHistory = 'gitlens.openFolderHistory',
OpenOnRemote = 'gitlens.openOnRemote',
OpenPullRequestOnRemote = 'gitlens.openPullRequestOnRemote',
OpenAssociatedPullRequestOnRemote = 'gitlens.openAssociatedPullRequestOnRemote',
@ -114,7 +116,6 @@ export enum Commands {
ShowCommitsView = 'gitlens.showCommitsView',
ShowContributorsView = 'gitlens.showContributorsView',
ShowFileHistoryView = 'gitlens.showFileHistoryView',
ShowFileHistoryInView = 'gitlens.showFileHistoryInView',
ShowLastQuickPick = 'gitlens.showLastQuickPick',
ShowLineHistoryView = 'gitlens.showLineHistoryView',
ShowQuickBranchHistory = 'gitlens.showQuickBranchHistory',
@ -174,6 +175,7 @@ export enum Commands {
Deprecated_OpenFileInRemote = 'gitlens.openFileInRemote',
Deprecated_OpenInRemote = 'gitlens.openInRemote',
Deprecated_OpenRepoInRemote = 'gitlens.openRepoInRemote',
Deprecated_ShowFileHistoryInView = 'gitlens.showFileHistoryInView',
}
export function executeActionCommand<T extends ActionContext>(action: Action<T>, args: Omit<T, 'type'>) {

+ 12
- 2
src/commands/showQuickFileHistory.ts 查看文件

@ -21,11 +21,21 @@ export interface ShowQuickFileHistoryCommandArgs {
@command()
export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand {
constructor() {
super([Commands.ShowFileHistoryInView, Commands.ShowQuickFileHistory, Commands.QuickOpenFileHistory]);
super([
Commands.OpenFileHistory,
Commands.OpenFolderHistory,
Commands.ShowQuickFileHistory,
Commands.QuickOpenFileHistory,
Commands.Deprecated_ShowFileHistoryInView,
]);
}
protected preExecute(context: CommandContext, args?: ShowQuickFileHistoryCommandArgs) {
if (context.command === Commands.ShowFileHistoryInView) {
if (
context.command === Commands.OpenFileHistory ||
context.command === Commands.OpenFolderHistory ||
context.command === Commands.Deprecated_ShowFileHistoryInView
) {
args = { ...args };
args.showInSideBar = true;
}

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

@ -553,6 +553,7 @@ export interface ContributorsViewConfig {
export interface FileHistoryViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
}
export interface LineHistoryViewConfig {

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

@ -871,7 +871,8 @@ export namespace Git {
if (format !== 'refs') {
if (startLine == null) {
if (format === 'simple') {
// If this is the log of a folder, use `--name-status` to match non-file logs (for parsing)
if (format === 'simple' || isFolderGlob(file)) {
params.push('--name-status');
} else {
params.push('--numstat', '--summary');
@ -1462,3 +1463,11 @@ export namespace Git {
}
}
}
export function isFolderGlob(path: string) {
return paths.basename(path) === '*';
}
export function toFolderGlob(fsPath: string) {
return paths.join(fsPath, '*');
}

+ 20
- 5
src/git/gitService.ts 查看文件

@ -68,6 +68,8 @@ import {
GitTagParser,
GitTree,
GitTreeParser,
GitUser,
isFolderGlob,
maxGitCliLength,
PullRequest,
PullRequestDateFormatting,
@ -142,7 +144,7 @@ export class GitService implements Disposable {
private readonly _stashesCache = new Map<string, GitStash | null>();
private readonly _tagsCache = new Map<string, GitTag[]>();
private readonly _trackedCache = new Map<string, boolean | Promise<boolean>>();
private readonly _userMapCache = new Map<string, { name?: string; email?: string } | null>();
private readonly _userMapCache = new Map<string, GitUser | null>();
constructor() {
this._repositoryTree = TernarySearchTree.forPaths();
@ -1116,8 +1118,7 @@ export class GitService implements Disposable {
startLine: lineToBlame,
endLine: lineToBlame,
});
const currentUser = await this.getCurrentUser(uri.repoPath!);
const blame = GitBlameParser.parse(data, uri.repoPath, fileName, currentUser);
const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!));
if (blame == null) return undefined;
return {
@ -1549,7 +1550,7 @@ export class GitService implements Disposable {
@log()
@gate()
async getCurrentUser(repoPath: string) {
async getCurrentUser(repoPath: string): Promise<GitUser | undefined> {
let user = this._userMapCache.get(repoPath);
if (user != null) return user;
// If we found the repo, but no user data was found just return
@ -2355,7 +2356,8 @@ export class GitService implements Disposable {
});
const log = GitLogParser.parse(
data,
GitCommitType.LogFile,
// If this is the log of a folder, parse it as a normal log rather than a file log
isFolderGlob(file) ? GitCommitType.Log : GitCommitType.LogFile,
root,
file,
ref,
@ -3553,6 +3555,19 @@ export class GitService implements Disposable {
}
@log()
async getStatusForFiles(repoPath: string, path: string): Promise<GitStatusFile[] | undefined> {
const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
const data = await Git.status__file(repoPath, path, porcelainVersion, {
similarityThreshold: Container.config.advanced.similarityThreshold,
});
const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
if (status == null || !status.files.length) return [];
return status.files;
}
@log()
async getStatusForRepo(repoPath: string | undefined): Promise<GitStatus | undefined> {
if (repoPath == null) return undefined;

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

@ -363,3 +363,4 @@ export * from './stashCommit';
export * from './status';
export * from './tag';
export * from './tree';
export * from './user';

+ 2
- 4
src/git/models/status.ts 查看文件

@ -5,7 +5,7 @@ import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitFile, GitFileConflictStatus, GitFileIndexStatus, GitFileStatus, GitFileWorkingTreeStatus } from './file';
import { GitUri } from '../gitUri';
import { GitCommitType, GitLogCommit, GitRemote, GitRevision } from './models';
import { GitCommitType, GitLogCommit, GitRemote, GitRevision, GitUser } from './models';
import { memoize, Strings } from '../../system';
export interface ComputedWorkingTreeGitStatus {
@ -406,11 +406,10 @@ export class GitStatusFile implements GitFile {
return GitFile.getStatusText(this.status);
}
async toPsuedoCommits(): Promise<GitLogCommit[]> {
toPsuedoCommits(user: GitUser | undefined): GitLogCommit[] {
const commits: GitLogCommit[] = [];
if (this.conflictStatus != null) {
const user = await Container.git.getCurrentUser(this.repoPath);
commits.push(
new GitLogCommit(
GitCommitType.LogFile,
@ -434,7 +433,6 @@ export class GitStatusFile implements GitFile {
if (this.workingTreeStatus == null && this.indexStatus == null) return commits;
const user = await Container.git.getCurrentUser(this.repoPath);
if (this.workingTreeStatus != null && this.indexStatus != null) {
commits.push(
new GitLogCommit(

+ 6
- 0
src/git/models/user.ts 查看文件

@ -0,0 +1,6 @@
'use strict';
export interface GitUser {
name: string | undefined;
email: string | undefined;
}

+ 2
- 2
src/git/parsers/blameParser.ts 查看文件

@ -1,6 +1,6 @@
'use strict';
import * as paths from 'path';
import { GitAuthor, GitBlame, GitBlameCommit, GitCommitLine, GitRevision } from '../git';
import { GitAuthor, GitBlame, GitBlameCommit, GitCommitLine, GitRevision, GitUser } from '../git';
import { debug, Strings } from '../../system';
const emptyStr = '';
@ -35,7 +35,7 @@ export class GitBlameParser {
data: string,
repoPath: string | undefined,
fileName: string,
currentUser: { name?: string; email?: string } | undefined,
currentUser: GitUser | undefined,
): GitBlame | undefined {
if (!data) return undefined;

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

@ -10,6 +10,7 @@ import {
GitLogCommit,
GitLogCommitLine,
GitRevision,
GitUser,
} from '../git';
import { Arrays, debug, Strings } from '../../system';
@ -86,7 +87,7 @@ export class GitLogParser {
repoPath: string | undefined,
fileName: string | undefined,
sha: string | undefined,
currentUser: { name?: string; email?: string } | undefined,
currentUser: GitUser | undefined,
limit: number | undefined,
reverse: boolean,
range: Range | undefined,

+ 43
- 24
src/system/array.ts 查看文件

@ -64,37 +64,47 @@ export function filterMapAsync(
export const findLastIndex = _findLastIndex;
export function groupBy<T>(source: T[], accessor: (item: T) => string): Record<string, T[]> {
export function groupBy<T>(source: T[], groupingKey: (item: T) => string): Record<string, T[]> {
return source.reduce((groupings, current) => {
const value = accessor(current);
groupings[value] = groupings[value] ?? [];
groupings[value].push(current);
const value = groupingKey(current);
const group = groupings[value];
if (group === undefined) {
groupings[value] = [current];
} else {
group.push(current);
}
return groupings;
}, Object.create(null) as Record<string, T[]>);
}
export function groupByMap<TKey, TValue>(source: TValue[], accessor: (item: TValue) => TKey): Map<TKey, TValue[]> {
export function groupByMap<TKey, TValue>(source: TValue[], groupingKey: (item: TValue) => TKey): Map<TKey, TValue[]> {
return source.reduce((groupings, current) => {
const value = accessor(current);
const group = groupings.get(value) ?? [];
groupings.set(value, group);
group.push(current);
const value = groupingKey(current);
const group = groupings.get(value);
if (group === undefined) {
groupings.set(value, [current]);
} else {
group.push(current);
}
return groupings;
}, new Map<TKey, TValue[]>());
}
export function groupByFilterMap<TKey, TValue, TMapped>(
source: TValue[],
accessor: (item: TValue) => TKey,
groupingKey: (item: TValue) => TKey,
predicateMapper: (item: TValue) => TMapped | null | undefined,
): Map<TKey, TMapped[]> {
return source.reduce((groupings, current) => {
const mapped = predicateMapper(current);
if (mapped != null) {
const value = accessor(current);
const group = groupings.get(value) ?? [];
groupings.set(value, group);
group.push(mapped);
const value = groupingKey(current);
const group = groupings.get(value);
if (group === undefined) {
groupings.set(value, [mapped]);
} else {
group.push(mapped);
}
}
return groupings;
}, new Map<TKey, TMapped[]>());
@ -204,14 +214,23 @@ export function compactHierarchy(
return root;
}
export function uniqueBy<T>(source: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] {
const uniqueValues = Object.create(null) as Record<string, any>;
return source.filter(item => {
const value = accessor(item);
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (uniqueValues[value]) return false;
uniqueValues[value] = accessor;
return predicate?.(item) ?? true;
});
export function uniqueBy<TKey, TValue>(
source: TValue[],
uniqueKey: (item: TValue) => TKey,
onDeduplicate: (original: TValue, current: TValue) => TValue | void,
) {
const map = source.reduce((uniques, current) => {
const value = uniqueKey(current);
const original = uniques.get(value);
if (original === undefined) {
uniques.set(value, current);
} else {
const updated = onDeduplicate(original, current);
if (updated !== undefined) {
uniques.set(value, updated);
}
}
return uniques;
}, new Map<TKey, TValue>());
return [...map.values()];
}

+ 5
- 0
src/views/fileHistoryView.ts 查看文件

@ -122,12 +122,17 @@ export class FileHistoryView extends ViewBase
}
private setCursorFollowing(enabled: boolean) {
const uri = !this._followEditor && this.root?.hasUri ? this.root.uri : undefined;
this._followCursor = enabled;
void setContext(ContextKeys.ViewsFileHistoryCursorFollowing, enabled);
this.title = this._followCursor ? 'Line History' : 'File History';
const root = this.ensureRoot(true);
if (uri != null) {
root.setUri(uri);
}
root.setEditorFollowing(this._followEditor);
void root.ensureSubscription();
void this.refresh(true);

+ 2
- 1
src/views/nodes/commitFileNode.ts 查看文件

@ -3,12 +3,13 @@ import * as paths from 'path';
import { Command, Selection, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { Container } from '../../container';
import { FileHistoryView } from '../fileHistoryView';
import { GitBranch, GitFile, GitLogCommit, GitRevisionReference, StatusFileFormatter } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { View, ViewsWithCommits } from '../viewBase';
import { ContextValues, ViewNode, ViewRefFileNode } from './viewNode';
export class CommitFileNode<TView extends View = ViewsWithCommits> extends ViewRefFileNode<TView> {
export class CommitFileNode<TView extends View = ViewsWithCommits | FileHistoryView> extends ViewRefFileNode<TView> {
constructor(
view: TView,
parent: ViewNode,

+ 4
- 3
src/views/nodes/commitNode.ts 查看文件

@ -6,6 +6,7 @@ import { CommitFileNode } from './commitFileNode';
import { ViewFilesLayout } from '../../configuration';
import { Colors, GlyphChars } from '../../constants';
import { Container } from '../../container';
import { FileHistoryView } from '../fileHistoryView';
import { FileNode, FolderNode } from './folderNode';
import { CommitFormatter, GitBranch, GitLogCommit, GitRevisionReference } from '../../git/git';
import { PullRequestNode } from './pullRequestNode';
@ -14,9 +15,9 @@ import { TagsView } from '../tagsView';
import { ViewsWithCommits } from '../viewBase';
import { ContextValues, ViewNode, ViewRefNode } from './viewNode';
export class CommitNode extends ViewRefNode<ViewsWithCommits, GitRevisionReference> {
export class CommitNode extends ViewRefNode<ViewsWithCommits | FileHistoryView, GitRevisionReference> {
constructor(
view: ViewsWithCommits,
view: ViewsWithCommits | FileHistoryView,
parent: ViewNode,
public readonly commit: GitLogCommit,
private readonly unpublished?: boolean,
@ -89,7 +90,7 @@ export class CommitNode extends ViewRefNode
);
}
if (!(this.view instanceof TagsView)) {
if (!(this.view instanceof TagsView) && !(this.view instanceof FileHistoryView)) {
if (this.view.config.pullRequests.enabled && this.view.config.pullRequests.showForCommits) {
const pr = await commit.getAssociatedPullRequest();
if (pr != null) {

+ 61
- 17
src/views/nodes/fileHistoryNode.ts 查看文件

@ -1,6 +1,7 @@
'use strict';
import { Disposable, TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { LoadMoreNode, MessageNode } from './common';
import { CommitNode } from './commitNode';
import { configuration } from '../../configuration';
import { Container } from '../../container';
import { FileHistoryTrackerNode } from './fileHistoryTrackerNode';
@ -14,12 +15,13 @@ import {
RepositoryChangeComparisonMode,
RepositoryChangeEvent,
RepositoryFileSystemChangeEvent,
toFolderGlob,
} from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { insertDateMarkers } from './helpers';
import { Logger } from '../../logger';
import { RepositoryNode } from './repositoryNode';
import { debug, gate, Iterables } from '../../system';
import { Arrays, debug, gate, Iterables, memoize } from '../../system';
import { ContextValues, PageableViewNode, SubscribeableViewNode, ViewNode } from './viewNode';
export class FileHistoryNode extends SubscribeableViewNode<FileHistoryView> implements PageableViewNode {
@ -30,7 +32,13 @@ export class FileHistoryNode extends SubscribeableViewNode impl
protected splatted = true;
constructor(uri: GitUri, view: FileHistoryView, parent: ViewNode, private readonly branch: GitBranch | undefined) {
constructor(
uri: GitUri,
view: FileHistoryView,
parent: ViewNode,
private readonly folder: boolean,
private readonly branch: GitBranch | undefined,
) {
super(uri, view, parent);
}
@ -50,9 +58,12 @@ export class FileHistoryNode extends SubscribeableViewNode impl
const children: ViewNode[] = [];
const range = this.branch != null ? await Container.git.getBranchAheadRange(this.branch) : undefined;
const [log, status, unpublishedCommits] = await Promise.all([
const [log, fileStatuses, currentUser, unpublishedCommits] = await Promise.all([
this.getLog(),
this.uri.sha == null ? Container.git.getStatusForFile(this.uri.repoPath!, this.uri.fsPath) : undefined,
this.uri.sha == null
? Container.git.getStatusForFiles(this.uri.repoPath!, this.getPathOrGlob())
: undefined,
this.uri.sha == null ? Container.git.getCurrentUser(this.uri.repoPath!) : undefined,
range
? Container.git.getLogRefsOnly(this.uri.repoPath!, {
limit: 0,
@ -61,23 +72,47 @@ export class FileHistoryNode extends SubscribeableViewNode impl
: undefined,
]);
if (this.uri.sha == null) {
const commits = await status?.toPsuedoCommits();
if (commits?.length) {
children.push(...commits.map(commit => new FileRevisionAsCommitNode(this.view, this, status!, commit)));
if (fileStatuses?.length) {
if (this.folder) {
const commits = Arrays.uniqueBy(
[...Iterables.flatMap(fileStatuses, f => f.toPsuedoCommits(currentUser))],
c => c.sha,
(original, c) => void original.files.push(...c.files),
);
if (commits.length) {
children.push(...commits.map(commit => new CommitNode(this.view, this, commit)));
}
} else {
const [file] = fileStatuses;
const commits = file.toPsuedoCommits(currentUser);
if (commits.length) {
children.push(
...commits.map(commit => new FileRevisionAsCommitNode(this.view, this, file, commit)),
);
}
}
}
if (log != null) {
children.push(
...insertDateMarkers(
Iterables.map(
log.commits.values(),
c =>
new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
branch: this.branch,
unpublished: unpublishedCommits?.has(c.ref),
}),
Iterables.map(log.commits.values(), c =>
this.folder
? new CommitNode(
this.view as any,
this,
c,
unpublishedCommits?.has(c.ref),
this.branch,
undefined,
{
expand: false,
},
)
: new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
branch: this.branch,
unpublished: unpublishedCommits?.has(c.ref),
}),
),
this,
),
@ -161,7 +196,11 @@ export class FileHistoryNode extends SubscribeableViewNode impl
}
private onFileSystemChanged(e: RepositoryFileSystemChangeEvent) {
if (!e.uris.some(uri => uri.toString() === this.uri.toString())) return;
if (this.folder) {
if (!e.uris.some(uri => uri.fsPath.startsWith(this.uri.fsPath))) return;
} else if (!e.uris.some(uri => uri.toString() === this.uri.toString())) {
return;
}
Logger.debug(`FileHistoryNode.onFileSystemChanged(${this.uri.toString(true)}); triggering node refresh`);
@ -179,7 +218,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl
private _log: GitLog | undefined;
private async getLog() {
if (this._log == null) {
this._log = await Container.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, {
this._log = await Container.git.getLogForFile(this.uri.repoPath, this.getPathOrGlob(), {
limit: this.limit ?? this.view.config.pageItemLimit,
ref: this.uri.sha,
});
@ -188,6 +227,11 @@ export class FileHistoryNode extends SubscribeableViewNode impl
return this._log;
}
@memoize()
private getPathOrGlob() {
return this.folder ? toFolderGlob(this.uri.fsPath) : this.uri.fsPath;
}
get hasMore() {
return this._log?.hasMore ?? true;
}

+ 17
- 4
src/views/nodes/fileHistoryTrackerNode.ts 查看文件

@ -1,6 +1,7 @@
'use strict';
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { Disposable, FileType, TextEditor, TreeItem, TreeItemCollapsibleState, window, workspace } from 'vscode';
import { UriComparer } from '../../comparers';
import { ContextKeys, setContext } from '../../constants';
import { Container } from '../../container';
import { FileHistoryView } from '../fileHistoryView';
import { FileHistoryNode } from './fileHistoryNode';
@ -10,7 +11,6 @@ import { Logger } from '../../logger';
import { ReferencePicker } from '../../quickpicks';
import { debug, Functions, gate, log } from '../../system';
import { ContextValues, SubscribeableViewNode, unknownGitUri, ViewNode } from './viewNode';
import { ContextKeys, setContext } from '../../constants';
export class FileHistoryTrackerNode extends SubscribeableViewNode<FileHistoryView> {
private _base: string | undefined;
@ -53,6 +53,16 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode
};
const fileUri = new GitUri(this.uri, commitish);
let folder = false;
try {
const stat = await workspace.fs.stat(this.uri);
if (stat.type === FileType.Directory) {
folder = true;
}
} catch {}
this.view.title = folder ? 'Folder History' : 'File History';
let branch;
if (!commitish.sha || commitish.sha === 'HEAD') {
branch = await Container.git.getBranch(this.uri.repoPath);
@ -61,7 +71,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode
filter: b => b.name === commitish.sha,
});
}
this._child = new FileHistoryNode(fileUri, this.view, this, branch);
this._child = new FileHistoryNode(fileUri, this.view, this, folder, branch);
}
return this._child.getChildren();
@ -188,6 +198,9 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode
}
this.canSubscribe = enabled;
if (!enabled) {
void this.triggerChange();
}
}
@log()
@ -208,7 +221,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode
void this.triggerChange();
}
private setUri(uri?: GitUri) {
setUri(uri?: GitUri) {
this._uri = uri ?? unknownGitUri;
void setContext(ContextKeys.ViewsFileHistoryCanPin, this.hasUri);
}

+ 3
- 2
src/views/nodes/folderNode.ts 查看文件

@ -1,6 +1,7 @@
'use strict';
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ViewFilesLayout, ViewsFilesConfig } from '../../configuration';
import { FileHistoryView } from '../fileHistoryView';
import { GitUri } from '../../git/gitUri';
import { StashesView } from '../stashesView';
import { Arrays } from '../../system';
@ -15,11 +16,11 @@ export interface FileNode extends ViewNode {
root?: Arrays.HierarchicalItem<FileNode>;
}
export class FolderNode extends ViewNode<ViewsWithCommits | StashesView> {
export class FolderNode extends ViewNode<ViewsWithCommits | FileHistoryView | StashesView> {
readonly priority: number = 1;
constructor(
view: ViewsWithCommits | StashesView,
view: ViewsWithCommits | FileHistoryView | StashesView,
parent: ViewNode,
public readonly repoPath: string,
public readonly folderName: string,

+ 18
- 2
src/views/nodes/lineHistoryTrackerNode.ts 查看文件

@ -48,6 +48,22 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode
return [];
}
if (this._selection == null) {
this.view.description = undefined;
this.view.message = 'There was no selection provided for line history.';
this.view.description = `${this.uri.fileName}${
this.uri.sha
? ` ${
this.uri.sha === GitRevision.deletedOrMissing
? this.uri.shortSha
: `(${this.uri.shortSha})`
}`
: ''
}${!this.followingEditor ? ' (pinned)' : ''}`;
return [];
}
this.view.message = undefined;
const commitish: GitCommitish = {
@ -65,7 +81,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode
filter: b => b.name === commitish.sha,
});
}
this._child = new LineHistoryNode(fileUri, this.view, this, branch, this._selection!, this._editorContents);
this._child = new LineHistoryNode(fileUri, this.view, this, branch, this._selection, this._editorContents);
}
return this._child.getChildren();
@ -221,7 +237,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode
void this.triggerChange();
}
private setUri(uri?: GitUri) {
setUri(uri?: GitUri) {
this._uri = uri ?? unknownGitUri;
void setContext(ContextKeys.ViewsFileHistoryCanPin, this.hasUri);
}

+ 16
- 4
src/views/viewCommands.ts 查看文件

@ -581,8 +581,14 @@ export class ViewCommands {
}
@debug()
private async stageFile(node: FileRevisionAsCommitNode | StatusFileNode) {
if (!(node instanceof FileRevisionAsCommitNode) && !(node instanceof StatusFileNode)) return;
private async stageFile(node: CommitFileNode | FileRevisionAsCommitNode | StatusFileNode) {
if (
!(node instanceof CommitFileNode) &&
!(node instanceof FileRevisionAsCommitNode) &&
!(node instanceof StatusFileNode)
) {
return;
}
void (await Container.git.stageFile(node.repoPath, node.file.fileName));
void node.triggerChange();
@ -652,8 +658,14 @@ export class ViewCommands {
}
@debug()
private async unstageFile(node: FileRevisionAsCommitNode | StatusFileNode) {
if (!(node instanceof FileRevisionAsCommitNode) && !(node instanceof StatusFileNode)) return;
private async unstageFile(node: CommitFileNode | FileRevisionAsCommitNode | StatusFileNode) {
if (
!(node instanceof CommitFileNode) &&
!(node instanceof FileRevisionAsCommitNode) &&
!(node instanceof StatusFileNode)
) {
return;
}
void (await Container.git.unStageFile(node.repoPath, node.file.fileName));
void node.triggerChange();

+ 37
- 2
src/webviews/apps/settings/partials/views.file-history.html 查看文件

@ -19,8 +19,8 @@
href="command:gitlens.showFileHistoryView"
>File History view</a
>
to visualize, navigate, and explore the revision history of the current file or just the selected lines of
the current file
to visualize, navigate, and explore the revision history of the current file, a specified file or folder, or
just the selected lines of the current file
</p>
</div>
@ -30,6 +30,41 @@
<div class="settings settings--fixed ml-1">
<div class="setting">
<div class="setting__input">
<label for="views.fileHistory.files.layout"
>Layout files (when showing folder history)</label
>
<div class="select-container">
<select
id="views.fileHistory.files.layout"
name="views.fileHistory.files.layout"
data-setting
>
<option value="auto">automatically</option>
<option value="list">as a list</option>
<option value="tree">as a tree</option>
</select>
</div>
</div>
<p class="setting__hint" data-visibility="views.fileHistory.files.layout =auto">
Chooses the best layout based on the number of files at each nesting level
</p>
</div>
<div class="setting">
<div class="setting__input">
<input
id="views.fileHistory.files.compact"
name="views.fileHistory.files.compact"
type="checkbox"
data-setting
/>
<label for="views.fileHistory.files.compact">Use compact file layout</label>
</div>
<p class="setting__hint">Compacts (flattens) unnecessary nesting when using a tree layouts</p>
</div>
<div class="setting">
<div class="setting__input">
<input
id="views.fileHistory.avatars"
name="views.fileHistory.avatars"

正在加载...
取消
保存