From b5f98f12d894078a31d16bfbc316728c47f39c87 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 13 May 2019 03:10:41 -0400 Subject: [PATCH] Adds experimental "recent incoming changes" - #735 --- src/git/git.ts | 24 ++++++++--- src/git/gitService.ts | 22 +++++++++- src/git/models/models.ts | 1 + src/git/models/reflog.ts | 39 +++++++++++++++++ src/git/parsers/parsers.ts | 1 + src/git/parsers/reflogParser.ts | 62 ++++++++++++++++++++++++++++ src/views/nodes.ts | 1 + src/views/nodes/recentIncomingChangesNode.ts | 61 +++++++++++++++++++++++++++ src/views/nodes/repositoryNode.ts | 14 ++++++- src/views/nodes/viewNode.ts | 1 + 10 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/git/models/reflog.ts create mode 100644 src/git/parsers/reflogParser.ts create mode 100644 src/views/nodes/recentIncomingChangesNode.ts diff --git a/src/git/git.ts b/src/git/git.ts index 6521344..3486a01 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -8,7 +8,7 @@ import { Logger } from '../logger'; import { Objects, Strings } from '../system'; import { findGitPath, GitLocation } from './locator'; import { run, RunOptions } from './shell'; -import { GitBranchParser, GitLogParser, GitStashParser } from './parsers/parsers'; +import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser } from './parsers/parsers'; import { GitFileStatus } from './models/file'; export { GitLocation } from './locator'; @@ -774,10 +774,10 @@ export class Git { return data.length === 0 ? undefined : data.trim(); } - static async ls_tree(repoPath: string, ref: string, options: { fileName?: string } = {}) { + static async ls_tree(repoPath: string, ref: string, { fileName }: { fileName?: string } = {}) { const params = ['ls-tree']; - if (options.fileName) { - params.push('-l', ref, '--', options.fileName); + if (fileName) { + params.push('-l', ref, '--', fileName); } else { params.push('-lrt', ref, '--'); @@ -786,15 +786,27 @@ export class Git { return data.length === 0 ? undefined : data.trim(); } - static merge_base(repoPath: string, ref1: string, ref2: string, options: { forkPoint?: boolean } = {}) { + static merge_base(repoPath: string, ref1: string, ref2: string, { forkPoint }: { forkPoint?: boolean } = {}) { const params = ['merge-base']; - if (options.forkPoint) { + if (forkPoint) { params.push('--fork-point'); } return git({ cwd: repoPath }, ...params, ref1, ref2); } + static reflog(repoPath: string, { branch, since }: { branch?: string; since?: string } = {}): Promise { + const params = ['log', '-g', `--format=${GitReflogParser.defaultFormat}`, '--date=unix']; + if (branch) { + params.push(branch); + } + if (since) { + params.push(`--since=${since}`); + } + + return git({ cwd: repoPath }, ...params, '--'); + } + static remote(repoPath: string): Promise { return git({ cwd: repoPath }, 'remote', '-v'); } diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 49211da..6e2feef 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -51,6 +51,7 @@ import { GitLogCommit, GitLogDiffFilter, GitLogParser, + GitReflog, GitRemote, GitRemoteParser, GitStash, @@ -67,7 +68,7 @@ import { } from './git'; import { GitUri } from './gitUri'; import { RemoteProviderFactory, RemoteProviders } from './remotes/factory'; -import { GitShortLogParser } from './parsers/parsers'; +import { GitReflogParser, GitShortLogParser } from './parsers/parsers'; export * from './gitUri'; export * from './models/models'; @@ -1953,6 +1954,25 @@ export class GitService implements Disposable { } @log() + async getRecentIncomingChanges( + repoPath: string, + options: { branch?: string; since?: string } = {} + ): Promise { + const cc = Logger.getCorrelationContext(); + + try { + const data = await Git.reflog(repoPath, options); + if (data === undefined) return undefined; + + return GitReflogParser.parseRecentIncomingChanges(data, repoPath); + } + catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + @log() async getRemotes(repoPath: string | undefined, options: { includeAll?: boolean } = {}): Promise { if (repoPath === undefined) return []; diff --git a/src/git/models/models.ts b/src/git/models/models.ts index d3dda89..3ffc5b6 100644 --- a/src/git/models/models.ts +++ b/src/git/models/models.ts @@ -16,6 +16,7 @@ export * from './log'; export * from './logCommit'; export * from './remote'; export * from './repository'; +export * from './reflog'; export * from './shortlog'; export * from './stash'; export * from './stashCommit'; diff --git a/src/git/models/reflog.ts b/src/git/models/reflog.ts new file mode 100644 index 0000000..bd7b096 --- /dev/null +++ b/src/git/models/reflog.ts @@ -0,0 +1,39 @@ +'use strict'; +import { Dates, memoize } from '../../system'; +import { CommitFormatting } from '../git'; +import { DateStyle } from '../../config'; + +export class GitReflog { + previousRef: string | undefined; + + constructor( + public readonly repoPath: string, + public readonly ref: string, + public readonly date: Date, + public readonly command: string + ) {} + + @memoize(format => (format == null ? 'MMMM Do, YYYY h:mma' : format)) + formatDate(format?: string | null) { + if (format == null) { + format = 'MMMM Do, YYYY h:mma'; + } + + return this.dateFormatter.format(format); + } + + formatDateFromNow() { + return this.dateFormatter.fromNow(); + } + + get formattedDate(): string { + return CommitFormatting.dateStyle === DateStyle.Absolute + ? this.formatDate(CommitFormatting.dateFormat) + : this.formatDateFromNow(); + } + + @memoize() + private get dateFormatter(): Dates.DateFormatter { + return Dates.getFormatter(this.date); + } +} diff --git a/src/git/parsers/parsers.ts b/src/git/parsers/parsers.ts index 07d0952..8a05da2 100644 --- a/src/git/parsers/parsers.ts +++ b/src/git/parsers/parsers.ts @@ -4,6 +4,7 @@ export * from './blameParser'; export * from './branchParser'; export * from './diffParser'; export * from './logParser'; +export * from './reflogParser'; export * from './remoteParser'; export * from './shortlogParser'; export * from './stashParser'; diff --git a/src/git/parsers/reflogParser.ts b/src/git/parsers/reflogParser.ts new file mode 100644 index 0000000..e57fa6b --- /dev/null +++ b/src/git/parsers/reflogParser.ts @@ -0,0 +1,62 @@ +'use strict'; +import { debug } from '../../system'; +import { GitReflog } from '../models/reflog'; + +const incomingCommands = ['merge', 'pull']; +const reflogRegex = /^(.+)(?:.+?)@{(.+)}(\w*).*$/gm; + +// Using %x00 codes because some shells seem to try to expand things if not +const lb = '%x3c'; // `%x${'<'.charCodeAt(0).toString(16)}`; +const rb = '%x3e'; // `%x${'>'.charCodeAt(0).toString(16)}`; + +export class GitReflogParser { + static defaultFormat = [ + `${lb}r${rb}%H`, // ref + `${lb}d${rb}%gD`, // reflog selector (with UNIX timestamp) + `${lb}s${rb}%gs` // reflog subject + ].join(''); + + @debug({ args: false }) + static parseRecentIncomingChanges(data: string, repoPath: string): GitReflog | undefined { + if (!data) return undefined; + + let reflog: GitReflog | undefined; + + let match: RegExpExecArray | null; + let date; + let ref; + let command; + + do { + match = reflogRegex.exec(data); + if (match == null) break; + + [, ref, date, command] = match; + + // If we don't have a reflog, or are still at the same ref with a proper command, save it + if ( + (reflog === undefined || (reflog !== undefined && ref === reflog.ref)) && + incomingCommands.includes(command) + ) { + reflog = new GitReflog( + repoPath, + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${ref}`.substr(1), + new Date((date! as any) * 1000), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${command}`.substr(1) + ); + } + else if (reflog !== undefined && ref !== reflog.ref) { + reflog.previousRef = ref; + + break; + } + } while (match != null); + + // Ensure the regex state is reset + reflogRegex.lastIndex = 0; + + return reflog; + } +} diff --git a/src/views/nodes.ts b/src/views/nodes.ts index f518d5d..4ee9ff8 100644 --- a/src/views/nodes.ts +++ b/src/views/nodes.ts @@ -13,6 +13,7 @@ export * from './nodes/fileHistoryTrackerNode'; export * from './nodes/folderNode'; export * from './nodes/lineHistoryNode'; export * from './nodes/lineHistoryTrackerNode'; +export * from './nodes/recentIncomingChangesNode'; export * from './nodes/remoteNode'; export * from './nodes/remotesNode'; export * from './nodes/repositoriesNode'; diff --git a/src/views/nodes/recentIncomingChangesNode.ts b/src/views/nodes/recentIncomingChangesNode.ts new file mode 100644 index 0000000..45faea3 --- /dev/null +++ b/src/views/nodes/recentIncomingChangesNode.ts @@ -0,0 +1,61 @@ +'use strict'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { GlyphChars } from '../../constants'; +import { Container } from '../../container'; +import { GitReflog, GitUri } from '../../git/gitService'; +import { Iterables } from '../../system'; +import { ViewWithFiles } from '../viewBase'; +import { CommitNode } from './commitNode'; +import { MessageNode, ShowMoreNode } from './common'; +import { insertDateMarkers } from './helpers'; +import { PageableViewNode, ResourceType, ViewNode } from './viewNode'; + +export class RecentIncomingChangesNode extends ViewNode implements PageableViewNode { + readonly supportsPaging: boolean = true; + maxCount: number | undefined; + + constructor(view: ViewWithFiles, parent: ViewNode, public readonly reflog: GitReflog) { + super(GitUri.fromRepoPath(reflog.repoPath), view, parent); + } + + get id(): string { + return `${this._instanceId}:gitlens:repository(${this.uri.repoPath}):recent-incoming-changes`; + } + + async getChildren(): Promise { + const range = `${this.reflog.previousRef}..${this.reflog.ref}`; + + const log = await Container.git.getLog(this.uri.repoPath!, { + maxCount: this.maxCount !== undefined ? this.maxCount : this.view.config.defaultItemLimit, + ref: range + }); + if (log === undefined) return [new MessageNode(this.view, this, 'No changes')]; + + const children = [ + ...insertDateMarkers(Iterables.map(log.commits.values(), c => new CommitNode(this.view, this, c)), this, 1) + ]; + + if (log.truncated) { + children.push(new ShowMoreNode(this.view, this, 'Commits', children[children.length - 1])); + } + return children; + } + + getTreeItem(): TreeItem { + const item = new TreeItem('Recent incoming changes', TreeItemCollapsibleState.Collapsed); + item.id = this.id; + item.description = `via ${this.reflog.command} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${ + this.reflog.formattedDate + }`; + item.contextValue = ResourceType.RecentIncomingChanges; + item.tooltip = `Recent incoming changes via ${this.reflog.command}\n${this.reflog.formatDate()}`; + + // const iconSuffix = ahead ? 'upload' : 'download'; + // item.iconPath = { + // dark: Container.context.asAbsolutePath(`images/dark/icon-${iconSuffix}.svg`), + // light: Container.context.asAbsolutePath(`images/light/icon-${iconSuffix}.svg`) + // }; + + return item; + } +} diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index 1514cca..d2e2ff9 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -17,12 +17,13 @@ import { BranchesNode } from './branchesNode'; import { BranchNode } from './branchNode'; import { BranchTrackingStatusNode } from './branchTrackingStatusNode'; import { MessageNode } from './common'; +import { ContributorsNode } from './contributorsNode'; +import { RecentIncomingChangesNode } from './recentIncomingChangesNode'; import { RemotesNode } from './remotesNode'; import { StashesNode } from './stashesNode'; import { StatusFilesNode } from './statusFilesNode'; import { TagsNode } from './tagsNode'; import { ResourceType, SubscribeableViewNode, ViewNode } from './viewNode'; -import { ContributorsNode } from './contributorsNode'; const hasTimeRegex = /[hHm]/; @@ -46,6 +47,17 @@ export class RepositoryNode extends SubscribeableViewNode { const children = []; const status = await this._status; + + if (Container.config.insiders) { + const reflog = await Container.git.getRecentIncomingChanges(this.uri.repoPath!, { + // branch: status !== undefined ? status.branch : undefined, + since: '1 month ago' + }); + if (reflog !== undefined) { + children.push(new RecentIncomingChangesNode(this.view, this, reflog)); + } + } + if (status !== undefined) { const branch = new GitBranch( status.repoPath, diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index b62b48b..7fa09cf 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -27,6 +27,7 @@ export enum ResourceType { LineHistory = 'gitlens:history:line', Message = 'gitlens:message', Pager = 'gitlens:pager', + RecentIncomingChanges = 'gitlens:recent-incoming-changes', Remote = 'gitlens:remote', Remotes = 'gitlens:remotes', Repositories = 'gitlens:repositories',