From 03d2b74fc97435915ecb1df8f717baf4a5f0bbe0 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 26 Jan 2022 11:15:06 -0500 Subject: [PATCH] Centralizes uris into the provider model Reworks revision uris to use a packed authority for better stability --- package.json | 2 +- src/commands/browseRepoAtRevision.ts | 3 +- src/commands/diffWith.ts | 11 +- src/commands/diffWithRevisionFrom.ts | 2 +- src/commands/gitCommands.actions.ts | 6 +- src/commands/openFileAtRevision.ts | 6 +- src/commands/openFileAtRevisionFrom.ts | 4 +- src/commands/openRevisionFile.ts | 10 +- src/env/node/git/localGitProvider.ts | 299 +++++++++++++-------- src/git/formatters/commitFormatter.ts | 8 +- src/git/fsProvider.ts | 4 - src/git/gitProvider.ts | 13 +- src/git/gitProviderService.ts | 101 +++++-- src/git/gitUri.ts | 166 ++++-------- src/git/models/commit.ts | 10 +- src/git/models/logCommit.ts | 3 +- src/git/models/repository.ts | 47 ++-- src/git/models/status.ts | 16 +- src/system/path.ts | 10 +- src/trackers/documentTracker.ts | 1 + src/trackers/trackedDocument.ts | 6 +- src/views/nodes/lineHistoryTrackerNode.ts | 6 +- src/views/nodes/mergeConflictCurrentChangesNode.ts | 2 +- src/views/nodes/mergeConflictFileNode.ts | 4 +- .../nodes/mergeConflictIncomingChangesNode.ts | 8 +- src/views/nodes/statusFileNode.ts | 4 +- src/views/viewCommands.ts | 6 +- src/vsls/host.ts | 12 +- 28 files changed, 438 insertions(+), 332 deletions(-) diff --git a/package.json b/package.json index 22aac9f..de1a888 100644 --- a/package.json +++ b/package.json @@ -9965,7 +9965,7 @@ "scheme": "gitlens", "authority": "*", "formatting": { - "label": "${path} (${authority})", + "label": "${path} (${query.ref})", "separator": "/", "workspaceSuffix": "GitLens", "stripPathStartingSeparator": true diff --git a/src/commands/browseRepoAtRevision.ts b/src/commands/browseRepoAtRevision.ts index 8d8087f..e6811eb 100644 --- a/src/commands/browseRepoAtRevision.ts +++ b/src/commands/browseRepoAtRevision.ts @@ -2,7 +2,6 @@ import { commands, TextEditor, Uri } from 'vscode'; import { BuiltInCommands } from '../constants'; import type { Container } from '../container'; -import { toGitLensFSUri } from '../git/fsProvider'; import { GitUri } from '../git/gitUri'; import { Logger } from '../logger'; import { Messages } from '../messages'; @@ -68,7 +67,7 @@ export class BrowseRepoAtRevisionCommand extends ActiveEditorCommand { const sha = args?.before ? await this.container.git.resolveReference(gitUri.repoPath!, `${gitUri.sha}^`) : gitUri.sha; - uri = toGitLensFSUri(sha, gitUri.repoPath!); + uri = this.container.git.getRevisionUri(sha, gitUri.repoPath!, gitUri.repoPath!); gitUri = GitUri.fromRevisionUri(uri); openWorkspace(uri, { diff --git a/src/commands/diffWith.ts b/src/commands/diffWith.ts index 6879822..56af5fc 100644 --- a/src/commands/diffWith.ts +++ b/src/commands/diffWith.ts @@ -2,7 +2,6 @@ import { commands, Range, TextDocumentShowOptions, Uri, ViewColumn } from 'vscode'; import { BuiltInCommands, GlyphChars } from '../constants'; import type { Container } from '../container'; -import { GitUri } from '../git/gitUri'; import { GitCommit, GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; @@ -119,8 +118,8 @@ export class DiffWithCommand extends Command { } const [lhs, rhs] = await Promise.all([ - this.container.git.getVersionedUri(args.repoPath, args.lhs.uri.fsPath, args.lhs.sha), - this.container.git.getVersionedUri(args.repoPath, args.rhs.uri.fsPath, args.rhs.sha), + this.container.git.getBestRevisionUri(args.repoPath, args.lhs.uri.fsPath, args.lhs.sha), + this.container.git.getBestRevisionUri(args.repoPath, args.rhs.uri.fsPath, args.rhs.sha), ]); let rhsSuffix = GitRevision.shorten(rhsSha, { strings: { uncommitted: 'Working Tree' } }); @@ -172,8 +171,10 @@ export class DiffWithCommand extends Command { void (await commands.executeCommand( BuiltInCommands.Diff, - lhs ?? GitUri.toRevisionUri(GitRevision.deletedOrMissing, args.lhs.uri.fsPath, args.repoPath), - rhs ?? GitUri.toRevisionUri(GitRevision.deletedOrMissing, args.rhs.uri.fsPath, args.repoPath), + lhs ?? + this.container.git.getRevisionUri(GitRevision.deletedOrMissing, args.lhs.uri.fsPath, args.repoPath), + rhs ?? + this.container.git.getRevisionUri(GitRevision.deletedOrMissing, args.rhs.uri.fsPath, args.repoPath), title, args.showOptions, )); diff --git a/src/commands/diffWithRevisionFrom.ts b/src/commands/diffWithRevisionFrom.ts index 16836ae..0e7090f 100644 --- a/src/commands/diffWithRevisionFrom.ts +++ b/src/commands/diffWithRevisionFrom.ts @@ -86,7 +86,7 @@ export class DiffWithRevisionFromCommand extends ActiveEditorCommand { const fileName = normalizePath(relative(gitUri.repoPath, gitUri.fsPath)); const rename = files.find(s => s.fileName === fileName); if (rename?.originalFileName != null) { - renamedUri = GitUri.resolve(rename.originalFileName, gitUri.repoPath); + renamedUri = this.container.git.getAbsoluteUri(rename.originalFileName, gitUri.repoPath); renamedTitle = `${basename(rename.originalFileName)} (${GitRevision.shorten(ref)})`; } } diff --git a/src/commands/gitCommands.actions.ts b/src/commands/gitCommands.actions.ts index bc83414..83cb110 100644 --- a/src/commands/gitCommands.actions.ts +++ b/src/commands/gitCommands.actions.ts @@ -531,7 +531,7 @@ export namespace GitActions { file = fileOrRevisionUri; } - uri = GitUri.toRevisionUri( + uri = Container.instance.git.getRevisionUri( file.status === 'D' ? commit.previousFileSha : commit.sha, file, commit.repoPath, @@ -624,7 +624,9 @@ export namespace GitActions { } findOrOpenEditors( - files.map(file => GitUri.toRevisionUri(file.status === 'D' ? ref2! : ref1!, file, repoPath!)), + files.map(file => + Container.instance.git.getRevisionUri(file.status === 'D' ? ref2! : ref1!, file, repoPath!), + ), ); } diff --git a/src/commands/openFileAtRevision.ts b/src/commands/openFileAtRevision.ts index 4008970..6bff0c1 100644 --- a/src/commands/openFileAtRevision.ts +++ b/src/commands/openFileAtRevision.ts @@ -67,13 +67,15 @@ export class OpenFileAtRevisionCommand extends ActiveEditorCommand { gitUri.sha, ); if (diffUris?.previous != null) { - args.revisionUri = GitUri.toRevisionUri(diffUris.previous); + args.revisionUri = this.container.git.getRevisionUri(diffUris.previous); } else { void Messages.showCommitHasNoPreviousCommitWarningMessage(blame.commit); return undefined; } } else if (blame?.commit.previousSha != null) { - args.revisionUri = GitUri.toRevisionUri(GitUri.fromCommit(blame.commit, true)); + args.revisionUri = this.container.git.getRevisionUri( + GitUri.fromCommit(blame.commit, true), + ); } else { void Messages.showCommitHasNoPreviousCommitWarningMessage(blame.commit); return undefined; diff --git a/src/commands/openFileAtRevisionFrom.ts b/src/commands/openFileAtRevisionFrom.ts index 7bd5c36..95c2683 100644 --- a/src/commands/openFileAtRevisionFrom.ts +++ b/src/commands/openFileAtRevisionFrom.ts @@ -69,7 +69,7 @@ export class OpenFileAtRevisionFromCommand extends ActiveEditorCommand { const [item] = quickpick.activeItems; if (item != null) { void (await GitActions.Commit.openFileAtRevision( - GitUri.toRevisionUri(item.ref, gitUri.fsPath, gitUri.repoPath!), + this.container.git.getRevisionUri(item.ref, gitUri.fsPath, gitUri.repoPath!), { annotationType: args!.annotationType, line: args!.line, @@ -88,7 +88,7 @@ export class OpenFileAtRevisionFromCommand extends ActiveEditorCommand { } void (await GitActions.Commit.openFileAtRevision( - GitUri.toRevisionUri(args.reference.ref, gitUri.fsPath, gitUri.repoPath), + this.container.git.getRevisionUri(args.reference.ref, gitUri.fsPath, gitUri.repoPath), { annotationType: args.annotationType, line: args.line, diff --git a/src/commands/openRevisionFile.ts b/src/commands/openRevisionFile.ts index da8a921..ad2334a 100644 --- a/src/commands/openRevisionFile.ts +++ b/src/commands/openRevisionFile.ts @@ -40,10 +40,14 @@ export class OpenRevisionFileCommand extends ActiveEditorCommand { args.revisionUri = commit != null && commit.status === 'D' - ? GitUri.toRevisionUri(commit.previousSha!, commit.previousUri.fsPath, commit.repoPath) - : GitUri.toRevisionUri(gitUri); + ? this.container.git.getRevisionUri( + commit.previousSha!, + commit.previousUri.fsPath, + commit.repoPath, + ) + : this.container.git.getRevisionUri(gitUri); } else { - args.revisionUri = GitUri.toRevisionUri(gitUri); + args.revisionUri = this.container.git.getRevisionUri(gitUri); } } diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 7cb816b..a9addd3 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -1,7 +1,7 @@ 'use strict'; import { readdir, realpath } from 'fs'; import { hostname, userInfo } from 'os'; -import { dirname, relative, resolve as resolvePath } from 'path'; +import { resolve as resolvePath } from 'path'; import { Disposable, env, @@ -35,10 +35,11 @@ import { RepositoryCloseEvent, RepositoryInitWatcher, RepositoryOpenEvent, + RevisionUriData, ScmRepository, } from '../../../git/gitProvider'; -import { GitProviderService } from '../../../git/gitProviderService'; -import { GitUri } from '../../../git/gitUri'; +import { GitProviderService, isUriRegex } from '../../../git/gitProviderService'; +import { encodeGitLensRevisionUriAuthority, GitUri } from '../../../git/gitUri'; import { BranchSortOptions, GitAuthor, @@ -94,9 +95,10 @@ import { SearchPattern } from '../../../git/search'; import { LogCorrelationContext, Logger } from '../../../logger'; import { Messages } from '../../../messages'; import { Arrays, debug, Functions, gate, Iterables, log, Strings, Versions } from '../../../system'; -import { isFolderGlob, normalizePath, splitPath } from '../../../system/path'; +import { filterMap } from '../../../system/array'; +import { dirname, isAbsolute, isFolderGlob, normalizePath, relative, splitPath } from '../../../system/path'; import { any, PromiseOrValue } from '../../../system/promise'; -import { equalsIgnoreCase } from '../../../system/string'; +import { CharCode, equalsIgnoreCase } from '../../../system/string'; import { PathTrie } from '../../../system/trie'; import { CachedBlame, @@ -490,6 +492,157 @@ export class LocalGitProvider implements GitProvider, Disposable { }); } + getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri { + // Convert the base to a Uri if it isn't one + if (typeof base === 'string') { + // If it looks like a Uri parse it + if (isUriRegex.test(base)) { + base = Uri.parse(base); + } else { + if (!isAbsolute(base)) { + debugger; + throw new Error(`Base path '${base}' must be an absolute path`); + } + + base = Uri.file(base); + } + } + + // Short-circuit if the path is relative + if (typeof pathOrUri === 'string' && !isAbsolute(pathOrUri) && !isUriRegex.test(pathOrUri)) { + return Uri.joinPath(base, pathOrUri); + } + + const relativePath = this.getRelativePath(pathOrUri, base); + + const uri = Uri.joinPath(base, relativePath); + // TODO@eamodio We need to move live share support to a separate provider + if (this.container.vsls.isMaybeGuest) { + return uri.with({ scheme: DocumentSchemes.Vsls }); + } + return uri; + } + + @log() + async getBestRevisionUri(repoPath: string, path: string, ref: string | undefined): Promise { + if (ref === GitRevision.deletedOrMissing) return undefined; + + // TODO@eamodio Align this with isTrackedCore? + if (!ref || (GitRevision.isUncommitted(ref) && !GitRevision.isUncommittedStaged(ref))) { + // Make sure the file exists in the repo + let data = await Git.ls_files(repoPath, path); + if (data != null) return this.getAbsoluteUri(path, repoPath); + + // Check if the file exists untracked + data = await Git.ls_files(repoPath, path, { untracked: true }); + if (data != null) return this.getAbsoluteUri(path, repoPath); + + return undefined; + } + + if (GitRevision.isUncommittedStaged(ref)) return GitUri.git(path, repoPath); + + return this.getRevisionUri(repoPath, path, ref); + } + + getRelativePath(pathOrUri: string | Uri, base: string | Uri): string { + // Convert the base to a Uri if it isn't one + if (typeof base === 'string') { + // If it looks like a Uri parse it + if (isUriRegex.test(base)) { + base = Uri.parse(base); + } else { + if (!isAbsolute(base)) { + debugger; + throw new Error(`Base path '${base}' must be an absolute path`); + } + + base = Uri.file(base); + } + } + + // Convert the path to a Uri if it isn't one + if (typeof pathOrUri === 'string') { + if (isUriRegex.test(pathOrUri)) { + pathOrUri = Uri.parse(pathOrUri); + } else { + if (!isAbsolute(pathOrUri)) return normalizePath(pathOrUri); + + pathOrUri = Uri.file(pathOrUri); + } + } + + const relativePath = relative(base.fsPath, pathOrUri.fsPath); + return normalizePath(relativePath); + } + + getRevisionUri(repoPath: string, path: string, ref: string): Uri { + if (GitRevision.isUncommitted(ref)) { + return GitRevision.isUncommittedStaged(ref) + ? GitUri.git(path, repoPath) + : this.getAbsoluteUri(path, repoPath); + } + + path = normalizePath(this.getAbsoluteUri(path, repoPath).fsPath); + if (path.charCodeAt(0) !== CharCode.Slash) { + path = `/${path}`; + } + + const metadata: RevisionUriData = { + ref: ref, + repoPath: normalizePath(repoPath), + }; + + const uri = Uri.from({ + scheme: DocumentSchemes.GitLens, + authority: encodeGitLensRevisionUriAuthority(metadata), + path: path, + query: ref ? JSON.stringify({ ref: GitRevision.shorten(ref) }) : undefined, + }); + return uri; + } + + @log() + async getWorkingUri(repoPath: string, uri: Uri) { + let fileName = GitUri.relativeTo(uri, repoPath); + + let data; + let ref; + do { + data = await Git.ls_files(repoPath, fileName); + if (data != null) { + fileName = Strings.splitSingle(data, '\n')[0]; + break; + } + + // TODO: Add caching + // Get the most recent commit for this file name + ref = await Git.log__file_recent(repoPath, fileName, { + ordering: this.container.config.advanced.commitOrdering, + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + if (ref == null) return undefined; + + // Now check if that commit had any renames + data = await Git.log__file(repoPath, '.', ref, { + filters: ['R', 'C', 'D'], + format: 'simple', + limit: 1, + ordering: this.container.config.advanced.commitOrdering, + }); + if (data == null || data.length === 0) break; + + const [foundRef, foundFile, foundStatus] = GitLogParser.parseSimpleRenamed(data, fileName); + if (foundStatus === 'D' && foundFile != null) return undefined; + if (foundRef == null || foundFile == null) break; + + fileName = foundFile; + } while (true); + + uri = this.getAbsoluteUri(fileName, repoPath); + return (await fsExists(uri.fsPath)) ? uri : undefined; + } + @log() async addRemote(repoPath: string, name: string, url: string): Promise { await Git.remote__add(repoPath, name, url); @@ -1298,10 +1451,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const data = await Git.branch__containsOrPointsAt(repoPath, ref, options); if (!data) return []; - return data - .split('\n') - .map(b => b.trim()) - .filter((i?: T): i is T => Boolean(i)); + return filterMap(data.split('\n'), b => b.trim() || undefined); } @log() @@ -2580,12 +2730,12 @@ export class LocalGitProvider implements GitProvider, Disposable { ): Promise<{ current: GitUri; previous: GitUri | undefined } | undefined> { if (ref === GitRevision.deletedOrMissing) return undefined; - const fileName = GitUri.relativeTo(uri, repoPath); + const path = this.getRelativePath(uri, repoPath); // If we are at the working tree (i.e. no ref), we need to dig deeper to figure out where to go - if (ref == null || ref.length === 0) { + if (!ref) { // First, check the file status to see if there is anything staged - const status = await this.getStatusForFile(repoPath, fileName); + const status = await this.getStatusForFile(repoPath, path); if (status != null) { // If the file is staged with working changes, diff working with staged (index) // If the file is staged without working changes, diff staged with HEAD @@ -2598,20 +2748,20 @@ export class LocalGitProvider implements GitProvider, Disposable { if (skip === 0) { // Diff working with staged return { - current: GitUri.fromFile(fileName, repoPath, undefined), - previous: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), + current: GitUri.fromFile(path, repoPath, undefined), + previous: GitUri.fromFile(path, repoPath, GitRevision.uncommittedStaged), }; } return { // Diff staged with HEAD (or prior if more skips) - current: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), + current: GitUri.fromFile(path, repoPath, GitRevision.uncommittedStaged), previous: await this.getPreviousUri(repoPath, uri, ref, skip - 1, undefined, firstParent), }; } else if (status.workingTreeStatus != null) { if (skip === 0) { return { - current: GitUri.fromFile(fileName, repoPath, undefined), + current: GitUri.fromFile(path, repoPath, undefined), previous: await this.getPreviousUri(repoPath, uri, undefined, skip, undefined, firstParent), }; } @@ -2624,7 +2774,7 @@ export class LocalGitProvider implements GitProvider, Disposable { else if (GitRevision.isUncommittedStaged(ref)) { const current = skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) + ? GitUri.fromFile(path, repoPath, ref) : (await this.getPreviousUri(repoPath, uri, undefined, skip - 1, undefined, firstParent))!; if (current == null || current.sha === GitRevision.deletedOrMissing) return undefined; @@ -2637,7 +2787,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // If we are at a commit, diff commit with previous const current = skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) + ? GitUri.fromFile(path, repoPath, ref) : (await this.getPreviousUri(repoPath, uri, ref, skip - 1, undefined, firstParent))!; if (current == null || current.sha === GitRevision.deletedOrMissing) return undefined; @@ -2657,12 +2807,12 @@ export class LocalGitProvider implements GitProvider, Disposable { ): Promise<{ current: GitUri; previous: GitUri | undefined; line: number } | undefined> { if (ref === GitRevision.deletedOrMissing) return undefined; - let fileName = GitUri.relativeTo(uri, repoPath); + let path = this.getRelativePath(uri, repoPath); let previous; // If we are at the working tree (i.e. no ref), we need to dig deeper to figure out where to go - if (ref == null || ref.length === 0) { + if (!ref) { // First, check the blame on the current line to see if there are any working/staged changes const gitUri = new GitUri(uri, repoPath); @@ -2677,15 +2827,15 @@ export class LocalGitProvider implements GitProvider, Disposable { // If the document is dirty (unsaved), use the status to determine where to go if (document.isDirty) { // Check the file status to see if there is anything staged - const status = await this.getStatusForFile(repoPath, fileName); + const status = await this.getStatusForFile(repoPath, path); if (status != null) { // If the file is staged, diff working with staged (index) // If the file is not staged, diff working with HEAD if (status.indexStatus != null) { // Diff working with staged return { - current: GitUri.fromFile(fileName, repoPath, undefined), - previous: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), + current: GitUri.fromFile(path, repoPath, undefined), + previous: GitUri.fromFile(path, repoPath, GitRevision.uncommittedStaged), line: editorLine, }; } @@ -2693,7 +2843,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // Diff working with HEAD (or prior if more skips) return { - current: GitUri.fromFile(fileName, repoPath, undefined), + current: GitUri.fromFile(path, repoPath, undefined), previous: await this.getPreviousUri(repoPath, uri, undefined, skip, editorLine), line: editorLine, }; @@ -2715,19 +2865,19 @@ export class LocalGitProvider implements GitProvider, Disposable { // If line is committed, diff with line ref with previous else { ref = blameLine.commit.sha; - fileName = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? fileName); - uri = GitUri.resolve(fileName, repoPath); + path = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? path); + uri = this.getAbsoluteUri(path, repoPath); editorLine = blameLine.line.originalLine - 1; if (skip === 0 && blameLine.commit.previousSha) { - previous = GitUri.fromFile(fileName, repoPath, blameLine.commit.previousSha); + previous = GitUri.fromFile(path, repoPath, blameLine.commit.previousSha); } } } else { if (GitRevision.isUncommittedStaged(ref)) { const current = skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) + ? GitUri.fromFile(path, repoPath, ref) : (await this.getPreviousUri(repoPath, uri, undefined, skip - 1, editorLine))!; if (current.sha === GitRevision.deletedOrMissing) return undefined; @@ -2744,18 +2894,18 @@ export class LocalGitProvider implements GitProvider, Disposable { // Diff with line ref with previous ref = blameLine.commit.sha; - fileName = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? fileName); - uri = GitUri.resolve(fileName, repoPath); + path = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? path); + uri = this.getAbsoluteUri(path, repoPath); editorLine = blameLine.line.originalLine - 1; if (skip === 0 && blameLine.commit.previousSha) { - previous = GitUri.fromFile(fileName, repoPath, blameLine.commit.previousSha); + previous = GitUri.fromFile(path, repoPath, blameLine.commit.previousSha); } } const current = skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) + ? GitUri.fromFile(path, repoPath, ref) : (await this.getPreviousUri(repoPath, uri, ref, skip - 1, editorLine))!; if (current.sha === GitRevision.deletedOrMissing) return undefined; @@ -2783,11 +2933,12 @@ export class LocalGitProvider implements GitProvider, Disposable { ref = undefined; } - const fileName = GitUri.relativeTo(uri, repoPath); + const path = this.getRelativePath(uri, repoPath); + // TODO: Add caching let data; try { - data = await Git.log__file(repoPath, fileName, ref, { + data = await Git.log__file(repoPath, path, ref, { firstParent: firstParent, format: 'simple', limit: skip + 2, @@ -2799,16 +2950,16 @@ export class LocalGitProvider implements GitProvider, Disposable { // If the line count is invalid just fallback to the most recent commit if ((ref == null || GitRevision.isUncommittedStaged(ref)) && GitErrors.invalidLineCount.test(msg)) { if (ref == null) { - const status = await this.getStatusForFile(repoPath, fileName); + const status = await this.getStatusForFile(repoPath, path); if (status?.indexStatus != null) { - return GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged); + return GitUri.fromFile(path, repoPath, GitRevision.uncommittedStaged); } } - ref = await Git.log__file_recent(repoPath, fileName, { + ref = await Git.log__file_recent(repoPath, path, { ordering: this.container.config.advanced.commitOrdering, }); - return GitUri.fromFile(fileName, repoPath, ref ?? GitRevision.deletedOrMissing); + return GitUri.fromFile(path, repoPath, ref ?? GitRevision.deletedOrMissing); } Logger.error(ex, cc); @@ -2820,7 +2971,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // If the previous ref matches the ref we asked for assume we are at the end of the history if (ref != null && ref === previousRef) return undefined; - return GitUri.fromFile(file ?? fileName, repoPath, previousRef ?? GitRevision.deletedOrMissing); + return GitUri.fromFile(file ?? path, repoPath, previousRef ?? GitRevision.deletedOrMissing); } @log() @@ -3148,74 +3299,6 @@ export class LocalGitProvider implements GitProvider, Disposable { return GitTreeParser.parse(data) ?? []; } - @log() - async getVersionedUri(repoPath: string, fileName: string, ref: string | undefined): Promise { - if (ref === GitRevision.deletedOrMissing) return undefined; - - if ( - ref == null || - ref.length === 0 || - (GitRevision.isUncommitted(ref) && !GitRevision.isUncommittedStaged(ref)) - ) { - // Make sure the file exists in the repo - let data = await Git.ls_files(repoPath, fileName); - if (data != null) return GitUri.file(fileName); - - // Check if the file exists untracked - data = await Git.ls_files(repoPath, fileName, { untracked: true }); - if (data != null) return GitUri.file(fileName); - - return undefined; - } - - if (GitRevision.isUncommittedStaged(ref)) { - return GitUri.git(fileName, repoPath); - } - - return GitUri.toRevisionUri(ref, fileName, repoPath); - } - - @log() - async getWorkingUri(repoPath: string, uri: Uri) { - let fileName = GitUri.relativeTo(uri, repoPath); - - let data; - let ref; - do { - data = await Git.ls_files(repoPath, fileName); - if (data != null) { - fileName = Strings.splitSingle(data, '\n')[0]; - break; - } - - // TODO: Add caching - // Get the most recent commit for this file name - ref = await Git.log__file_recent(repoPath, fileName, { - ordering: this.container.config.advanced.commitOrdering, - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - if (ref == null) return undefined; - - // Now check if that commit had any renames - data = await Git.log__file(repoPath, '.', ref, { - filters: ['R', 'C', 'D'], - format: 'simple', - limit: 1, - ordering: this.container.config.advanced.commitOrdering, - }); - if (data == null || data.length === 0) break; - - const [foundRef, foundFile, foundStatus] = GitLogParser.parseSimpleRenamed(data, fileName); - if (foundStatus === 'D' && foundFile != null) return undefined; - if (foundRef == null || foundFile == null) break; - - fileName = foundFile; - } while (true); - - uri = GitUri.resolve(fileName, repoPath); - return (await fsExists(uri.fsPath)) ? uri : undefined; - } - @log({ args: { 1: false } }) async hasBranchOrTag( repoPath: string | undefined, @@ -3467,7 +3550,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return (await Git.rev_parse__verify(repoPath, ref)) ?? ref; } - const path = typeof pathOrUri === 'string' ? pathOrUri : normalizePath(relative(repoPath, pathOrUri.fsPath)); + const path = normalizePath(this.getRelativePath(pathOrUri, repoPath)); const blob = await Git.rev_parse__verify(repoPath, ref, path); if (blob == null) return GitRevision.deletedOrMissing; diff --git a/src/git/formatters/commitFormatter.ts b/src/git/formatters/commitFormatter.ts index b96f70a..1c762ef 100644 --- a/src/git/formatters/commitFormatter.ts +++ b/src/git/formatters/commitFormatter.ts @@ -19,7 +19,7 @@ import { emojify } from '../../emojis'; import { Iterables, Strings } from '../../system'; import { PromiseCancelledError } from '../../system/promise'; import { ContactPresence } from '../../vsls/vsls'; -import { GitUri } from '../gitUri'; +import type { GitUri } from '../gitUri'; import { GitCommit, GitLogCommit, GitRemote, GitRevision, IssueOrPullRequest, PullRequest } from '../models'; import { RemoteProvider } from '../remotes/provider'; import { FormatOptions, Formatter } from './formatter'; @@ -297,7 +297,7 @@ export class CommitFormatter extends Formatter { })} "Open Changes with Previous Revision")`; commands += `   [$(versions)](${OpenFileAtRevisionCommand.getMarkdownCommandArgs( - GitUri.toRevisionUri(diffUris.previous), + Container.instance.git.getRevisionUri(diffUris.previous), FileAnnotationType.Blame, this._options.editor?.line, )} "Open Blame Prior to this Change")`; @@ -325,7 +325,7 @@ export class CommitFormatter extends Formatter { )} "Open Changes with Previous Revision")`; if (this._item.previousSha != null) { - const uri = GitUri.toRevisionUri( + const uri = Container.instance.git.getRevisionUri( this._item.previousSha, this._item.previousUri.fsPath, this._item.repoPath, @@ -394,7 +394,7 @@ export class CommitFormatter extends Formatter { } commands += `${separator}[$(ellipsis)](${ShowQuickCommitFileCommand.getMarkdownCommandArgs({ - revisionUri: GitUri.toRevisionUri(this._item.toGitUri()).toString(true), + revisionUri: Container.instance.git.getRevisionUri(this._item.toGitUri()).toString(true), })} "Show More Actions")`; return this._padOrTruncate(commands, this._options.tokenOptions.commands); diff --git a/src/git/fsProvider.ts b/src/git/fsProvider.ts index e4488d3..cd70a6a 100644 --- a/src/git/fsProvider.ts +++ b/src/git/fsProvider.ts @@ -26,10 +26,6 @@ export function fromGitLensFSUri(uri: Uri): { path: string; ref: string; repoPat return { path: gitUri.relativePath, ref: gitUri.sha!, repoPath: gitUri.repoPath! }; } -export function toGitLensFSUri(ref: string, repoPath: string): Uri { - return GitUri.toRevisionUri(ref, repoPath, repoPath); -} - export class GitFileSystemProvider implements FileSystemProvider, Disposable { private readonly _disposable: Disposable; private readonly _searchTreeMap = new Map>>(); diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 2c9d98a..bb1a58a 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -87,6 +87,12 @@ export interface GitProvider extends Disposable { getOpenScmRepositories(): Promise; getOrOpenScmRepository(repoPath: string): Promise; + getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri; + getBestRevisionUri(repoPath: string, path: string, ref: string | undefined): Promise; + getRelativePath(pathOrUri: string | Uri, base: string | Uri): string; + getRevisionUri(repoPath: string, path: string, ref: string): Uri; + getWorkingUri(repoPath: string, uri: Uri): Promise; + addRemote(repoPath: string, name: string, url: string): Promise; pruneRemote(repoPath: string, remoteName: string): Promise; applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise; @@ -336,8 +342,6 @@ export interface GitProvider extends Disposable { ): Promise>; getTreeEntryForRevision(repoPath: string, path: string, ref: string): Promise; getTreeForRevision(repoPath: string, ref: string): Promise; - getVersionedUri(repoPath: string, fileName: string, ref: string | undefined): Promise; - getWorkingUri(repoPath: string, uri: Uri): Promise; hasBranchOrTag( repoPath: string | undefined, @@ -395,3 +399,8 @@ export interface GitProvider extends Disposable { options?: { includeUntracked?: boolean | undefined; keepIndex?: boolean | undefined }, ): Promise; } + +export interface RevisionUriData { + ref?: string; + repoPath: string; +} diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index be76d4b..84067d5 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -34,7 +34,7 @@ import { groupByFilterMap, groupByMap } from '../system/array'; import { gate } from '../system/decorators/gate'; import { debug, log } from '../system/decorators/log'; import { count, filter, first, flatMap, map } from '../system/iterable'; -import { basename, dirname, normalizePath } from '../system/path'; +import { basename, dirname, isAbsolute, normalizePath } from '../system/path'; import { cancellable, isPromise, PromiseCancelledError } from '../system/promise'; import { CharCode } from '../system/string'; import { VisitedPathsTrie } from '../system/trie'; @@ -83,6 +83,8 @@ import { RemoteProviders } from './remotes/factory'; import { Authentication, RemoteProvider, RichRemoteProvider } from './remotes/provider'; import { SearchPattern } from './search'; +export const isUriRegex = /^(\w[\w\d+.-]{1,}?):\/\//; + const maxDefaultBranchWeight = 100; const weightedDefaultBranches = new Map([ ['master', maxDefaultBranchWeight], @@ -589,6 +591,80 @@ export class GitProviderService implements Disposable { } } + getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri { + if (base == null) { + if (typeof pathOrUri === 'string') { + if (isUriRegex.test(pathOrUri)) { + debugger; + return Uri.parse(pathOrUri, true); + } + + // I think it is safe to assume this should be file:// + return Uri.file(pathOrUri); + } + + return pathOrUri; + } + + // Short-circuit if the base is already a Uri and the path is relative + if (typeof base !== 'string' && typeof pathOrUri === 'string' && !isAbsolute(pathOrUri)) { + return Uri.joinPath(base, pathOrUri); + } + + const { provider } = this.getProvider(base); + return provider.getAbsoluteUri(pathOrUri, base); + } + + @log() + async getBestRevisionUri( + repoPath: string | Uri | undefined, + path: string, + ref: string | undefined, + ): Promise { + if (repoPath == null || ref === GitRevision.deletedOrMissing) return undefined; + + const { provider, path: rp } = this.getProvider(repoPath); + return provider.getBestRevisionUri(rp, provider.getRelativePath(path, rp), ref); + } + + getRelativePath(pathOrUri: string | Uri, base: string | Uri): string { + const { provider } = this.getProvider(pathOrUri instanceof Uri ? pathOrUri : base); + return provider.getRelativePath(pathOrUri, base); + } + + getRevisionUri(uri: GitUri): Uri; + getRevisionUri(ref: string, path: string, repoPath: string): Uri; + getRevisionUri(ref: string, file: GitFile, repoPath: string): Uri; + @log() + getRevisionUri(refOrUri: string | GitUri, pathOrFile?: string | GitFile, repoPath?: string): Uri { + let path: string; + let ref: string | undefined; + + if (typeof refOrUri === 'string') { + ref = refOrUri; + + if (typeof pathOrFile === 'string') { + path = pathOrFile; + } else { + path = pathOrFile!.originalFileName ?? pathOrFile!.fileName; + } + } else { + ref = refOrUri.sha; + repoPath = refOrUri.repoPath!; + + path = refOrUri.scheme === DocumentSchemes.File ? refOrUri.fsPath : refOrUri.path; + } + + const { provider, path: rp } = this.getProvider(repoPath!); + return provider.getRevisionUri(rp, provider.getRelativePath(path, rp), ref!); + } + + @log() + async getWorkingUri(repoPath: string | Uri, uri: Uri) { + const { provider, path } = this.getProvider(repoPath); + return provider.getWorkingUri(path, uri); + } + @log() addRemote(repoPath: string | Uri, name: string, url: string): Promise { const { provider, path } = this.getProvider(repoPath); @@ -1521,7 +1597,7 @@ export class GitProviderService implements Disposable { if (typeof pathOrUri === 'string') { if (!pathOrUri) return undefined; - return this._repositories.getClosest(Uri.file(normalizePath(pathOrUri))); + return this._repositories.getClosest(this.getAbsoluteUri(pathOrUri)); } return this._repositories.getClosest(pathOrUri); @@ -1590,7 +1666,7 @@ export class GitProviderService implements Disposable { if (repoPath == null || !path) return undefined; const { provider, path: rp } = this.getProvider(repoPath); - return provider.getTreeEntryForRevision(rp, path, ref); + return provider.getTreeEntryForRevision(rp, provider.getRelativePath(path, rp), ref); } @log() @@ -1601,30 +1677,13 @@ export class GitProviderService implements Disposable { return provider.getTreeForRevision(path, ref); } + @gate() @log() getRevisionContent(repoPath: string | Uri, path: string, ref: string): Promise { const { provider, path: rp } = this.getProvider(repoPath); return provider.getRevisionContent(rp, path, ref); } - @log() - async getVersionedUri( - repoPath: string | Uri | undefined, - fileName: string, - ref: string | undefined, - ): Promise { - if (repoPath == null || ref === GitRevision.deletedOrMissing) return undefined; - - const { provider, path } = this.getProvider(repoPath); - return provider.getVersionedUri(path, fileName, ref); - } - - @log() - async getWorkingUri(repoPath: string | Uri, uri: Uri) { - const { provider, path } = this.getProvider(repoPath); - return provider.getWorkingUri(path, uri); - } - @log({ args: { 1: false } }) async hasBranchOrTag( repoPath: string | Uri | undefined, diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 9c54997..3eb8b49 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -1,12 +1,15 @@ 'use strict'; import { Uri } from 'vscode'; +import { decodeUtf8Hex, encodeUtf8Hex } from '@env/hex'; import { UriComparer } from '../comparers'; import { DocumentSchemes } from '../constants'; import { Container } from '../container'; import { Logger } from '../logger'; -import { debug, memoize, Strings } from '../system'; -import { basename, dirname, isAbsolute, joinPaths, normalizePath, relative } from '../system/path'; -import { CharCode } from '../system/string'; +import { debug } from '../system/decorators/log'; +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'; export interface GitCommitish { @@ -54,30 +57,18 @@ export class GitUri extends (Uri as any as UriEx) { } if (uri.scheme === DocumentSchemes.GitLens) { - const data = JSON.parse(uri.query) as UriRevisionData; - - // Fixes issues with uri.query: - // When Uri's come from the FileSystemProvider, the uri.query only contains the root repo info (not the actual file path) - // When Uri's come from breadcrumbs (via the FileSystemProvider), the uri.query contains the wrong file path - if (data.path !== uri.path) { - if (data.path.startsWith('//') && !uri.path.startsWith('//')) { - data.path = `/${uri.path}`; - } else { - data.path = uri.path; - } - } - super({ scheme: uri.scheme, authority: uri.authority, - path: data.path, - query: JSON.stringify(data), + path: uri.path, + query: uri.query, fragment: uri.fragment, }); - this.repoPath = data.repoPath; - if (GitRevision.isUncommittedStaged(data.ref) || !GitRevision.isUncommitted(data.ref)) { - this.sha = data.ref; + const metadata = decodeGitLensRevisionUriAuthority(uri.authority); + this.repoPath = metadata.repoPath; + if (GitRevision.isUncommittedStaged(metadata.ref) || !GitRevision.isUncommitted(metadata.ref)) { + this.sha = metadata.ref; } return; @@ -98,7 +89,10 @@ export class GitUri extends (Uri as any as UriEx) { } let authority = uri.authority; - let fsPath = GitUri.resolvePath(commitOrRepoPath.fileName ?? uri.fsPath, commitOrRepoPath.repoPath); + let fsPath = normalizePath( + Container.instance.git.getAbsoluteUri(commitOrRepoPath.fileName ?? uri.fsPath, commitOrRepoPath.repoPath) + .fsPath, + ); // Check for authority as used in UNC shares or use the path as given if (fsPath.charCodeAt(0) === CharCode.Slash && fsPath.charCodeAt(1) === CharCode.Slash) { @@ -165,7 +159,7 @@ export class GitUri extends (Uri as any as UriEx) { @memoize() private get relativeFsPath() { - return this.repoPath == null || this.repoPath.length === 0 ? this.fsPath : relative(this.repoPath, this.fsPath); + return !this.repoPath ? this.fsPath : relative(this.repoPath, this.fsPath); } @memoize() @@ -180,6 +174,7 @@ export class GitUri extends (Uri as any as UriEx) { @memoize() documentUri() { + // TODO@eamodio which is correct? return Uri.from({ scheme: this.scheme, authority: this.authority, @@ -187,6 +182,7 @@ export class GitUri extends (Uri as any as UriEx) { query: this.query, fragment: this.fragment, }); + return Container.instance.git.getAbsoluteUri(this.fsPath, this.repoPath); } equals(uri: Uri | undefined) { @@ -205,7 +201,7 @@ export class GitUri extends (Uri as any as UriEx) { @memoize() toFileUri() { - return GitUri.file(this.fsPath); + return Container.instance.git.getAbsoluteUri(this.fsPath, this.repoPath); } static file(path: string, useVslsScheme?: boolean) { @@ -227,7 +223,7 @@ export class GitUri extends (Uri as any as UriEx) { } static fromFile(file: string | GitFile, repoPath: string, ref?: string, original: boolean = false): GitUri { - const uri = GitUri.resolve( + const uri = Container.instance.git.getAbsoluteUri( typeof file === 'string' ? file : (original && file.originalFileName) || file.fileName, repoPath, ); @@ -237,9 +233,9 @@ export class GitUri extends (Uri as any as UriEx) { } static fromRepoPath(repoPath: string, ref?: string) { - return ref == null || ref.length === 0 - ? new GitUri(GitUri.file(repoPath), repoPath) - : new GitUri(GitUri.file(repoPath), { repoPath: repoPath, sha: ref }); + return !ref + ? new GitUri(Container.instance.git.getAbsoluteUri(repoPath, repoPath), repoPath) + : new GitUri(Container.instance.git.getAbsoluteUri(repoPath, repoPath), { repoPath: repoPath, sha: ref }); } static fromRevisionUri(uri: Uri): GitUri { @@ -362,12 +358,12 @@ export class GitUri extends (Uri as any as UriEx) { let file = basename(fileName); if (options?.truncateTo != null && file.length >= options.truncateTo) { - return Strings.truncateMiddle(file, options.truncateTo); + return truncateMiddle(file, options.truncateTo); } if (options?.suffix) { if (options?.truncateTo != null && file.length + options.suffix.length >= options?.truncateTo) { - return `${Strings.truncateMiddle(file, options.truncateTo - options.suffix.length)}${options.suffix}`; + return `${truncateMiddle(file, options.truncateTo - options.suffix.length)}${options.suffix}`; } file += options.suffix; @@ -395,12 +391,12 @@ export class GitUri extends (Uri as any as UriEx) { let file = basename(fileName); if (truncateTo != null && file.length >= truncateTo) { - return Strings.truncateMiddle(file, truncateTo); + return truncateMiddle(file, truncateTo); } if (suffix) { if (truncateTo != null && file.length + suffix.length >= truncateTo) { - return `${Strings.truncateMiddle(file, truncateTo - suffix.length)}${suffix}`; + return `${truncateMiddle(file, truncateTo - suffix.length)}${suffix}`; } file += suffix; @@ -412,7 +408,7 @@ export class GitUri extends (Uri as any as UriEx) { file = `/${file}`; if (truncateTo != null && file.length + directory.length >= truncateTo) { - return `${Strings.truncateLeft(directory, truncateTo - file.length)}${file}`; + return `${truncateLeft(directory, truncateTo - file.length)}${file}`; } return `${directory}${file}`; @@ -427,34 +423,17 @@ export class GitUri extends (Uri as any as UriEx) { return normalizePath(relativePath); } - static git(fileName: string, repoPath?: string) { - const path = GitUri.resolvePath(fileName, repoPath); - return Uri.parse( - // Change encoded / back to / otherwise uri parsing won't work properly - `${DocumentSchemes.Git}:/${encodeURIComponent(path).replace(/%2F/g, '/')}?${encodeURIComponent( - JSON.stringify({ - // Ensure we use the fsPath here, otherwise the url won't open properly - path: Uri.file(path).fsPath, - ref: '~', - }), - )}`, - ); - } - - static resolvePath(fileName: string, repoPath?: string) { - const normalizedFileName = normalizePath(fileName); - if (repoPath === undefined) return normalizedFileName; - - const normalizedRepoPath = normalizePath(repoPath); - if (normalizedFileName == null || normalizedFileName.length === 0) return normalizedRepoPath; - - if (normalizedFileName.startsWith(normalizedRepoPath)) return normalizedFileName; - - return normalizePath(joinPaths(normalizedRepoPath, normalizedFileName)); - } - - static resolve(fileName: string, repoPath?: string) { - return GitUri.file(this.resolvePath(fileName, repoPath)); + static git(path: string, repoPath?: string): Uri { + const uri = Container.instance.git.getAbsoluteUri(path, repoPath); + return Uri.from({ + scheme: DocumentSchemes.Git, + path: uri.path, + query: JSON.stringify({ + // Ensure we use the fsPath here, otherwise the url won't open properly + path: uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path, + ref: '~', + }), + }); } static toKey(fileName: string): string; @@ -467,67 +446,12 @@ export class GitUri extends (Uri as any as UriEx) { // ? GitUri.file(fileNameOrUri).toString(true) // : fileNameOrUri.toString(true); } +} - static toRevisionUri(uri: GitUri): Uri; - static toRevisionUri(ref: string, fileName: string, repoPath: string): Uri; - static toRevisionUri(ref: string, file: GitFile, repoPath: string): Uri; - static toRevisionUri(uriOrRef: string | GitUri, fileNameOrFile?: string | GitFile, repoPath?: string): Uri { - let fileName: string; - let ref: string | undefined; - let shortSha: string | undefined; - - if (typeof uriOrRef === 'string') { - if (typeof fileNameOrFile === 'string') { - fileName = fileNameOrFile; - } else { - //if (fileNameOrFile!.status === 'D') { - fileName = GitUri.resolvePath(fileNameOrFile!.originalFileName ?? fileNameOrFile!.fileName, repoPath); - // } else { - // fileName = GitUri.resolve(fileNameOrFile!.fileName, repoPath); - } - - ref = uriOrRef; - shortSha = GitRevision.shorten(ref); - } else { - fileName = uriOrRef.fsPath; - - ref = uriOrRef.sha; - shortSha = uriOrRef.shortSha; - repoPath = uriOrRef.repoPath!; - } - - if (ref == null || ref.length === 0) { - return Uri.file(fileName); - } - - if (GitRevision.isUncommitted(ref)) { - return GitRevision.isUncommittedStaged(ref) ? GitUri.git(fileName, repoPath) : Uri.file(fileName); - } - - let filePath = normalizePath(fileName); - if (filePath.charCodeAt(0) !== CharCode.Slash) { - filePath = `/${filePath}`; - } - - const data: UriRevisionData = { - path: filePath, - ref: ref, - repoPath: normalizePath(repoPath!), - }; - - const uri = Uri.parse( - // Replace / in the authority with a similar unicode characters otherwise parsing will be wrong - `${DocumentSchemes.GitLens}://${encodeURIComponent(shortSha.replace(/\//g, '\u200A\u2215\u200A'))}${ - // Change encoded / back to / otherwise uri parsing won't work properly - filePath === '/' ? '' : encodeURIComponent(filePath).replace(/%2F/g, '/') - }?${encodeURIComponent(JSON.stringify(data))}`, - ); - return uri; - } +export function decodeGitLensRevisionUriAuthority(authority: string): T { + return JSON.parse(decodeUtf8Hex(authority)) as T; } -interface UriRevisionData { - path: string; - ref?: string; - repoPath: string; +export function encodeGitLensRevisionUriAuthority(metadata: T): string { + return encodeUtf8Hex(JSON.stringify(metadata)); } diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index a937928..43a2d94 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -126,7 +126,9 @@ export abstract class GitCommit implements GitRevisionReference { @memoize() get originalUri(): Uri { - return this.originalFileName ? GitUri.resolve(this.originalFileName, this.repoPath) : this.uri; + return this.originalFileName + ? Container.instance.git.getAbsoluteUri(this.originalFileName, this.repoPath) + : this.uri; } get previousFileSha(): string { @@ -138,12 +140,14 @@ export abstract class GitCommit implements GitRevisionReference { } get previousUri(): Uri { - return this.previousFileName ? GitUri.resolve(this.previousFileName, this.repoPath) : this.uri; + return this.previousFileName + ? Container.instance.git.getAbsoluteUri(this.previousFileName, this.repoPath) + : this.uri; } @memoize() get uri(): Uri { - return GitUri.resolve(this.fileName, this.repoPath); + return Container.instance.git.getAbsoluteUri(this.fileName, this.repoPath); } @memoize() diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index 47c3061..dbe725a 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -1,5 +1,6 @@ 'use strict'; import { Uri } from 'vscode'; +import { Container } from '../../container'; import { memoize, Strings } from '../../system'; import { GitUri } from '../gitUri'; import { GitReference } from '../models'; @@ -90,7 +91,7 @@ export class GitLogCommit extends GitCommit { } get nextUri(): Uri { - return this.nextFileName ? GitUri.resolve(this.nextFileName, this.repoPath) : this.uri; + return this.nextFileName ? Container.instance.git.getAbsoluteUri(this.nextFileName, this.repoPath) : this.uri; } override get previousFileSha(): string { diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 2443ebf..a96e820 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -19,8 +19,14 @@ import { BuiltInGitCommands, BuiltInGitConfiguration, Starred, WorkspaceState } import { Container } from '../../container'; import { Logger } from '../../logger'; import { Messages } from '../../messages'; -import { Arrays, Dates, debug, Functions, gate, Iterables, log, logName, memoize } from '../../system'; -import { basename, joinPaths, relative } from '../../system/path'; +import { filterMap, groupByMap } from '../../system/array'; +import { getFormatter } from '../../system/date'; +import { gate } from '../../system/decorators/gate'; +import { debug, log, logName } from '../../system/decorators/log'; +import { memoize } from '../../system/decorators/memoize'; +import { debounce } from '../../system/function'; +import { filter, join, some } from '../../system/iterable'; +import { basename } from '../../system/path'; import { runGitCommandInTerminal } from '../../terminal'; import { GitProviderDescriptor } from '../gitProvider'; import { GitUri } from '../gitUri'; @@ -85,8 +91,8 @@ export class RepositoryChangeEvent { toString(changesOnly: boolean = false): string { return changesOnly - ? `changes=${Iterables.join(this._changes, ', ')}` - : `{ repository: ${this.repository?.name ?? ''}, changes: ${Iterables.join(this._changes, ', ')} }`; + ? `changes=${join(this._changes, ', ')}` + : `{ repository: ${this.repository?.name ?? ''}, changes: ${join(this._changes, ', ')} }`; } changed(...args: [...RepositoryChange[], RepositoryChangeComparisonMode]) { @@ -94,7 +100,7 @@ export class RepositoryChangeEvent { const mode = args[args.length - 1] as RepositoryChangeComparisonMode; if (mode === RepositoryChangeComparisonMode.Any) { - return Iterables.some(this._changes, c => affected.includes(c)); + return some(this._changes, c => affected.includes(c)); } let changes = this._changes; @@ -116,7 +122,7 @@ export class RepositoryChangeEvent { } } - const intersection = [...Iterables.filter(changes, c => affected.includes(c))]; + const intersection = [...filter(changes, c => affected.includes(c))]; return mode === RepositoryChangeComparisonMode.Exclusive ? intersection.length === changes.size : intersection.length === affected.length; @@ -135,7 +141,7 @@ export interface RepositoryFileSystemChangeEvent { @logName((r, name) => `${name}(${r.id})`) export class Repository implements Disposable { static formatLastFetched(lastFetched: number, short: boolean = true): string { - const formatter = Dates.getFormatter(new Date(lastFetched)); + const formatter = getFormatter(new Date(lastFetched)); if (Date.now() - lastFetched < millisecondsPerDay) { return formatter.fromNow(); } @@ -204,7 +210,7 @@ export class Repository implements Disposable { suspended: boolean, closed: boolean = false, ) { - const relativePath = relative(folder.uri.fsPath, path); + const relativePath = container.git.getRelativePath(folder.uri, path); if (root) { // Check if the repository is not contained by a workspace folder const repoFolder = workspace.getWorkspaceFolder(GitUri.fromRepoPath(path)); @@ -261,7 +267,7 @@ export class Repository implements Disposable { @memoize() get uri(): Uri { - return Uri.file(this.path); + return this.container.git.getAbsoluteUri(this.path); } get etag(): number { @@ -402,9 +408,7 @@ export class Repository implements Disposable { if (remote) { const trackingBranches = localBranches.filter(b => b.upstream != null); if (trackingBranches.length !== 0) { - const branchesByOrigin = Arrays.groupByMap(trackingBranches, b => - GitBranch.getRemote(b.upstream!.name), - ); + const branchesByOrigin = groupByMap(trackingBranches, b => GitBranch.getRemote(b.upstream!.name)); for (const [remote, branches] of branchesByOrigin.entries()) { this.runTerminalCommand( @@ -420,7 +424,7 @@ export class Repository implements Disposable { const remoteBranches = branches.filter(b => b.remote); if (remoteBranches.length !== 0) { - const branchesByOrigin = Arrays.groupByMap(remoteBranches, b => GitBranch.getRemote(b.name)); + const branchesByOrigin = groupByMap(remoteBranches, b => GitBranch.getRemote(b.name)); for (const [remote, branches] of branchesByOrigin.entries()) { this.runTerminalCommand( @@ -440,7 +444,7 @@ export class Repository implements Disposable { containsUri(uri: Uri) { if (GitUri.is(uri)) { - uri = uri.repoPath != null ? GitUri.file(uri.repoPath) : uri.documentUri(); + uri = uri.repoPath != null ? this.container.git.getAbsoluteUri(uri.repoPath) : uri.documentUri(); } return this.folder === workspace.getWorkspaceFolder(uri); @@ -531,10 +535,11 @@ export class Repository implements Disposable { } try { - const stat = await workspace.fs.stat(Uri.file(joinPaths(this.path, '.git/FETCH_HEAD'))); + // TODO@eamodio: Need to move this into an explicit provider call + const stats = await workspace.fs.stat(this.container.git.getAbsoluteUri('.git/FETCH_HEAD', this.path)); // If the file is empty, assume the fetch failed, and don't update the timestamp - if (stat.size > 0) { - this._lastFetched = stat.mtime; + if (stats.size > 0) { + this._lastFetched = stats.mtime; } } catch { this._lastFetched = undefined; @@ -581,7 +586,7 @@ export class Repository implements Disposable { this._remotesDisposable = undefined; this._remotesDisposable = Disposable.from( - ...Iterables.filterMap(await remotes, r => { + ...filterMap(await remotes, r => { if (!RichRemoteProvider.is(r.provider)) return undefined; return r.provider.onDidChange(() => this.fireChange(RepositoryChange.RemoteProviders)); @@ -879,7 +884,7 @@ export class Repository implements Disposable { } toAbsoluteUri(path: string, options?: { validate?: boolean }): Uri | undefined { - const uri = Uri.joinPath(GitUri.file(this.path), path); + const uri = this.container.git.getAbsoluteUri(path, this.path); return !(options?.validate ?? true) || this.containsUri(uri) ? uri : undefined; } @@ -964,7 +969,7 @@ export class Repository implements Disposable { this._updatedAt = Date.now(); if (this._fireChangeDebounced == null) { - this._fireChangeDebounced = Functions.debounce(this.fireChangeCore.bind(this), 250); + this._fireChangeDebounced = debounce(this.fireChangeCore.bind(this), 250); } this._pendingRepoChange = this._pendingRepoChange?.with(changes) ?? new RepositoryChangeEvent(this, changes); @@ -997,7 +1002,7 @@ export class Repository implements Disposable { this._updatedAt = Date.now(); if (this._fireFileSystemChangeDebounced == null) { - this._fireFileSystemChangeDebounced = Functions.debounce(this.fireFileSystemChangeCore.bind(this), 2500); + this._fireFileSystemChangeDebounced = debounce(this.fireFileSystemChangeCore.bind(this), 2500); } if (this._pendingFileSystemChange == null) { diff --git a/src/git/models/status.ts b/src/git/models/status.ts index 873d8b6..3c1e6ae 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -2,8 +2,8 @@ import { Uri } from 'vscode'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { memoize, Strings } from '../../system'; -import { GitUri } from '../gitUri'; +import { memoize } from '../../system/decorators/memoize'; +import { pluralize } from '../../system/string'; import { GitCommitType, GitLogCommit, GitRemote, GitRevision, GitUser } from '../models'; import { GitBranch, GitTrackingState } from './branch'; import { GitFile, GitFileConflictStatus, GitFileIndexStatus, GitFileStatus, GitFileWorkingTreeStatus } from './file'; @@ -205,13 +205,13 @@ export class GitStatus { if (expand) { let status = ''; if (added) { - status += `${Strings.pluralize('file', added)} added`; + status += `${pluralize('file', added)} added`; } if (changed) { - status += `${status.length === 0 ? '' : separator}${Strings.pluralize('file', changed)} changed`; + status += `${status.length === 0 ? '' : separator}${pluralize('file', changed)} changed`; } if (deleted) { - status += `${status.length === 0 ? '' : separator}${Strings.pluralize('file', deleted)} deleted`; + status += `${status.length === 0 ? '' : separator}${pluralize('file', deleted)} deleted`; } return `${prefix}${status}${suffix}`; } @@ -282,12 +282,12 @@ export class GitStatus { status = 'missing'; } else { if (state.behind) { - status += `${Strings.pluralize('commit', state.behind, { + status += `${pluralize('commit', state.behind, { infix: icons ? '$(arrow-down) ' : undefined, })} behind`; } if (state.ahead) { - status += `${status.length === 0 ? '' : separator}${Strings.pluralize('commit', state.ahead, { + status += `${status.length === 0 ? '' : separator}${pluralize('commit', state.ahead, { infix: icons ? '$(arrow-up) ' : undefined, })} ahead`; if (suffix.startsWith(` ${upstream.name.split('/')[0]}`)) { @@ -403,7 +403,7 @@ export class GitStatusFile implements GitFile { @memoize() get uri(): Uri { - return GitUri.resolve(this.fileName, this.repoPath); + return Container.instance.git.getAbsoluteUri(this.fileName, this.repoPath); } getFormattedDirectory(includeOriginal: boolean = false): string { diff --git a/src/system/path.ts b/src/system/path.ts index 414d6dc..334a348 100644 --- a/src/system/path.ts +++ b/src/system/path.ts @@ -5,7 +5,7 @@ import { isLinux, isWindows } from '@env/platform'; // TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies // import { CharCode } from './string'; -export { basename, dirname, extname, isAbsolute, join as joinPaths, relative } from 'path'; +export { basename, dirname, extname, isAbsolute, join as joinPaths } from 'path'; const driveLetterNormalizeRegex = /(?<=^\/?)([A-Z])(?=:\/)/; const pathNormalizeRegex = /\\/g; @@ -120,6 +120,14 @@ export function normalizePath(path: string): string { return path; } +export function relative(from: string, to: string, ignoreCase?: boolean): string { + from = normalizePath(from); + to = normalizePath(to); + + const index = commonBaseIndex(`${to}/`, `${from}/`, '/', ignoreCase); + return index > 0 ? to.substring(index + 1) : to; +} + export function splitPath( path: string, repoPath: string | undefined, diff --git a/src/trackers/documentTracker.ts b/src/trackers/documentTracker.ts index 8646401..760e6a9 100644 --- a/src/trackers/documentTracker.ts +++ b/src/trackers/documentTracker.ts @@ -67,6 +67,7 @@ export class DocumentTracker implements Disposable { private _dirtyIdleTriggerDelay: number; private readonly _disposable: Disposable; + // TODO@eamodio: replace with a trie? private readonly _documentMap = new Map>>(); constructor(protected readonly container: Container) { diff --git a/src/trackers/trackedDocument.ts b/src/trackers/trackedDocument.ts index 900524b..7ef6bf9 100644 --- a/src/trackers/trackedDocument.ts +++ b/src/trackers/trackedDocument.ts @@ -5,7 +5,7 @@ import { Container } from '../container'; import { GitUri } from '../git/gitUri'; import { GitRevision } from '../git/models'; import { Logger } from '../logger'; -import { Functions } from '../system'; +import { debounce, Deferrable } from '../system/function'; export interface DocumentBlameStateChangeEvent { readonly editor: TextEditor; @@ -115,7 +115,7 @@ export class TrackedDocument implements Disposable { } private _updateDebounced: - | Functions.Deferrable<({ forceBlameChange }?: { forceBlameChange?: boolean | undefined }) => Promise> + | Deferrable<({ forceBlameChange }?: { forceBlameChange?: boolean | undefined }) => Promise> | undefined; reset(reason: 'config' | 'document' | 'repository') { @@ -130,7 +130,7 @@ export class TrackedDocument implements Disposable { if (reason === 'repository' && isActiveDocument(this.document)) { if (this._updateDebounced == null) { - this._updateDebounced = Functions.debounce(this.update.bind(this), 250); + this._updateDebounced = debounce(this.update.bind(this), 250); } void this._updateDebounced(); diff --git a/src/views/nodes/lineHistoryTrackerNode.ts b/src/views/nodes/lineHistoryTrackerNode.ts index 743c138..1aaf2bd 100644 --- a/src/views/nodes/lineHistoryTrackerNode.ts +++ b/src/views/nodes/lineHistoryTrackerNode.ts @@ -6,7 +6,9 @@ import { GitCommitish, GitUri } from '../../git/gitUri'; import { GitReference, GitRevision } from '../../git/models'; import { Logger } from '../../logger'; import { ReferencePicker } from '../../quickpicks'; -import { debug, Functions, gate, log } from '../../system'; +import { gate } from '../../system/decorators/gate'; +import { debug, log } from '../../system/decorators/log'; +import { debounce } from '../../system/function'; import { LinesChangeEvent } from '../../trackers/gitLineTracker'; import { FileHistoryView } from '../fileHistoryView'; import { LineHistoryView } from '../lineHistoryView'; @@ -220,7 +222,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode implements this.file, ); // Use the file icon and decorations - item.resourceUri = GitUri.resolve(this.file.fileName, this.repoPath); + item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath); item.iconPath = ThemeIcon.File; item.command = this.getCommand(); @@ -115,7 +115,7 @@ export class MergeConflictFileNode extends ViewNode implements title: 'Open File', command: BuiltInCommands.Open, arguments: [ - GitUri.resolve(this.file.fileName, this.repoPath), + this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath), { preserveFocus: true, preview: true, diff --git a/src/views/nodes/mergeConflictIncomingChangesNode.ts b/src/views/nodes/mergeConflictIncomingChangesNode.ts index fbd2cfc..6e04814 100644 --- a/src/views/nodes/mergeConflictIncomingChangesNode.ts +++ b/src/views/nodes/mergeConflictIncomingChangesNode.ts @@ -83,7 +83,13 @@ export class MergeConflictIncomingChangesNode extends ViewNode implements FileNo } // Use the file icon and decorations - item.resourceUri = GitUri.resolve(this.file.fileName, this.repoPath); + item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath); item.iconPath = ThemeIcon.File; item.command = this.getCommand(); @@ -104,7 +104,7 @@ export class StatusFileNode extends ViewNode implements FileNo } // Use the file icon and decorations - item.resourceUri = GitUri.resolve(this.file.fileName, this.repoPath); + item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.fileName, this.repoPath); item.iconPath = ThemeIcon.File; } else { item.contextValue = ContextValues.StatusFileCommits; diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 9a4f20f..6b5b5e2 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -1090,16 +1090,16 @@ export class ViewCommands { let uri = options.revisionUri; if (uri == null) { if (node instanceof ResultsFileNode || node instanceof MergeConflictFileNode) { - uri = GitUri.toRevisionUri(node.uri); + uri = Container.instance.git.getRevisionUri(node.uri); } else { uri = node.commit.status === 'D' - ? GitUri.toRevisionUri( + ? Container.instance.git.getRevisionUri( node.commit.previousSha!, node.commit.previousUri.fsPath, node.commit.repoPath, ) - : GitUri.toRevisionUri(node.uri); + : Container.instance.git.getRevisionUri(node.uri); } } diff --git a/src/vsls/host.ts b/src/vsls/host.ts index 228fe1a..e79cd3c 100644 --- a/src/vsls/host.ts +++ b/src/vsls/host.ts @@ -3,9 +3,9 @@ import { CancellationToken, Disposable, Uri, workspace, WorkspaceFoldersChangeEv import { git } from '@env/git'; import type { LiveShare, SharedService } from '../@types/vsls'; import { Container } from '../container'; -import { GitUri } from '../git/gitUri'; import { Logger } from '../logger'; -import { debug, Iterables, log } from '../system'; +import { debug, log } from '../system/decorators/log'; +import { filterMap, join } from '../system/iterable'; import { normalizePath } from '../system/path'; import { GitCommandRequest, @@ -120,11 +120,11 @@ export class VslsHostService implements Disposable { this._sharedToLocalPaths.set(sharedPath, localPath); } - let localPaths = Iterables.join(this._sharedToLocalPaths.values(), '|'); + let localPaths = join(this._sharedToLocalPaths.values(), '|'); localPaths = localPaths.replace(/(\/|\\)/g, '[\\\\/|\\\\]'); this._localPathsRegex = new RegExp(`(${localPaths})`, 'gi'); - let sharedPaths = Iterables.join(this._localToSharedPaths.values(), '|'); + let sharedPaths = join(this._localToSharedPaths.values(), '|'); sharedPaths = sharedPaths.replace(/(\/|\\)/g, '[\\\\/|\\\\]'); this._sharedPathsRegex = new RegExp(`^(${sharedPaths})`, 'i'); } @@ -155,7 +155,7 @@ export class VslsHostService implements Disposable { const localCwd = this._sharedToLocalPaths.get('/~0'); if (localCwd !== undefined) { isRootWorkspace = true; - options.cwd = GitUri.resolvePath(options.cwd, localCwd); + options.cwd = normalizePath(this.container.git.getAbsoluteUri(options.cwd, localCwd).fsPath); } } } @@ -216,7 +216,7 @@ export class VslsHostService implements Disposable { const normalized = normalizePath(uri.fsPath).toLowerCase(); const repos = [ - ...Iterables.filterMap(this.container.git.repositories, r => { + ...filterMap(this.container.git.repositories, r => { if (!r.normalizedPath.startsWith(normalized)) return undefined; const vslsUri = this.convertLocalUriToShared(r.folder.uri);