@ -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<GitReflog['formatDate']>(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); | |||
} | |||
} |
@ -0,0 +1,62 @@ | |||
'use strict'; | |||
import { debug } from '../../system'; | |||
import { GitReflog } from '../models/reflog'; | |||
const incomingCommands = ['merge', 'pull']; | |||
const reflogRegex = /^<r>(.+)<d>(?:.+?)@{(.+)}<s>(\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; | |||
} | |||
} |
@ -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<ViewWithFiles> 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<ViewNode[]> { | |||
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; | |||
} | |||
} |