From 1d93f9cdbd99d06e6240d1b1442a90ef7276ab3a Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 28 Dec 2021 01:39:47 -0500 Subject: [PATCH] Reorganizes git models/parsers/formatters imports Reorganizes local git support to exclude from web Swaps sha1 usages to md5 to avoid needing sha1 on the web (md5 is required by Gravatar) Avoids crypto-browserfy and imports md5.js directly Uses platform crypto for random nonce strings --- .eslintrc.browser.json | 2 +- package.json | 6 +- src/@types/md5.js.d.ts | 13 + src/annotations/annotations.ts | 3 +- src/annotations/autolinks.ts | 2 +- src/annotations/blameAnnotationProvider.ts | 2 +- src/annotations/gutterBlameAnnotationProvider.ts | 3 +- src/annotations/gutterChangesAnnotationProvider.ts | 2 +- .../gutterHeatmapBlameAnnotationProvider.ts | 2 +- src/annotations/lineAnnotationController.ts | 4 +- src/avatars.ts | 2 +- src/codelens/codeLensProvider.ts | 3 +- src/commands/common.ts | 11 +- src/commands/createPullRequestOnRemote.ts | 3 +- src/commands/diffLineWithPrevious.ts | 2 +- src/commands/diffLineWithWorking.ts | 2 +- src/commands/diffWith.ts | 2 +- src/commands/diffWithNext.ts | 2 +- src/commands/diffWithPrevious.ts | 2 +- src/commands/diffWithRevision.ts | 2 +- src/commands/diffWithRevisionFrom.ts | 2 +- src/commands/diffWithWorking.ts | 2 +- src/commands/externalDiff.ts | 2 +- src/commands/git/branch.ts | 2 +- src/commands/git/cherry-pick.ts | 2 +- src/commands/git/coauthors.ts | 2 +- src/commands/git/fetch.ts | 2 +- src/commands/git/log.ts | 2 +- src/commands/git/merge.ts | 2 +- src/commands/git/pull.ts | 2 +- src/commands/git/push.ts | 2 +- src/commands/git/rebase.ts | 2 +- src/commands/git/reset.ts | 2 +- src/commands/git/revert.ts | 2 +- src/commands/git/search.ts | 3 +- src/commands/git/show.ts | 2 +- src/commands/git/stash.ts | 29 +- src/commands/git/status.ts | 2 +- src/commands/git/switch.ts | 2 +- src/commands/git/tag.ts | 2 +- src/commands/gitCommands.actions.ts | 4 +- src/commands/openBranchOnRemote.ts | 2 +- src/commands/openBranchesOnRemote.ts | 2 +- src/commands/openCommitOnRemote.ts | 2 +- src/commands/openComparisonOnRemote.ts | 2 +- src/commands/openFileAtRevision.ts | 2 +- src/commands/openFileAtRevisionFrom.ts | 2 +- src/commands/openFileOnRemote.ts | 3 +- src/commands/openOnRemote.ts | 3 +- src/commands/openRepoOnRemote.ts | 2 +- src/commands/quickCommand.steps.ts | 11 +- src/commands/remoteProviders.ts | 3 +- src/commands/searchCommits.ts | 2 +- src/commands/showCommitsInView.ts | 2 +- src/commands/showQuickBranchHistory.ts | 2 +- src/commands/showQuickCommit.ts | 2 +- src/commands/showQuickCommitFile.ts | 2 +- src/commands/showQuickFileHistory.ts | 2 +- src/commands/stashApply.ts | 2 +- src/constants.ts | 2 +- src/container.ts | 20 +- src/emojis.ts | 7 +- src/env/browser/crypto.ts | 10 + src/env/browser/git.ts | 11 + src/env/browser/platform.ts | 5 + src/env/node/crypto.ts | 9 + src/env/node/git.ts | 12 + src/env/node/git/git.ts | 1544 ++++++++ src/env/node/git/localGitProvider.ts | 3835 ++++++++++++++++++++ src/env/node/git/locator.ts | 141 + src/env/node/git/shell.ts | 223 ++ src/env/node/platform.ts | 4 + src/extension.ts | 4 +- src/git/commandOptions.ts | 23 + src/git/errors.ts | 17 + src/git/formatters.ts | 4 + src/git/formatters/commitFormatter.ts | 11 +- src/git/formatters/formatters.ts | 4 - src/git/fsProvider.ts | 2 +- src/git/git.ts | 1569 -------- src/git/gitProvider.ts | 7 +- src/git/gitProviderService.ts | 12 +- src/git/gitUri.ts | 2 +- src/git/locator.ts | 141 - src/git/models.ts | 28 + src/git/models/branch.ts | 5 +- src/git/models/commit.ts | 4 +- src/git/models/diff.ts | 2 + src/git/models/logCommit.ts | 2 +- src/git/models/merge.ts | 2 +- src/git/models/models.ts | 368 -- src/git/models/rebase.ts | 2 +- src/git/models/reference.ts | 341 ++ src/git/models/reflog.ts | 2 +- src/git/models/remote.ts | 2 +- src/git/models/repository.ts | 37 +- src/git/models/stashCommit.ts | 2 +- src/git/models/status.ts | 2 +- src/git/models/tag.ts | 2 +- src/git/parsers.ts | 13 + src/git/parsers/blameParser.ts | 2 +- src/git/parsers/logParser.ts | 2 +- src/git/parsers/parsers.ts | 13 - src/git/parsers/remoteParser.ts | 4 +- src/git/parsers/shortlogParser.ts | 2 +- src/git/parsers/stashParser.ts | 3 +- src/git/parsers/statusParser.ts | 2 +- src/git/parsers/tagParser.ts | 2 +- src/git/parsers/treeParser.ts | 2 +- src/git/providers/localGitProvider.ts | 3790 ------------------- src/git/remotes/bitbucket-server.ts | 2 +- src/git/remotes/bitbucket.ts | 2 +- src/git/remotes/factory.ts | 4 +- src/git/remotes/gerrit.ts | 2 +- src/git/remotes/gitea.ts | 2 +- src/git/remotes/github.ts | 2 +- src/git/remotes/gitlab.ts | 2 +- src/git/remotes/provider.ts | 2 +- src/git/search.ts | 3 +- src/git/shell.ts | 223 -- src/github/github.ts | 6 +- src/hovers/hovers.ts | 8 +- src/messages.ts | 2 +- src/quickpicks/commitPicker.ts | 2 +- src/quickpicks/commitQuickPickItems.ts | 3 +- src/quickpicks/gitQuickPickItems.ts | 2 +- src/quickpicks/quickPicksItems.ts | 3 +- src/quickpicks/referencePicker.ts | 2 +- src/quickpicks/remoteProviderPicker.ts | 10 +- src/quickpicks/repositoryPicker.ts | 2 +- src/statusbar/statusBarController.ts | 3 +- src/system/date.ts | 10 +- src/system/string.ts | 12 +- src/terminal/linkProvider.ts | 2 +- src/trackers/documentTracker.ts | 2 +- src/trackers/gitDocumentTracker.ts | 2 +- src/trackers/gitLineTracker.ts | 2 +- src/trackers/trackedDocument.ts | 2 +- src/views/branchesView.ts | 4 +- src/views/commitsView.ts | 4 +- src/views/contributorsView.ts | 2 +- src/views/nodes/autolinkedItemNode.ts | 2 +- src/views/nodes/autolinkedItemsNode.ts | 3 +- src/views/nodes/branchNode.ts | 4 +- src/views/nodes/branchTrackingStatusFilesNode.ts | 2 +- src/views/nodes/branchTrackingStatusNode.ts | 2 +- src/views/nodes/branchesNode.ts | 2 +- src/views/nodes/commitFileNode.ts | 3 +- src/views/nodes/commitNode.ts | 3 +- src/views/nodes/compareBranchNode.ts | 2 +- src/views/nodes/compareResultsNode.ts | 4 +- src/views/nodes/contributorNode.ts | 2 +- src/views/nodes/contributorsNode.ts | 2 +- src/views/nodes/fileHistoryNode.ts | 7 +- src/views/nodes/fileHistoryTrackerNode.ts | 2 +- src/views/nodes/fileRevisionAsCommitNode.ts | 10 +- src/views/nodes/helpers.ts | 2 +- src/views/nodes/lineHistoryNode.ts | 4 +- src/views/nodes/lineHistoryTrackerNode.ts | 2 +- src/views/nodes/mergeConflictCurrentChangesNode.ts | 3 +- src/views/nodes/mergeConflictFileNode.ts | 3 +- .../nodes/mergeConflictIncomingChangesNode.ts | 3 +- src/views/nodes/mergeStatusNode.ts | 2 +- src/views/nodes/pullRequestNode.ts | 2 +- src/views/nodes/rebaseStatusNode.ts | 6 +- src/views/nodes/reflogNode.ts | 2 +- src/views/nodes/reflogRecordNode.ts | 2 +- src/views/nodes/remoteNode.ts | 2 +- src/views/nodes/remotesNode.ts | 2 +- src/views/nodes/repositoryNode.ts | 4 +- src/views/nodes/resultsCommitsNode.ts | 2 +- src/views/nodes/resultsFileNode.ts | 3 +- src/views/nodes/resultsFilesNode.ts | 2 +- src/views/nodes/searchResultsNode.ts | 5 +- src/views/nodes/stashFileNode.ts | 2 +- src/views/nodes/stashNode.ts | 3 +- src/views/nodes/stashesNode.ts | 2 +- src/views/nodes/statusFileNode.ts | 3 +- src/views/nodes/statusFilesNode.ts | 4 +- src/views/nodes/tagNode.ts | 2 +- src/views/nodes/tagsNode.ts | 2 +- src/views/nodes/viewNode.ts | 6 +- src/views/remotesView.ts | 4 +- src/views/repositoriesView.ts | 12 +- src/views/searchAndCompareView.ts | 3 +- src/views/stashesView.ts | 4 +- src/views/tagsView.ts | 4 +- src/views/viewCommands.ts | 2 +- src/views/viewDecorationProvider.ts | 2 +- src/vsls/guest.ts | 3 +- src/vsls/host.ts | 2 +- src/vsls/protocol.ts | 2 +- src/webviews/rebaseEditor.ts | 6 +- src/webviews/webviewBase.ts | 7 +- tsconfig.browser.json | 3 +- webpack.config.js | 35 +- yarn.lock | 517 +-- 197 files changed, 6658 insertions(+), 6847 deletions(-) create mode 100644 src/@types/md5.js.d.ts create mode 100644 src/env/browser/crypto.ts create mode 100644 src/env/browser/git.ts create mode 100644 src/env/browser/platform.ts create mode 100644 src/env/node/crypto.ts create mode 100644 src/env/node/git.ts create mode 100644 src/env/node/git/git.ts create mode 100644 src/env/node/git/localGitProvider.ts create mode 100644 src/env/node/git/locator.ts create mode 100644 src/env/node/git/shell.ts create mode 100644 src/env/node/platform.ts create mode 100644 src/git/commandOptions.ts create mode 100644 src/git/formatters.ts delete mode 100644 src/git/formatters/formatters.ts delete mode 100644 src/git/git.ts delete mode 100644 src/git/locator.ts create mode 100644 src/git/models.ts delete mode 100644 src/git/models/models.ts create mode 100644 src/git/models/reference.ts create mode 100644 src/git/parsers.ts delete mode 100644 src/git/parsers/parsers.ts delete mode 100644 src/git/providers/localGitProvider.ts delete mode 100644 src/git/shell.ts diff --git a/.eslintrc.browser.json b/.eslintrc.browser.json index cd1ce88..747fa31 100644 --- a/.eslintrc.browser.json +++ b/.eslintrc.browser.json @@ -3,7 +3,7 @@ "env": { "worker": true }, - "ignorePatterns": ["src/webviews/apps/**/*", "src/env/node/*"], + "ignorePatterns": ["src/webviews/apps/**/*", "src/env/node/**/*"], "parserOptions": { "project": "tsconfig.browser.json" } diff --git a/package.json b/package.json index 53d601a..687df62 100644 --- a/package.json +++ b/package.json @@ -10134,10 +10134,10 @@ "@octokit/graphql": "4.8.0", "@vscode/codicons": "0.0.27", "chroma-js": "2.1.2", - "crypto-browserify": "3.12.0", "dayjs": "1.10.7", "iconv-lite": "0.6.3", "lodash-es": "4.17.21", + "md5.js": "1.3.5", "node-fetch": "3.0.0", "path-browserify": "1.0.1", "sortablejs": "1.14.0" @@ -10157,7 +10157,7 @@ "cross-env": "7.0.3", "csp-html-webpack-plugin": "5.1.0", "css-loader": "6.5.1", - "esbuild": "0.14.7", + "esbuild": "0.14.8", "esbuild-loader": "2.18.0", "eslint": "8.5.0", "eslint-cli": "1.1.1", @@ -10185,7 +10185,7 @@ "webpack-cli": "4.9.1" }, "resolutions": { - "esbuild": "0.14.7", + "esbuild": "0.14.8", "node-fetch": "2.6.1", "semver-regex": "3.1.3", "trim-newlines": "4.0.2" diff --git a/src/@types/md5.js.d.ts b/src/@types/md5.js.d.ts new file mode 100644 index 0000000..39670ba --- /dev/null +++ b/src/@types/md5.js.d.ts @@ -0,0 +1,13 @@ +/// + +declare module 'md5.js' { + import type { Hash } from 'crypto'; + + export = MD5; + + interface MD5 extends Hash { + new (): MD5; + } + + declare const MD5: MD5; +} diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 60fb8fa..c05ab4d 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -15,7 +15,8 @@ import { HeatmapLocations } from '../config'; import { Config, configuration } from '../configuration'; import { Colors, GlyphChars } from '../constants'; import { Container } from '../container'; -import { CommitFormatOptions, CommitFormatter, GitCommit } from '../git/git'; +import { CommitFormatOptions, CommitFormatter } from '../git/formatters'; +import { GitCommit } from '../git/models'; import { Strings } from '../system'; import { toRgba } from '../webviews/apps/shared/colors'; diff --git a/src/annotations/autolinks.ts b/src/annotations/autolinks.ts index 182c9fe..0382621 100644 --- a/src/annotations/autolinks.ts +++ b/src/annotations/autolinks.ts @@ -3,7 +3,7 @@ import { ConfigurationChangeEvent, Disposable } from 'vscode'; import { AutolinkReference, configuration } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitRemote, IssueOrPullRequest } from '../git/git'; +import { GitRemote, IssueOrPullRequest } from '../git/models'; import { Logger } from '../logger'; import { Dates, debug, Encoding, Iterables, Promises, Strings } from '../system'; diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 628ab82..4acfc23 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -2,8 +2,8 @@ import { CancellationToken, Disposable, Hover, languages, Position, Range, TextDocument, TextEditor } from 'vscode'; import { FileAnnotationType } from '../config'; import { Container } from '../container'; -import { GitBlame, GitBlameCommit, GitCommit } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitBlame, GitBlameCommit, GitCommit } from '../git/models'; import { Hovers } from '../hovers/hovers'; import { log } from '../system'; import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker'; diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index b54b939..3b862fa 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -3,7 +3,8 @@ import { DecorationOptions, Range, TextEditor, ThemableDecorationAttachmentRende import { FileAnnotationType, GravatarDefaultStyle } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { CommitFormatOptions, CommitFormatter, GitBlame, GitBlameCommit } from '../git/git'; +import { CommitFormatOptions, CommitFormatter } from '../git/formatters'; +import { GitBlame, GitBlameCommit } from '../git/models'; import { Logger } from '../logger'; import { Arrays, Iterables, log, Stopwatch, Strings } from '../system'; import { GitDocumentState } from '../trackers/gitDocumentTracker'; diff --git a/src/annotations/gutterChangesAnnotationProvider.ts b/src/annotations/gutterChangesAnnotationProvider.ts index 8d74b3c..66709a2 100644 --- a/src/annotations/gutterChangesAnnotationProvider.ts +++ b/src/annotations/gutterChangesAnnotationProvider.ts @@ -15,7 +15,7 @@ import { } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { Container } from '../container'; -import { GitDiff, GitLogCommit } from '../git/git'; +import { GitDiff, GitLogCommit } from '../git/models'; import { Hovers } from '../hovers/hovers'; import { Logger } from '../logger'; import { log, Stopwatch } from '../system'; diff --git a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts index 5c2b972..10848dc 100644 --- a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts +++ b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts @@ -2,7 +2,7 @@ import { Range, TextEditor, TextEditorDecorationType } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { Container } from '../container'; -import { GitBlameCommit } from '../git/git'; +import { GitBlameCommit } from '../git/models'; import { Logger } from '../logger'; import { log, Stopwatch } from '../system'; import { GitDocumentState } from '../trackers/gitDocumentTracker'; diff --git a/src/annotations/lineAnnotationController.ts b/src/annotations/lineAnnotationController.ts index f7fa166..9ec4597 100644 --- a/src/annotations/lineAnnotationController.ts +++ b/src/annotations/lineAnnotationController.ts @@ -14,7 +14,9 @@ import { import { configuration } from '../configuration'; import { GlyphChars, isTextEditor } from '../constants'; import { Container } from '../container'; -import { Authentication, CommitFormatter, GitBlameCommit, PullRequest } from '../git/git'; +import { CommitFormatter } from '../git/formatters'; +import { GitBlameCommit, PullRequest } from '../git/models'; +import { Authentication } from '../git/remotes/provider'; import { LogCorrelationContext, Logger } from '../logger'; import { debug, Iterables, log, Promises } from '../system'; import { LinesChangeEvent, LineSelection } from '../trackers/gitLineTracker'; diff --git a/src/avatars.ts b/src/avatars.ts index cd2f024..6a7f362 100644 --- a/src/avatars.ts +++ b/src/avatars.ts @@ -3,7 +3,7 @@ import { EventEmitter, Uri } from 'vscode'; import { GravatarDefaultStyle } from './config'; import { GlobalState } from './constants'; import { Container } from './container'; -import { GitRevisionReference } from './git/git'; +import { GitRevisionReference } from './git/models'; import { Dates, Functions, Iterables, Strings } from './system'; import { ContactPresenceStatus } from './vsls/vsls'; diff --git a/src/codelens/codeLensProvider.ts b/src/codelens/codeLensProvider.ts index ea2fd8b..6b3dbad 100644 --- a/src/codelens/codeLensProvider.ts +++ b/src/codelens/codeLensProvider.ts @@ -38,8 +38,9 @@ import { } from '../configuration'; import { BuiltInCommands, DocumentSchemes } from '../constants'; import { Container } from '../container'; -import { GitBlame, GitBlameLines, GitCommit, RemoteResourceType } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitBlame, GitBlameLines, GitCommit } from '../git/models'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { Functions, Iterables } from '../system'; diff --git a/src/commands/common.ts b/src/commands/common.ts index f02d910..7709423 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -19,8 +19,17 @@ import { import type { Action, ActionContext } from '../api/gitlens'; import { BuiltInCommands, DocumentSchemes, ImageMimetypes } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitCommit, GitContributor, GitFile, GitReference, GitRemote, GitTag, Repository } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { + GitBranch, + GitCommit, + GitContributor, + GitFile, + GitReference, + GitRemote, + GitTag, + Repository, +} from '../git/models'; import { Logger } from '../logger'; import { CommandQuickPickItem, RepositoryPicker } from '../quickpicks'; import { ViewNode, ViewRefNode } from '../views/nodes'; diff --git a/src/commands/createPullRequestOnRemote.ts b/src/commands/createPullRequestOnRemote.ts index 022ec19..4cfda0a 100644 --- a/src/commands/createPullRequestOnRemote.ts +++ b/src/commands/createPullRequestOnRemote.ts @@ -1,6 +1,7 @@ 'use strict'; import { Container } from '../container'; -import { GitRemote, RemoteProvider, RemoteResource, RemoteResourceType } from '../git/git'; +import { GitRemote } from '../git/models'; +import { RemoteProvider, RemoteResource, RemoteResourceType } from '../git/remotes/provider'; import { Command, command, Commands, executeCommand } from './common'; import { OpenOnRemoteCommandArgs } from './openOnRemote'; diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index 05c1da0..52f5e36 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { Container } from '../container'; -import { GitCommit } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitCommit } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index d4814a8..a81e155 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitCommit, GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; diff --git a/src/commands/diffWith.ts b/src/commands/diffWith.ts index ac92781..905e740 100644 --- a/src/commands/diffWith.ts +++ b/src/commands/diffWith.ts @@ -3,8 +3,8 @@ import * as paths from 'path'; import { commands, Range, TextDocumentShowOptions, Uri, ViewColumn } from 'vscode'; import { BuiltInCommands, GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitCommit, GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { command, Command, Commands } from './common'; diff --git a/src/commands/diffWithNext.ts b/src/commands/diffWithNext.ts index 470cdb5..1fdd82b 100644 --- a/src/commands/diffWithNext.ts +++ b/src/commands/diffWithNext.ts @@ -1,8 +1,8 @@ 'use strict'; import { Range, TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { Container } from '../container'; -import { GitLogCommit } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitLogCommit } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, command, CommandContext, Commands, executeCommand, getCommandUri } from './common'; diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index 818612b..71a187a 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitCommit, GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { diff --git a/src/commands/diffWithRevision.ts b/src/commands/diffWithRevision.ts index 2dd9926..2bf8e88 100644 --- a/src/commands/diffWithRevision.ts +++ b/src/commands/diffWithRevision.ts @@ -2,8 +2,8 @@ import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; -import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { CommandQuickPickItem, CommitPicker } from '../quickpicks'; diff --git a/src/commands/diffWithRevisionFrom.ts b/src/commands/diffWithRevisionFrom.ts index e4aa193..ce437ed 100644 --- a/src/commands/diffWithRevisionFrom.ts +++ b/src/commands/diffWithRevisionFrom.ts @@ -3,8 +3,8 @@ import * as paths from 'path'; import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; -import { GitReference, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitReference, GitRevision } from '../git/models'; import { Messages } from '../messages'; import { ReferencePicker, StashPicker } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index deb84e7..ebe5e85 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, command, Commands, executeCommand, getCommandUri } from './common'; diff --git a/src/commands/externalDiff.ts b/src/commands/externalDiff.ts index 5f018a6..ace9cab 100644 --- a/src/commands/externalDiff.ts +++ b/src/commands/externalDiff.ts @@ -3,8 +3,8 @@ import { env, SourceControlResourceState, Uri, window } from 'vscode'; import { ScmResource } from '../@types/vscode.git.resources'; import { ScmResourceGroupType, ScmStatus } from '../@types/vscode.git.resources.enums'; import { Container } from '../container'; -import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { Arrays } from '../system'; diff --git a/src/commands/git/branch.ts b/src/commands/git/branch.ts index 5993114..b16d64d 100644 --- a/src/commands/git/branch.ts +++ b/src/commands/git/branch.ts @@ -1,7 +1,7 @@ 'use strict'; import { QuickInputButtons } from 'vscode'; import { Container } from '../../container'; -import { GitBranchReference, GitReference, Repository } from '../../git/git'; +import { GitBranchReference, GitReference, Repository } from '../../git/models'; import { FlagsQuickPickItem, QuickPickItemOfT } from '../../quickpicks'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/cherry-pick.ts b/src/commands/git/cherry-pick.ts index f99ac7a..6db2f35 100644 --- a/src/commands/git/cherry-pick.ts +++ b/src/commands/git/cherry-pick.ts @@ -1,6 +1,6 @@ 'use strict'; import { Container } from '../../container'; -import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/git'; +import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/models'; import { FlagsQuickPickItem } from '../../quickpicks'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { diff --git a/src/commands/git/coauthors.ts b/src/commands/git/coauthors.ts index c5bbaea..e8a6817 100644 --- a/src/commands/git/coauthors.ts +++ b/src/commands/git/coauthors.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands } from 'vscode'; import { Container } from '../../container'; -import { GitContributor, Repository } from '../../git/git'; +import { GitContributor, Repository } from '../../git/models'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { diff --git a/src/commands/git/fetch.ts b/src/commands/git/fetch.ts index d4b0806..b967a4d 100644 --- a/src/commands/git/fetch.ts +++ b/src/commands/git/fetch.ts @@ -1,7 +1,7 @@ 'use strict'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitBranchReference, GitReference, Repository } from '../../git/git'; +import { GitBranchReference, GitReference, Repository } from '../../git/models'; import { FlagsQuickPickItem } from '../../quickpicks'; import { Arrays, Dates, Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/log.ts b/src/commands/git/log.ts index e76200b..58178af 100644 --- a/src/commands/git/log.ts +++ b/src/commands/git/log.ts @@ -1,8 +1,8 @@ 'use strict'; import { GlyphChars, quickPickTitleMaxChars } from '../../constants'; import { Container } from '../../container'; -import { GitLog, GitLogCommit, GitReference, Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitLog, GitLogCommit, GitReference, Repository } from '../../git/models'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { GitCommandsCommand } from '../gitCommands'; diff --git a/src/commands/git/merge.ts b/src/commands/git/merge.ts index d4dabca..06a411d 100644 --- a/src/commands/git/merge.ts +++ b/src/commands/git/merge.ts @@ -1,6 +1,6 @@ 'use strict'; import { Container } from '../../container'; -import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/git'; +import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/models'; import { Directive, DirectiveQuickPickItem, FlagsQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/pull.ts b/src/commands/git/pull.ts index 010da08..059ed62 100644 --- a/src/commands/git/pull.ts +++ b/src/commands/git/pull.ts @@ -1,7 +1,7 @@ 'use strict'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitBranch, GitBranchReference, GitReference, Repository } from '../../git/git'; +import { GitBranch, GitBranchReference, GitReference, Repository } from '../../git/models'; import { Directive, DirectiveQuickPickItem, FlagsQuickPickItem } from '../../quickpicks'; import { Arrays, Dates, Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/push.ts b/src/commands/git/push.ts index 7b3f3d5..17f6b01 100644 --- a/src/commands/git/push.ts +++ b/src/commands/git/push.ts @@ -2,7 +2,7 @@ import { configuration } from '../../configuration'; import { BuiltInGitConfiguration, GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitBranch, GitBranchReference, GitReference, Repository } from '../../git/git'; +import { GitBranch, GitBranchReference, GitReference, Repository } from '../../git/models'; import { Directive, DirectiveQuickPickItem, FlagsQuickPickItem } from '../../quickpicks'; import { Arrays, Dates, Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/rebase.ts b/src/commands/git/rebase.ts index 2403307..c07be2c 100644 --- a/src/commands/git/rebase.ts +++ b/src/commands/git/rebase.ts @@ -1,7 +1,7 @@ 'use strict'; import { env } from 'vscode'; import { Container } from '../../container'; -import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/git'; +import { GitBranch, GitLog, GitReference, GitRevision, Repository } from '../../git/models'; import { Directive, DirectiveQuickPickItem, FlagsQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/reset.ts b/src/commands/git/reset.ts index 9c78b8e..7ea4f98 100644 --- a/src/commands/git/reset.ts +++ b/src/commands/git/reset.ts @@ -1,6 +1,6 @@ 'use strict'; import { Container } from '../../container'; -import { GitBranch, GitLog, GitReference, GitRevisionReference, Repository } from '../../git/git'; +import { GitBranch, GitLog, GitReference, GitRevisionReference, Repository } from '../../git/models'; import { FlagsQuickPickItem } from '../../quickpicks'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { diff --git a/src/commands/git/revert.ts b/src/commands/git/revert.ts index 90950b3..a3476b5 100644 --- a/src/commands/git/revert.ts +++ b/src/commands/git/revert.ts @@ -1,6 +1,6 @@ 'use strict'; import { Container } from '../../container'; -import { GitBranch, GitLog, GitReference, GitRevisionReference, Repository } from '../../git/git'; +import { GitBranch, GitLog, GitReference, GitRevisionReference, Repository } from '../../git/models'; import { FlagsQuickPickItem } from '../../quickpicks'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { diff --git a/src/commands/git/search.ts b/src/commands/git/search.ts index d6bc67f..7b2b22f 100644 --- a/src/commands/git/search.ts +++ b/src/commands/git/search.ts @@ -1,7 +1,8 @@ 'use strict'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitLog, GitLogCommit, Repository, SearchOperators, searchOperators, SearchPattern } from '../../git/git'; +import { GitLog, GitLogCommit, Repository } from '../../git/models'; +import { searchOperators, SearchOperators, SearchPattern } from '../../git/search'; import { ActionQuickPickItem, QuickPickItemOfT } from '../../quickpicks'; import { Strings } from '../../system'; import { SearchResultsNode } from '../../views/nodes'; diff --git a/src/commands/git/show.ts b/src/commands/git/show.ts index 2526f5c..be45f9b 100644 --- a/src/commands/git/show.ts +++ b/src/commands/git/show.ts @@ -1,6 +1,6 @@ 'use strict'; import { Container } from '../../container'; -import { GitAuthor, GitLogCommit, GitRevisionReference, GitStashCommit, Repository } from '../../git/git'; +import { GitAuthor, GitLogCommit, GitRevisionReference, GitStashCommit, Repository } from '../../git/models'; import { CommandQuickPickItem, CommitFilesQuickPickItem, GitCommandQuickPickItem } from '../../quickpicks'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index 0b26b56..0c3f054 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -2,8 +2,9 @@ import { QuickInputButtons, QuickPickItem, Uri, window } from 'vscode'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitReference, GitStashCommit, GitStashReference, Repository, RunError } from '../../git/git'; +import { StashApplyError, StashApplyErrorReason } from '../../git/errors'; import { GitUri } from '../../git/gitUri'; +import { GitReference, GitStashCommit, GitStashReference, Repository } from '../../git/models'; import { Logger } from '../../logger'; import { Messages } from '../../messages'; import { FlagsQuickPickItem, QuickPickItemOfT } from '../../quickpicks'; @@ -291,32 +292,14 @@ export class StashGitCommand extends QuickCommand { } catch (ex) { Logger.error(ex, context.title); - if (ex instanceof Error) { - const msg: string = ex.message ?? ''; - if (msg.includes('Your local changes to the following files would be overwritten by merge')) { + if (ex instanceof StashApplyError) { + if (ex.reason === StashApplyErrorReason.WorkingChanges) { void window.showWarningMessage( 'Unable to apply stash. Your working tree changes would be overwritten. Please commit or stash your changes before trying again', ); - - return; - } - - if ( - (msg.includes('Auto-merging') && msg.includes('CONFLICT')) || - (ex instanceof RunError && - ((ex.stdout.includes('Auto-merging') && ex.stdout.includes('CONFLICT')) || - ex.stdout.includes('needs merge'))) - ) { - void window.showInformationMessage('Stash applied with conflicts'); - - return; + } else { + void Messages.showGenericErrorMessage(ex.message); } - - void Messages.showGenericErrorMessage( - `Unable to apply stash \u2014 ${msg.trim().replace(/\n+?/g, '; ')}`, - ); - - return; } } } diff --git a/src/commands/git/status.ts b/src/commands/git/status.ts index c985e71..c21b51f 100644 --- a/src/commands/git/status.ts +++ b/src/commands/git/status.ts @@ -1,7 +1,7 @@ 'use strict'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitReference, GitStatus, Repository } from '../../git/git'; +import { GitReference, GitStatus, Repository } from '../../git/models'; import { CommandQuickPickItem, GitCommandQuickPickItem } from '../../quickpicks'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/git/switch.ts b/src/commands/git/switch.ts index fa4a930..be46d2f 100644 --- a/src/commands/git/switch.ts +++ b/src/commands/git/switch.ts @@ -2,7 +2,7 @@ import { ProgressLocation, QuickPickItem, window } from 'vscode'; import { BranchSorting } from '../../config'; import { Container } from '../../container'; -import { GitReference, Repository } from '../../git/git'; +import { GitReference, Repository } from '../../git/models'; import { Arrays } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; import { diff --git a/src/commands/git/tag.ts b/src/commands/git/tag.ts index 2566bd8..f04c816 100644 --- a/src/commands/git/tag.ts +++ b/src/commands/git/tag.ts @@ -1,7 +1,7 @@ 'use strict'; import { QuickInputButtons, QuickPickItem } from 'vscode'; import { Container } from '../../container'; -import { GitReference, GitTagReference, Repository } from '../../git/git'; +import { GitReference, GitTagReference, Repository } from '../../git/models'; import { FlagsQuickPickItem, QuickPickItemOfT } from '../../quickpicks'; import { Strings } from '../../system'; import { ViewsWithRepositoryFolders } from '../../views/viewBase'; diff --git a/src/commands/gitCommands.actions.ts b/src/commands/gitCommands.actions.ts index 5430c97..20c9b87 100644 --- a/src/commands/gitCommands.actions.ts +++ b/src/commands/gitCommands.actions.ts @@ -13,6 +13,7 @@ import { } from '../commands'; import { FileAnnotationType } from '../configuration'; import { Container } from '../container'; +import { GitUri } from '../git/gitUri'; import { GitBranchReference, GitContributor, @@ -25,8 +26,7 @@ import { GitStashReference, GitTagReference, Repository, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { RepositoryPicker } from '../quickpicks'; import { ViewsWithRepositoryFolders } from '../views/viewBase'; import { ResetGitCommandArgs } from './git/reset'; diff --git a/src/commands/openBranchOnRemote.ts b/src/commands/openBranchOnRemote.ts index 87b6535..b49879e 100644 --- a/src/commands/openBranchOnRemote.ts +++ b/src/commands/openBranchOnRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; -import { RemoteResourceType } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { CommandQuickPickItem, ReferencePicker, ReferencesQuickPickIncludes } from '../quickpicks'; import { diff --git a/src/commands/openBranchesOnRemote.ts b/src/commands/openBranchesOnRemote.ts index 9569bd9..77a523f 100644 --- a/src/commands/openBranchesOnRemote.ts +++ b/src/commands/openBranchesOnRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; -import { RemoteResourceType } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { ActiveEditorCommand, diff --git a/src/commands/openCommitOnRemote.ts b/src/commands/openCommitOnRemote.ts index 8b85ed9..1b15831 100644 --- a/src/commands/openCommitOnRemote.ts +++ b/src/commands/openCommitOnRemote.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { RemoteResourceType } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { diff --git a/src/commands/openComparisonOnRemote.ts b/src/commands/openComparisonOnRemote.ts index cbcf689..094a581 100644 --- a/src/commands/openComparisonOnRemote.ts +++ b/src/commands/openComparisonOnRemote.ts @@ -1,6 +1,6 @@ 'use strict'; import { window } from 'vscode'; -import { RemoteResourceType } from '../git/git'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { ResultsCommitsNode } from '../views/nodes'; import { Command, command, CommandContext, Commands, executeCommand } from './common'; diff --git a/src/commands/openFileAtRevision.ts b/src/commands/openFileAtRevision.ts index 1ed0d2d..9e2bb76 100644 --- a/src/commands/openFileAtRevision.ts +++ b/src/commands/openFileAtRevision.ts @@ -3,8 +3,8 @@ import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; -import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitRevision } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { CommandQuickPickItem, CommitPicker } from '../quickpicks'; diff --git a/src/commands/openFileAtRevisionFrom.ts b/src/commands/openFileAtRevisionFrom.ts index 5995edb..dc06857 100644 --- a/src/commands/openFileAtRevisionFrom.ts +++ b/src/commands/openFileAtRevisionFrom.ts @@ -4,8 +4,8 @@ import { TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; -import { GitReference } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitReference } from '../git/models'; import { Messages } from '../messages'; import { ReferencePicker, StashPicker } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/openFileOnRemote.ts b/src/commands/openFileOnRemote.ts index b15b8d6..38091a6 100644 --- a/src/commands/openFileOnRemote.ts +++ b/src/commands/openFileOnRemote.ts @@ -4,8 +4,9 @@ import { UriComparer } from '../comparers'; import { BranchSorting, TagSorting } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitRevision, RemoteResourceType } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitBranch, GitRevision } from '../git/models'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { ReferencePicker } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/openOnRemote.ts b/src/commands/openOnRemote.ts index 28c33e4..b752e14 100644 --- a/src/commands/openOnRemote.ts +++ b/src/commands/openOnRemote.ts @@ -1,7 +1,8 @@ 'use strict'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitRemote, GitRevision, RemoteProvider, RemoteResource, RemoteResourceType } from '../git/git'; +import { GitRemote, GitRevision } from '../git/models'; +import { RemoteProvider, RemoteResource, RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { RemoteProviderPicker } from '../quickpicks'; diff --git a/src/commands/openRepoOnRemote.ts b/src/commands/openRepoOnRemote.ts index 29330fc..3dd8bf5 100644 --- a/src/commands/openRepoOnRemote.ts +++ b/src/commands/openRepoOnRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; -import { RemoteResourceType } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { RemoteResourceType } from '../git/remotes/provider'; import { Logger } from '../logger'; import { ActiveEditorCommand, diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index 118dfd0..dc8c2da 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -3,6 +3,8 @@ import { QuickInputButton, QuickPick } from 'vscode'; import { BranchSorting, configuration, TagSorting } from '../configuration'; import { GlyphChars, quickPickTitleMaxChars } from '../constants'; import { Container } from '../container'; +import { PagedResult } from '../git/gitProvider'; +import { GitUri } from '../git/gitUri'; import { BranchSortOptions, GitBranch, @@ -19,14 +21,11 @@ import { GitStatus, GitTag, GitTagReference, - RemoteProvider, - RemoteResourceType, Repository, - SearchPattern, TagSortOptions, -} from '../git/git'; -import { PagedResult } from '../git/gitProvider'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; +import { RemoteProvider, RemoteResourceType } from '../git/remotes/provider'; +import { SearchPattern } from '../git/search'; import { BranchQuickPickItem, CommandQuickPickItem, diff --git a/src/commands/remoteProviders.ts b/src/commands/remoteProviders.ts index 4a94346..0c61b26 100644 --- a/src/commands/remoteProviders.ts +++ b/src/commands/remoteProviders.ts @@ -1,6 +1,7 @@ 'use strict'; import { Container } from '../container'; -import { GitCommit, GitRemote, Repository, RichRemoteProvider } from '../git/git'; +import { GitCommit, GitRemote, Repository } from '../git/models'; +import { RichRemoteProvider } from '../git/remotes/provider'; import { RepositoryPicker } from '../quickpicks/repositoryPicker'; import { Iterables } from '../system'; import { command, Command, CommandContext, Commands, isCommandContextViewNodeHasRemote } from './common'; diff --git a/src/commands/searchCommits.ts b/src/commands/searchCommits.ts index 4232e4e..7c49b90 100644 --- a/src/commands/searchCommits.ts +++ b/src/commands/searchCommits.ts @@ -1,7 +1,7 @@ 'use strict'; import { executeGitCommand } from '../commands'; import { Container } from '../container'; -import { SearchPattern } from '../git/git'; +import { SearchPattern } from '../git/search'; import { SearchResultsNode } from '../views/nodes'; import { Command, command, CommandContext, Commands, isCommandContextViewNodeHasRepository } from './common'; diff --git a/src/commands/showCommitsInView.ts b/src/commands/showCommitsInView.ts index 47c0235..0fec06f 100644 --- a/src/commands/showCommitsInView.ts +++ b/src/commands/showCommitsInView.ts @@ -2,8 +2,8 @@ import { TextEditor, Uri } from 'vscode'; import { executeGitCommand } from '../commands'; import { Container } from '../container'; -import { SearchPattern } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { SearchPattern } from '../git/search'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { Iterables } from '../system'; diff --git a/src/commands/showQuickBranchHistory.ts b/src/commands/showQuickBranchHistory.ts index 19297b3..0259fb1 100644 --- a/src/commands/showQuickBranchHistory.ts +++ b/src/commands/showQuickBranchHistory.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, Uri } from 'vscode'; import { Container } from '../container'; -import { GitReference } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitReference } from '../git/models'; import { ActiveEditorCachedCommand, command, CommandContext, Commands, getCommandUri } from './common'; import { executeGitCommand } from './gitCommands'; diff --git a/src/commands/showQuickCommit.ts b/src/commands/showQuickCommit.ts index 6680546..e0f0fb8 100644 --- a/src/commands/showQuickCommit.ts +++ b/src/commands/showQuickCommit.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, Uri } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitLog, GitLogCommit } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitCommit, GitLog, GitLogCommit } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { diff --git a/src/commands/showQuickCommitFile.ts b/src/commands/showQuickCommitFile.ts index e45aa37..573c124 100644 --- a/src/commands/showQuickCommitFile.ts +++ b/src/commands/showQuickCommitFile.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitBlameCommit, GitCommit, GitLog, GitLogCommit } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitBlameCommit, GitCommit, GitLog, GitLogCommit } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 9f5b4b6..339fb87 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -1,8 +1,8 @@ 'use strict'; import { Range, TextEditor, Uri } from 'vscode'; import { Container } from '../container'; -import { GitBranch, GitLog, GitReference, GitTag } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitBranch, GitLog, GitReference, GitTag } from '../git/models'; import { CommandQuickPickItem } from '../quickpicks'; import { ActiveEditorCachedCommand, command, CommandContext, Commands, getCommandUri } from './common'; import { executeGitCommand } from './gitCommands'; diff --git a/src/commands/stashApply.ts b/src/commands/stashApply.ts index 60cef52..92fb8ab 100644 --- a/src/commands/stashApply.ts +++ b/src/commands/stashApply.ts @@ -1,6 +1,6 @@ 'use strict'; import { GitActions } from '../commands'; -import { GitStashCommit, GitStashReference } from '../git/git'; +import { GitStashCommit, GitStashReference } from '../git/models'; import { CommandQuickPickItem } from '../quickpicks'; import { command, diff --git a/src/constants.ts b/src/constants.ts index 37b4071..f1583c5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, TextDocument, TextEditor, window } from 'vscode'; import { ViewShowBranchComparison } from './config'; -import { SearchPattern } from './git/git'; +import { SearchPattern } from './git/search'; export const quickPickTitleMaxChars = 80; diff --git a/src/container.ts b/src/container.ts index a85a3bf..e2403e3 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,14 +1,6 @@ 'use strict'; -import { - commands, - ConfigurationChangeEvent, - ConfigurationScope, - env, - Event, - EventEmitter, - ExtensionContext, - UIKind, -} from 'vscode'; +import { commands, ConfigurationChangeEvent, ConfigurationScope, Event, EventEmitter, ExtensionContext } from 'vscode'; +import { getSupportedGitProviders } from '@env/git'; import { Autolinks } from './annotations/autolinks'; import { FileAnnotationController } from './annotations/fileAnnotationController'; import { LineAnnotationController } from './annotations/lineAnnotationController'; @@ -24,8 +16,7 @@ import { FileAnnotationType, } from './configuration'; import { GitFileSystemProvider } from './git/fsProvider'; -import { GitProviderId, GitProviderService } from './git/gitProviderService'; -import { LocalGitProvider } from './git/providers/localGitProvider'; +import { GitProviderService } from './git/gitProviderService'; import { LineHoverController } from './hovers/lineHoverController'; import { Keyboard } from './keyboard'; import { Logger } from './logger'; @@ -155,8 +146,9 @@ export class Container { @log() private registerGitProviders() { - if (env.uiKind !== UIKind.Web) { - this._context.subscriptions.push(this._git.register(GitProviderId.Git, new LocalGitProvider(this))); + const providers = getSupportedGitProviders(this); + for (const provider of providers) { + this._context.subscriptions.push(this._git.register(provider.descriptor.id, provider)); } this._git.registrationComplete(); diff --git a/src/emojis.ts b/src/emojis.ts index ae77bd1..223c627 100644 --- a/src/emojis.ts +++ b/src/emojis.ts @@ -1,11 +1,8 @@ 'use strict'; -import * as emojis from './emojis.json'; +import emojis from './emojis.json'; -const emojiMap = (emojis as any).default as Record; const emojiRegex = /:([-+_a-z0-9]+):/g; export function emojify(message: string) { - return message.replace(emojiRegex, (s, code) => { - return emojiMap[code] || s; - }); + return message.replace(emojiRegex, (s, code) => (emojis as Record)[code] || s); } diff --git a/src/env/browser/crypto.ts b/src/env/browser/crypto.ts new file mode 100644 index 0000000..0c0358b --- /dev/null +++ b/src/env/browser/crypto.ts @@ -0,0 +1,10 @@ +import MD5 from 'md5.js'; +import { base64 } from './base64'; + +export function getNonce(): string { + return base64(globalThis.crypto.getRandomValues(new Uint8Array(16))); +} + +export function md5(data: string | Uint8Array, encoding: 'base64' | 'hex' = 'base64'): string { + return new MD5().update(data).digest(encoding); +} diff --git a/src/env/browser/git.ts b/src/env/browser/git.ts new file mode 100644 index 0000000..a0e76e8 --- /dev/null +++ b/src/env/browser/git.ts @@ -0,0 +1,11 @@ +import { Container } from '../../container'; +import { GitCommandOptions } from '../../git/commandOptions'; +import { GitProvider } from '../../git/gitProvider'; + +export function git(_options: GitCommandOptions, ..._args: any[]): Promise { + return Promise.resolve(''); +} + +export function getSupportedGitProviders(_container: Container): GitProvider[] { + return []; +} diff --git a/src/env/browser/platform.ts b/src/env/browser/platform.ts new file mode 100644 index 0000000..fd506e0 --- /dev/null +++ b/src/env/browser/platform.ts @@ -0,0 +1,5 @@ +export const isWeb = true; +export const isWindows = + (navigator as any)?.userAgentData?.platform === 'Windows' || + navigator.platform === 'Win32' || + navigator.platform === 'Win64'; diff --git a/src/env/node/crypto.ts b/src/env/node/crypto.ts new file mode 100644 index 0000000..3ab5663 --- /dev/null +++ b/src/env/node/crypto.ts @@ -0,0 +1,9 @@ +import { createHash, randomBytes } from 'crypto'; + +export function getNonce(): string { + return randomBytes(16).toString('base64'); +} + +export function md5(data: string | Uint8Array, encoding: 'base64' | 'hex' = 'base64'): string { + return createHash('md5').update(data).digest(encoding); +} diff --git a/src/env/node/git.ts b/src/env/node/git.ts new file mode 100644 index 0000000..fa496da --- /dev/null +++ b/src/env/node/git.ts @@ -0,0 +1,12 @@ +import { Container } from '../../container'; +import { GitProvider } from '../../git/gitProvider'; +import { LocalGitProvider } from './git/localGitProvider'; +import { isWeb } from './platform'; + +export { git } from './git/git'; + +export function getSupportedGitProviders(container: Container): GitProvider[] { + if (isWeb) return []; + + return [new LocalGitProvider(container)]; +} diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts new file mode 100644 index 0000000..8814fae --- /dev/null +++ b/src/env/node/git/git.ts @@ -0,0 +1,1544 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +'use strict'; +import * as paths from 'path'; +import { Uri, window, workspace } from 'vscode'; +import { hrtime } from '@env/hrtime'; +import { GlyphChars } from '../../../constants'; +import { Container } from '../../../container'; +import { GitCommandOptions, GitErrorHandling } from '../../../git/commandOptions'; +import { GitDiffFilter, GitRevision } from '../../../git/models'; +import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from '../../../git/parsers'; +import { Logger } from '../../../logger'; +import { Paths, Strings, Versions } from '../../../system'; +import { GitLocation } from './locator'; +import { fsExists, run, RunError, RunOptions } from './shell'; + +const emptyArray = Object.freeze([]) as unknown as any[]; +const emptyObj = Object.freeze({}); +const emptyStr = ''; +export const maxGitCliLength = 30000; + +const textDecoder = new TextDecoder('utf8'); + +// This is a root sha of all git repo's if using sha1 +const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; + +export const GitErrors = { + badRevision: /bad revision '(.*?)'/i, + noFastForward: /\(non-fast-forward\)/i, + noMergeBase: /no merge base/i, + notAValidObjectName: /Not a valid object name/i, + invalidLineCount: /file .+? has only \d+ lines/i, +}; + +const GitWarnings = { + notARepository: /Not a git repository/i, + outsideRepository: /is outside repository/i, + noPath: /no such path/i, + noCommits: /does not have any commits/i, + notFound: /Path '.*?' does not exist in/i, + foundButNotInRevision: /Path '.*?' exists on disk, but not in/i, + headNotABranch: /HEAD does not point to a branch/i, + noUpstream: /no upstream configured for branch '(.*?)'/i, + unknownRevision: + /ambiguous argument '.*?': unknown revision or path not in the working tree|not stored as a remote-tracking branch/i, + mustRunInWorkTree: /this operation must be run in a work tree/i, + patchWithConflicts: /Applied patch to '.*?' with conflicts/i, + noRemoteRepositorySpecified: /No remote repository specified\./i, + remoteConnectionError: /Could not read from remote repository/i, + notAGitCommand: /'.+' is not a git command/i, +}; + +// A map of running git commands -- avoids running duplicate overlaping commands +const pendingCommands = new Map>(); + +export async function git(options: GitCommandOptions, ...args: any[]): Promise { + if (Container.instance.vsls.isMaybeGuest) { + if (options.local !== true) { + const guest = await Container.instance.vsls.guest(); + if (guest !== undefined) { + return guest.git(options, ...args); + } + } else { + // Since we will have a live share path here, just blank it out + options.cwd = emptyStr; + } + } + + const start = hrtime(); + + const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options; + + const runOpts: RunOptions = { + ...opts, + encoding: (encoding ?? 'utf8') === 'utf8' ? 'utf8' : 'buffer', + // Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581 + // Shouldn't *really* be needed but better safe than sorry + env: { + ...process.env, + ...(options.env ?? emptyObj), + GCM_INTERACTIVE: 'NEVER', + GCM_PRESERVE_CREDS: 'TRUE', + LC_ALL: 'C', + }, + }; + + const gitCommand = `[${runOpts.cwd}] git ${args.join(' ')}`; + + const command = `${correlationKey !== undefined ? `${correlationKey}:` : emptyStr}${gitCommand}`; + + let waiting; + let promise = pendingCommands.get(command); + if (promise === undefined) { + waiting = false; + + // Fixes https://github.com/eamodio/vscode-gitlens/issues/73 & https://github.com/eamodio/vscode-gitlens/issues/161 + // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x + args.splice( + 0, + 0, + '-c', + 'core.quotepath=false', + '-c', + 'color.ui=false', + ...(configs !== undefined ? configs : emptyArray), + ); + + if (process.platform === 'win32') { + args.splice(0, 0, '-c', 'core.longpaths=true'); + } + + promise = run(await Git.path(), args, encoding ?? 'utf8', runOpts); + + pendingCommands.set(command, promise); + } else { + waiting = true; + Logger.debug(`${gitCommand} ${GlyphChars.Dot} waiting...`); + } + + let exception: Error | undefined; + try { + return (await promise) as TOut; + } catch (ex) { + exception = ex; + + switch (errorHandling) { + case GitErrorHandling.Ignore: + exception = undefined; + return emptyStr as TOut; + + case GitErrorHandling.Throw: + throw ex; + + default: { + const result = defaultExceptionHandler(ex, options.cwd, start); + exception = undefined; + return result as TOut; + } + } + } finally { + pendingCommands.delete(command); + + const duration = Strings.getDurationMilliseconds(start); + const elapsed = `${duration} ms ${waiting ? '(waited) ' : emptyStr}`; + if (exception != null) { + Logger.error( + '', + `[${runOpts.cwd}] Git ${(exception.message || exception.toString() || emptyStr) + .trim() + .replace(/fatal: /g, '') + .replace(/\r?\n|\r/g, ` ${GlyphChars.Dot} `)} ${GlyphChars.Dot} ${elapsed}`, + ); + } else if (duration > Logger.slowCallWarningThreshold) { + Logger.warn(`${gitCommand} ${GlyphChars.Dot} ${elapsed} (slow)`); + } else { + Logger.log(`${gitCommand} ${GlyphChars.Dot} ${elapsed}`); + } + Logger.logGitCommand( + `${gitCommand} ${GlyphChars.Dot} ${exception !== undefined ? 'FAILED ' : emptyStr}${elapsed}`, + duration, + exception, + ); + } +} + +function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [number, number]): string { + const msg = ex.message || ex.toString(); + if (msg != null && msg.length !== 0) { + for (const warning of Object.values(GitWarnings)) { + if (warning.test(msg)) { + const duration = start !== undefined ? `${Strings.getDurationMilliseconds(start)} ms` : emptyStr; + Logger.warn( + `[${cwd}] Git ${msg + .trim() + .replace(/fatal: /g, '') + .replace(/\r?\n|\r/g, ` ${GlyphChars.Dot} `)} ${GlyphChars.Dot} ${duration}`, + ); + return emptyStr; + } + } + + const match = GitErrors.badRevision.exec(msg); + if (match != null) { + const [, ref] = match; + + // Since looking up a ref with ^3 (e.g. looking for untracked files in a stash) can error on some versions of git just ignore it + if (ref?.endsWith('^3')) return emptyStr; + } + } + + throw ex; +} + +export namespace Git { + let gitLocator!: () => Promise; + export function setLocator(locator: () => Promise): void { + gitLocator = locator; + } + + export async function path(): Promise { + return (await gitLocator()).path; + } + + export async function version(): Promise { + return (await gitLocator()).version; + } + + export async function isAtLeastVersion(minimum: string): Promise { + const result = Versions.compare(Versions.fromString(await Git.version()), Versions.fromString(minimum)); + return result !== -1; + } + + // Git commands + + export function add(repoPath: string | undefined, pathspec: string) { + return git({ cwd: repoPath }, 'add', '-A', '--', pathspec); + } + + export function apply(repoPath: string | undefined, patch: string, options: { allowConflicts?: boolean } = {}) { + const params = ['apply', '--whitespace=warn']; + if (options.allowConflicts) { + params.push('-3'); + } + return git({ cwd: repoPath, stdin: patch }, ...params); + } + + const ignoreRevsFileMap = new Map(); + + export async function blame( + repoPath: string | undefined, + fileName: string, + ref?: string, + options: { args?: string[] | null; ignoreWhitespace?: boolean; startLine?: number; endLine?: number } = {}, + ) { + const [file, root] = Paths.splitPath(fileName, repoPath); + + const params = ['blame', '--root', '--incremental']; + + if (options.ignoreWhitespace) { + params.push('-w'); + } + if (options.startLine != null && options.endLine != null) { + params.push(`-L ${options.startLine},${options.endLine}`); + } + if (options.args != null) { + params.push(...options.args); + + const index = params.indexOf('--ignore-revs-file'); + if (index !== -1) { + // Ensure the version of Git supports the --ignore-revs-file flag, otherwise the blame will fail + let supported = await Git.isAtLeastVersion('2.23'); + if (supported) { + let ignoreRevsFile = params[index + 1]; + if (!paths.isAbsolute(ignoreRevsFile)) { + ignoreRevsFile = paths.join(repoPath ?? emptyStr, ignoreRevsFile); + } + + const exists = ignoreRevsFileMap.get(ignoreRevsFile); + if (exists !== undefined) { + supported = exists; + } else { + // Ensure the specified --ignore-revs-file exists, otherwise the blame will fail + try { + supported = await fsExists(ignoreRevsFile); + } catch { + supported = false; + } + + ignoreRevsFileMap.set(ignoreRevsFile, supported); + } + } + + if (!supported) { + params.splice(index, 2); + } + } + } + + let stdin; + if (ref) { + if (GitRevision.isUncommittedStaged(ref)) { + // Pipe the blame contents to stdin + params.push('--contents', '-'); + + // Get the file contents for the staged version using `:` + stdin = await Git.show(repoPath, fileName, ':'); + } else { + params.push(ref); + } + } + + return git({ cwd: root, stdin: stdin }, ...params, '--', file); + } + + export function blame__contents( + repoPath: string | undefined, + fileName: string, + contents: string, + options: { + args?: string[] | null; + correlationKey?: string; + ignoreWhitespace?: boolean; + startLine?: number; + endLine?: number; + } = {}, + ) { + const [file, root] = Paths.splitPath(fileName, repoPath); + + const params = ['blame', '--root', '--incremental']; + + if (options.ignoreWhitespace) { + params.push('-w'); + } + if (options.startLine != null && options.endLine != null) { + params.push(`-L ${options.startLine},${options.endLine}`); + } + if (options.args != null) { + params.push(...options.args); + } + + // Pipe the blame contents to stdin + params.push('--contents', '-'); + + return git( + { cwd: root, stdin: contents, correlationKey: options.correlationKey }, + ...params, + '--', + file, + ); + } + + export function branch__containsOrPointsAt( + repoPath: string, + ref: string, + { + mode = 'contains', + name = undefined, + remotes = false, + }: { mode?: 'contains' | 'pointsAt'; name?: string; remotes?: boolean } = {}, + ) { + const params = ['branch']; + if (remotes) { + params.push('-r'); + } + params.push(mode === 'pointsAt' ? `--points-at=${ref}` : `--contains=${ref}`, '--format=%(refname:short)'); + if (name != null) { + params.push(name); + } + + return git( + { cwd: repoPath, configs: ['-c', 'color.branch=false'], errors: GitErrorHandling.Ignore }, + ...params, + ); + } + + export function check_ignore(repoPath: string, ...files: string[]) { + return git( + { cwd: repoPath, errors: GitErrorHandling.Ignore, stdin: files.join('\0') }, + 'check-ignore', + '-z', + '--stdin', + ); + } + + export function check_mailmap(repoPath: string, author: string) { + return git({ cwd: repoPath, errors: GitErrorHandling.Ignore, local: true }, 'check-mailmap', author); + } + + export async function check_ref_format( + ref: string, + repoPath?: string, + options: { branch?: boolean } = { branch: true }, + ) { + const params = ['check-ref-format']; + if (options.branch) { + params.push('--branch'); + } else { + params.push('--normalize'); + } + + try { + const data = await git( + { cwd: repoPath ?? emptyStr, errors: GitErrorHandling.Throw, local: true }, + ...params, + ref, + ); + return Boolean(data.trim()); + } catch { + return false; + } + } + + export function checkout( + repoPath: string, + ref: string, + { createBranch, fileName }: { createBranch?: string; fileName?: string } = {}, + ) { + const params = ['checkout']; + if (createBranch) { + params.push('-b', createBranch, ref, '--'); + } else { + params.push(ref, '--'); + + if (fileName) { + [fileName, repoPath] = Paths.splitPath(fileName, repoPath); + + params.push(fileName); + } + } + + return git({ cwd: repoPath }, ...params); + } + + export async function config__get(key: string, repoPath?: string, options: { local?: boolean } = {}) { + const data = await git( + { cwd: repoPath ?? emptyStr, errors: GitErrorHandling.Ignore, local: options.local }, + 'config', + '--get', + key, + ); + return data.length === 0 ? undefined : data.trim(); + } + + export async function config__get_regex(pattern: string, repoPath?: string, options: { local?: boolean } = {}) { + const data = await git( + { cwd: repoPath ?? emptyStr, errors: GitErrorHandling.Ignore, local: options.local }, + 'config', + '--get-regex', + pattern, + ); + return data.length === 0 ? undefined : data.trim(); + } + + export async function diff( + repoPath: string, + fileName: string, + ref1?: string, + ref2?: string, + options: { + encoding?: string; + filters?: GitDiffFilter[]; + linesOfContext?: number; + renames?: boolean; + similarityThreshold?: number | null; + } = {}, + ): Promise { + const params = ['diff', '--no-ext-diff', '--minimal']; + + if (options.linesOfContext != null) { + params.push(`-U${options.linesOfContext}`); + } + + if (options.renames) { + params.push(`-M${options.similarityThreshold == null ? '' : `${options.similarityThreshold}%`}`); + } + + if (options.filters != null && options.filters.length !== 0) { + params.push(`--diff-filter=${options.filters.join(emptyStr)}`); + } + + if (ref1) { + // ^3 signals an untracked file in a stash and if we are trying to find its parent, use the root sha + if (ref1.endsWith('^3^')) { + ref1 = rootSha; + } + params.push(GitRevision.isUncommittedStaged(ref1) ? '--staged' : ref1); + } + if (ref2) { + params.push(GitRevision.isUncommittedStaged(ref2) ? '--staged' : ref2); + } + + try { + return await git( + { + cwd: repoPath, + configs: ['-c', 'color.diff=false'], + encoding: options.encoding, + }, + ...params, + '--', + fileName, + ); + } catch (ex) { + const match = GitErrors.badRevision.exec(ex.message); + if (match !== null) { + const [, ref] = match; + + // If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha + if (ref === ref1 && ref != null && ref.endsWith('^')) { + return Git.diff(repoPath, fileName, rootSha, ref2, options); + } + } + + throw ex; + } + } + + export async function diff__contents( + repoPath: string, + fileName: string, + ref: string, + contents: string, + options: { encoding?: string; filters?: GitDiffFilter[]; similarityThreshold?: number | null } = {}, + ): Promise { + const params = [ + 'diff', + `-M${options.similarityThreshold == null ? '' : `${options.similarityThreshold}%`}`, + '--no-ext-diff', + '-U0', + '--minimal', + ]; + + if (options.filters != null && options.filters.length !== 0) { + params.push(`--diff-filter=${options.filters.join(emptyStr)}`); + } + + // // ^3 signals an untracked file in a stash and if we are trying to find its parent, use the root sha + // if (ref.endsWith('^3^')) { + // ref = rootSha; + // } + // params.push(GitRevision.isUncommittedStaged(ref) ? '--staged' : ref); + + params.push('--no-index'); + + try { + return await git( + { + cwd: repoPath, + configs: ['-c', 'color.diff=false'], + encoding: options.encoding, + stdin: contents, + }, + ...params, + '--', + fileName, + // Pipe the contents to stdin + '-', + ); + } catch (ex) { + if (ex instanceof RunError && ex.stdout) { + return ex.stdout; + } + + const match = GitErrors.badRevision.exec(ex.message); + if (match !== null) { + const [, matchedRef] = match; + + // If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha + if (matchedRef === ref && matchedRef != null && matchedRef.endsWith('^')) { + return Git.diff__contents(repoPath, fileName, rootSha, contents, options); + } + } + + throw ex; + } + } + + export function diff__name_status( + repoPath: string, + ref1?: string, + ref2?: string, + { filters, similarityThreshold }: { filters?: GitDiffFilter[]; similarityThreshold?: number | null } = {}, + ) { + const params = [ + 'diff', + '--name-status', + `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + '--no-ext-diff', + ]; + if (filters != null && filters.length !== 0) { + params.push(`--diff-filter=${filters.join(emptyStr)}`); + } + if (ref1) { + params.push(ref1); + } + if (ref2) { + params.push(ref2); + } + + return git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); + } + + export async function diff__shortstat(repoPath: string, ref?: string) { + const params = ['diff', '--shortstat', '--no-ext-diff']; + if (ref) { + params.push(ref); + } + + try { + return await git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + if (GitErrors.noMergeBase.test(msg)) { + return undefined; + } + + throw ex; + } + } + + export function difftool( + repoPath: string, + fileName: string, + tool: string, + options: { ref1?: string; ref2?: string; staged?: boolean } = {}, + ) { + const params = ['difftool', '--no-prompt', `--tool=${tool}`]; + if (options.staged) { + params.push('--staged'); + } + if (options.ref1) { + params.push(options.ref1); + } + if (options.ref2) { + params.push(options.ref2); + } + + return git({ cwd: repoPath }, ...params, '--', fileName); + } + + export function difftool__dir_diff(repoPath: string, tool: string, ref1: string, ref2?: string) { + const params = ['difftool', '--dir-diff', `--tool=${tool}`, ref1]; + if (ref2) { + params.push(ref2); + } + + return git({ cwd: repoPath }, ...params); + } + + export async function fetch( + repoPath: string, + options: + | { all?: boolean; branch?: undefined; prune?: boolean; remote?: string } + | { + all?: undefined; + branch: string; + prune?: undefined; + pull?: boolean; + remote: string; + upstream: string; + } = {}, + ): Promise { + const params = ['fetch']; + + if (options.prune) { + params.push('--prune'); + } + + if (options.branch && options.remote) { + if (options.upstream && options.pull) { + params.push('-u', options.remote, `${options.upstream}:${options.branch}`); + + try { + void (await git({ cwd: repoPath }, ...params)); + return; + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + if (GitErrors.noFastForward.test(msg)) { + void window.showErrorMessage( + `Unable to pull the '${options.branch}' branch, as it can't be fast-forwarded.`, + ); + + return; + } + + throw ex; + } + } else { + params.push(options.remote, options.branch); + } + } else if (options.remote) { + params.push(options.remote); + } else if (options.all) { + params.push('--all'); + } + + void (await git({ cwd: repoPath }, ...params)); + } + + export function for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) { + const params = ['for-each-ref', `--format=${GitBranchParser.defaultFormat}`, 'refs/heads']; + if (options.all) { + params.push('refs/remotes'); + } + + return git({ cwd: repoPath }, ...params); + } + + export function log( + repoPath: string, + ref: string | undefined, + { + all, + authors, + format = 'default', + limit, + merges, + ordering, + reverse, + similarityThreshold, + since, + }: { + all?: boolean; + authors?: string[]; + format?: 'default' | 'refs' | 'shortlog' | 'shortlog+stats'; + limit?: number; + merges?: boolean; + ordering?: string | null; + reverse?: boolean; + similarityThreshold?: number | null; + since?: string; + }, + ) { + const params = [ + 'log', + `--format=${ + format === 'refs' + ? GitLogParser.simpleRefs + : format === 'shortlog' || format === 'shortlog+stats' + ? GitLogParser.shortlog + : GitLogParser.defaultFormat + }`, + '--full-history', + `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + '-m', + ]; + + if (format === 'default') { + params.push('--name-status'); + } else if (format === 'shortlog+stats') { + params.push('--shortstat'); + } + + if (ordering) { + params.push(`--${ordering}-order`); + } + + if (limit && !reverse) { + params.push(`-n${limit + 1}`); + } + + if (since) { + params.push(`--since="${since}"`); + } + + if (!merges) { + params.push('--first-parent'); + } + + if (authors != null && authors.length !== 0) { + params.push('--use-mailmap', ...authors.map(a => `--author=${a}`)); + } else if (format === 'shortlog') { + params.push('--use-mailmap'); + } + + if (all) { + params.push('--all'); + } + + if (ref && !GitRevision.isUncommittedStaged(ref)) { + // If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking + if (reverse) { + params.push('--reverse', '--ancestry-path', `${ref}..HEAD`); + } else { + params.push(ref); + } + } + + return git( + { cwd: repoPath, configs: ['-c', 'diff.renameLimit=0', '-c', 'log.showSignature=false'] }, + ...params, + '--', + ); + } + + export function log__file( + repoPath: string, + fileName: string, + ref: string | undefined, + { + all, + filters, + firstParent = false, + format = 'default', + limit, + ordering, + renames = true, + reverse = false, + since, + skip, + startLine, + endLine, + }: { + all?: boolean; + filters?: GitDiffFilter[]; + firstParent?: boolean; + format?: 'default' | 'refs' | 'simple'; + limit?: number; + ordering?: string | null; + renames?: boolean; + reverse?: boolean; + since?: string; + skip?: number; + startLine?: number; + endLine?: number; + } = {}, + ) { + const [file, root] = Paths.splitPath(fileName, repoPath); + + const params = [ + 'log', + `--format=${format === 'default' ? GitLogParser.defaultFormat : GitLogParser.simpleFormat}`, + ]; + + if (ordering) { + params.push(`--${ordering}-order`); + } + + if (limit && !reverse) { + params.push(`-n${limit + 1}`); + } + + if (skip) { + params.push(`--skip=${skip}`); + } + + if (since) { + params.push(`--since="${since}"`); + } + + if (all) { + params.push('--all'); + } + + // Can't allow rename detection (`--follow`) if `all` or a `startLine` is specified + if (renames && (all || startLine != null)) { + renames = false; + } + + params.push(renames ? '--follow' : '-m'); + if (/*renames ||*/ firstParent) { + params.push('--first-parent'); + // In Git >= 2.29.0 `--first-parent` implies `-m`, so lets include it for consistency + if (renames) { + params.push('-m'); + } + } + + if (filters != null && filters.length !== 0) { + params.push(`--diff-filter=${filters.join(emptyStr)}`); + } + + if (format !== 'refs') { + if (startLine == null) { + // 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'); + } + } else { + // Don't include `--name-status`, `--numstat`, or `--summary` because they aren't supported with `-L` + params.push(`-L ${startLine},${endLine == null ? startLine : endLine}:${file}`); + } + } + + if (ref && !GitRevision.isUncommittedStaged(ref)) { + // If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking + if (reverse) { + params.push('--reverse', '--ancestry-path', `${ref}..HEAD`); + } else { + params.push(ref); + } + } + + // Don't specify a file spec when using a line number (so say the git docs) + if (startLine == null) { + params.push('--', file); + } + + return git({ cwd: root, configs: ['-c', 'log.showSignature=false'] }, ...params); + } + + export async function log__file_recent( + repoPath: string, + fileName: string, + { + ordering, + ref, + similarityThreshold, + }: { ordering?: string | null; ref?: string; similarityThreshold?: number | null } = {}, + ) { + const params = [ + 'log', + `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + '-n1', + '--format=%H', + ]; + + if (ordering) { + params.push(`--${ordering}-order`); + } + + if (ref) { + params.push(ref); + } + + const data = await git( + { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, + ...params, + '--', + fileName, + ); + return data.length === 0 ? undefined : data.trim(); + } + + export async function log__find_object( + repoPath: string, + objectId: string, + ref: string, + ordering: string | null, + file?: string, + ) { + const params = ['log', '-n1', '--no-renames', '--format=%H', `--find-object=${objectId}`, ref]; + + if (ordering) { + params.push(`--${ordering}-order`); + } + + if (file) { + params.push('--', file); + } + + const data = await git( + { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, + ...params, + ); + return data.length === 0 ? undefined : data.trim(); + } + + export async function log__recent(repoPath: string, ordering?: string | null) { + const params = ['log', '-n1', '--format=%H']; + + if (ordering) { + params.push(`--${ordering}-order`); + } + + const data = await git( + { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, + ...params, + '--', + ); + + return data.length === 0 ? undefined : data.trim(); + } + + export async function log__recent_committerdate(repoPath: string, ordering?: string | null) { + const params = ['log', '-n1', '--format=%ct']; + + if (ordering) { + params.push(`--${ordering}-order`); + } + + const data = await git( + { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, + ...params, + '--', + ); + + return data.length === 0 ? undefined : data.trim(); + } + + export function log__search( + repoPath: string, + search: string[] = emptyArray, + { + limit, + ordering, + skip, + useShow, + }: { limit?: number; ordering?: string | null; skip?: number; useShow?: boolean } = {}, + ) { + const params = [ + useShow ? 'show' : 'log', + '--name-status', + `--format=${GitLogParser.defaultFormat}`, + '--use-mailmap', + ]; + + if (limit && !useShow) { + params.push(`-n${limit + 1}`); + } + + if (skip && !useShow) { + params.push(`--skip=${skip}`); + } + + if (ordering && !useShow) { + params.push(`--${ordering}-order`); + } + + return git( + { cwd: repoPath, configs: useShow ? undefined : ['-c', 'log.showSignature=false'] }, + ...params, + ...search, + ); + } + + // export function log__shortstat(repoPath: string, options: { ref?: string }) { + // const params = ['log', '--shortstat', '--oneline']; + // if (options.ref && !GitRevision.isUncommittedStaged(options.ref)) { + // params.push(options.ref); + // } + // return git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); + // } + + export async function ls_files( + repoPath: string, + fileName: string, + { ref, untracked }: { ref?: string; untracked?: boolean } = {}, + ): Promise { + const params = ['ls-files']; + if (ref && !GitRevision.isUncommitted(ref)) { + params.push(`--with-tree=${ref}`); + } + + if (!ref && untracked) { + params.push('-o'); + } + + const data = await git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params, '--', fileName); + return data.length === 0 ? undefined : data.trim(); + } + + export function ls_remote(repoPath: string, remote: string, ref?: string) { + return git({ cwd: repoPath }, 'ls-remote', remote, ref); + } + + export function ls_remote__HEAD(repoPath: string, remote: string) { + return git({ cwd: repoPath }, 'ls-remote', '--symref', remote, 'HEAD'); + } + + export async function ls_tree(repoPath: string, ref: string, { fileName }: { fileName?: string } = {}) { + const params = ['ls-tree']; + if (fileName) { + params.push('-l', ref, '--', fileName); + } else { + params.push('-lrt', ref, '--'); + } + const data = await git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params); + return data.length === 0 ? undefined : data.trim(); + } + + export function merge_base( + repoPath: string, + ref1: string, + ref2: string, + { forkPoint }: { forkPoint?: boolean } = {}, + ) { + const params = ['merge-base']; + if (forkPoint) { + params.push('--fork-point'); + } + + return git({ cwd: repoPath }, ...params, ref1, ref2); + } + + export function reflog( + repoPath: string, + { + all, + branch, + limit, + ordering, + skip, + }: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number } = {}, + ): Promise { + const params = ['log', '--walk-reflogs', `--format=${GitReflogParser.defaultFormat}`, '--date=iso8601']; + + if (ordering) { + params.push(`--${ordering}-order`); + } + + if (all) { + params.push('--all'); + } + + if (limit) { + params.push(`-n${limit}`); + } + + if (skip) { + params.push(`--skip=${skip}`); + } + + if (branch) { + params.push(branch); + } + + return git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); + } + + export function remote(repoPath: string): Promise { + return git({ cwd: repoPath }, 'remote', '-v'); + } + + export function remote__add(repoPath: string, name: string, url: string) { + return git({ cwd: repoPath }, 'remote', 'add', name, url); + } + + export function remote__prune(repoPath: string, remoteName: string) { + return git({ cwd: repoPath }, 'remote', 'prune', remoteName); + } + + export function remote__get_url(repoPath: string, remote: string): Promise { + return git({ cwd: repoPath }, 'remote', 'get-url', remote); + } + + export function reset(repoPath: string | undefined, fileName: string) { + return git({ cwd: repoPath }, 'reset', '-q', '--', fileName); + } + + export async function rev_list__count(repoPath: string, ref: string): Promise { + let data = await git( + { cwd: repoPath, errors: GitErrorHandling.Ignore }, + 'rev-list', + '--count', + ref, + '--', + ); + data = data.trim(); + if (data.length === 0) return undefined; + + const result = parseInt(data, 10); + return isNaN(result) ? undefined : result; + } + + export async function rev_list__left_right( + repoPath: string, + refs: string[], + ): Promise<{ ahead: number; behind: number } | undefined> { + const data = await git( + { cwd: repoPath, errors: GitErrorHandling.Ignore }, + 'rev-list', + '--left-right', + '--count', + ...refs, + '--', + ); + if (data.length === 0) return undefined; + + const parts = data.split('\t'); + if (parts.length !== 2) return undefined; + + const [ahead, behind] = parts; + const result = { + ahead: parseInt(ahead, 10), + behind: parseInt(behind, 10), + }; + + if (isNaN(result.ahead) || isNaN(result.behind)) return undefined; + + return result; + } + + export async function rev_parse__currentBranch( + repoPath: string, + ordering: string | null, + ): Promise<[string, string | undefined] | undefined> { + try { + const data = await git( + { cwd: repoPath, errors: GitErrorHandling.Throw }, + 'rev-parse', + '--abbrev-ref', + '--symbolic-full-name', + '@', + '@{u}', + '--', + ); + return [data, undefined]; + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + if (GitErrors.badRevision.test(msg) || GitWarnings.noUpstream.test(msg)) { + if (ex.stdout != null && ex.stdout.length !== 0) { + return [ex.stdout, undefined]; + } + + try { + const data = await symbolic_ref(repoPath, 'HEAD'); + if (data != null) return [data.trim(), undefined]; + } catch {} + + try { + const data = await symbolic_ref(repoPath, 'refs/remotes/origin/HEAD'); + if (data != null) return [data.trim().substr('origin/'.length), undefined]; + } catch (ex) { + if (/is not a symbolic ref/.test(ex.stderr)) { + try { + const data = await ls_remote__HEAD(repoPath, 'origin'); + if (data != null) { + const match = /ref:\s(\S+)\s+HEAD/m.exec(data); + if (match != null) { + const [, branch] = match; + return [branch.substr('refs/heads/'.length), undefined]; + } + } + } catch {} + } + } + + const defaultBranch = (await config__get('init.defaultBranch', repoPath, { local: true })) ?? 'main'; + const branchConfig = await config__get_regex(`branch\\.${defaultBranch}\\.+`, repoPath, { + local: true, + }); + + let remote; + let remoteBranch; + + if (branchConfig) { + let match = /^branch\..+\.remote\s(.+)$/m.exec(branchConfig); + if (match != null) { + remote = match[1]; + } + + match = /^branch\..+\.merge\srefs\/heads\/(.+)$/m.exec(branchConfig); + if (match != null) { + remoteBranch = match[1]; + } + } + return [`${defaultBranch}${remote && remoteBranch ? `\n${remote}/${remoteBranch}` : ''}`, undefined]; + } + + if (GitWarnings.headNotABranch.test(msg)) { + const sha = await log__recent(repoPath, ordering); + if (sha === undefined) return undefined; + + return [`(HEAD detached at ${GitRevision.shorten(sha)})`, sha]; + } + + defaultExceptionHandler(ex, repoPath); + return undefined; + } + } + + export async function rev_parse__show_toplevel(cwd: string): Promise { + try { + const data = await git( + { cwd: cwd, errors: GitErrorHandling.Throw }, + 'rev-parse', + '--show-toplevel', + ); + // Make sure to normalize: https://github.com/git-for-windows/git/issues/2478 + // Keep trailing spaces which are part of the directory name + return data.length === 0 ? undefined : Strings.normalizePath(data.trimLeft().replace(/[\r|\n]+$/, '')); + } catch (ex) { + const inDotGit = /this operation must be run in a work tree/.test(ex.stderr); + if (inDotGit || ex.code === 'ENOENT') { + // If the `cwd` doesn't exist, walk backward to see if any parent folder exists + let exists = inDotGit ? false : await fsExists(cwd); + if (!exists) { + do { + const parent = paths.dirname(cwd); + if (parent === cwd || parent.length === 0) return undefined; + + cwd = parent; + exists = await fsExists(cwd); + } while (!exists); + + return rev_parse__show_toplevel(cwd); + } + } + return undefined; + } + } + + export async function rev_parse__verify( + repoPath: string, + ref: string, + fileName?: string, + ): Promise { + const data = await git( + { cwd: repoPath, errors: GitErrorHandling.Ignore }, + 'rev-parse', + '--verify', + fileName ? `${ref}:./${fileName}` : `${ref}^{commit}`, + ); + return data.length === 0 ? undefined : data.trim(); + } + + export function shortlog(repoPath: string) { + return git({ cwd: repoPath }, 'shortlog', '-sne', '--all', '--no-merges', 'HEAD'); + } + + export async function show( + repoPath: string | undefined, + fileName: string, + ref: string, + options: { + encoding?: 'binary' | 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'hex' | 'buffer'; + } = {}, + ): Promise { + const [file, root] = Paths.splitPath(fileName, repoPath); + + if (GitRevision.isUncommittedStaged(ref)) { + ref = ':'; + } + if (GitRevision.isUncommitted(ref)) throw new Error(`ref=${ref} is uncommitted`); + + const opts: GitCommandOptions = { + configs: ['-c', 'log.showSignature=false'], + cwd: root, + encoding: options.encoding ?? 'utf8', + errors: GitErrorHandling.Throw, + }; + const args = ref.endsWith(':') ? `${ref}./${file}` : `${ref}:./${file}`; + + try { + const data = await git(opts, 'show', '--textconv', args, '--'); + return data; + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + if (ref === ':' && GitErrors.badRevision.test(msg)) { + return Git.show(repoPath, fileName, 'HEAD:', options); + } + + if ( + GitErrors.badRevision.test(msg) || + GitWarnings.notFound.test(msg) || + GitWarnings.foundButNotInRevision.test(msg) + ) { + return undefined; + } + + return defaultExceptionHandler(ex, opts.cwd) as TOut; + } + } + + export function show__diff( + repoPath: string, + fileName: string, + ref: string, + originalFileName?: string, + { similarityThreshold }: { similarityThreshold?: number | null } = {}, + ) { + const params = [ + 'show', + `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + '--format=', + '--minimal', + '-U0', + ref, + '--', + fileName, + ]; + if (originalFileName != null && originalFileName.length !== 0) { + params.push(originalFileName); + } + + return git({ cwd: repoPath }, ...params); + } + + export function show__name_status(repoPath: string, fileName: string, ref: string) { + return git({ cwd: repoPath }, 'show', '--name-status', '--format=', ref, '--', fileName); + } + + export function show_ref__tags(repoPath: string) { + return git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'show-ref', '--tags'); + } + + export function stash__apply( + repoPath: string, + stashName: string, + deleteAfter: boolean, + ): Promise { + if (!stashName) return Promise.resolve(undefined); + return git({ cwd: repoPath }, 'stash', deleteAfter ? 'pop' : 'apply', stashName); + } + + export async function stash__delete(repoPath: string, stashName: string, ref?: string) { + if (!stashName) return undefined; + + if (ref) { + const stashRef = await git( + { cwd: repoPath, errors: GitErrorHandling.Ignore }, + 'show', + '--format=%H', + '--no-patch', + stashName, + ); + if (stashRef?.trim() !== ref) { + throw new Error('Unable to delete stash; mismatch with stash number'); + } + } + + return git({ cwd: repoPath }, 'stash', 'drop', stashName); + } + + export function stash__list( + repoPath: string, + { + format = GitStashParser.defaultFormat, + similarityThreshold, + }: { format?: string; similarityThreshold?: number | null } = {}, + ) { + return git( + { cwd: repoPath }, + 'stash', + 'list', + '--name-status', + `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + `--format=${format}`, + ); + } + + export async function stash__push( + repoPath: string, + message?: string, + { + includeUntracked, + keepIndex, + pathspecs, + stdin, + }: { includeUntracked?: boolean; keepIndex?: boolean; pathspecs?: string[]; stdin?: boolean } = {}, + ): Promise { + const params = ['stash', 'push']; + + if (includeUntracked || (pathspecs != null && pathspecs.length !== 0)) { + params.push('-u'); + } + + if (keepIndex) { + params.push('-k'); + } + + if (message) { + params.push('-m', message); + } + + if (stdin && pathspecs != null && pathspecs.length !== 0) { + void (await git( + { cwd: repoPath, stdin: pathspecs.join('\0') }, + ...params, + '--pathspec-from-file=-', + '--pathspec-file-nul', + )); + + return; + } + + params.push('--'); + if (pathspecs != null && pathspecs.length !== 0) { + params.push(...pathspecs); + } + + void (await git({ cwd: repoPath }, ...params)); + } + + export async function status( + repoPath: string, + porcelainVersion: number = 1, + { similarityThreshold }: { similarityThreshold?: number | null } = {}, + ): Promise { + const params = [ + 'status', + porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain', + '--branch', + '-u', + ]; + if (await Git.isAtLeastVersion('2.18')) { + params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); + } + + return git( + { cwd: repoPath, configs: ['-c', 'color.status=false'], env: { GIT_OPTIONAL_LOCKS: '0' } }, + ...params, + '--', + ); + } + + export async function status__file( + repoPath: string, + fileName: string, + porcelainVersion: number = 1, + { similarityThreshold }: { similarityThreshold?: number | null } = {}, + ): Promise { + const [file, root] = Paths.splitPath(fileName, repoPath); + + const params = ['status', porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain']; + if (await Git.isAtLeastVersion('2.18')) { + params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); + } + + return git( + { cwd: root, configs: ['-c', 'color.status=false'], env: { GIT_OPTIONAL_LOCKS: '0' } }, + ...params, + '--', + file, + ); + } + + export function symbolic_ref(repoPath: string, ref: string) { + return git({ cwd: repoPath }, 'symbolic-ref', '--short', ref); + } + + export function tag(repoPath: string) { + return git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); + } + + export async function readDotGitFile( + repoPath: string, + paths: string[], + options?: { numeric?: false; throw?: boolean; trim?: boolean }, + ): Promise; + export async function readDotGitFile( + repoPath: string, + path: string[], + options?: { numeric: true; throw?: boolean; trim?: boolean }, + ): Promise; + export async function readDotGitFile( + repoPath: string, + pathParts: string[], + options?: { numeric?: boolean; throw?: boolean; trim?: boolean }, + ): Promise { + try { + const bytes = await workspace.fs.readFile(Uri.file(paths.join(...[repoPath, '.git', ...pathParts]))); + let contents = textDecoder.decode(bytes); + contents = options?.trim ?? true ? contents.trim() : contents; + + if (options?.numeric) { + const number = Number.parseInt(contents, 10); + return isNaN(number) ? undefined : number; + } + + return contents; + } catch (ex) { + if (options?.throw) throw ex; + + return undefined; + } + } +} + +export function isFolderGlob(path: string) { + return paths.basename(path) === '*'; +} diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts new file mode 100644 index 0000000..0c7555e --- /dev/null +++ b/src/env/node/git/localGitProvider.ts @@ -0,0 +1,3835 @@ +'use strict'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as paths from 'path'; +import { + Disposable, + env, + Event, + EventEmitter, + extensions, + Range, + TextEditor, + Uri, + window, + workspace, + WorkspaceFolder, +} from 'vscode'; +import { hrtime } from '@env/hrtime'; +import { isWindows } from '@env/platform'; +import type { + API as BuiltInGitApi, + Repository as BuiltInGitRepository, + GitExtension, +} from '../../../@types/vscode.git'; +import { configuration } from '../../../configuration'; +import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars } from '../../../constants'; +import { Container } from '../../../container'; +import { StashApplyError, StashApplyErrorReason } from '../../../git/errors'; +import { + GitProvider, + GitProviderId, + PagedResult, + RepositoryInitWatcher, + ScmRepository, +} from '../../../git/gitProvider'; +import { GitProviderService } from '../../../git/gitProviderService'; +import { GitUri } from '../../../git/gitUri'; +import { + BranchSortOptions, + GitAuthor, + GitBlame, + GitBlameCommit, + GitBlameLine, + GitBlameLines, + GitBranch, + GitBranchReference, + GitCommitType, + GitContributor, + GitDiff, + GitDiffFilter, + GitDiffHunkLine, + GitDiffShortStat, + GitFile, + GitLog, + GitLogCommit, + GitMergeStatus, + GitRebaseStatus, + GitReference, + GitReflog, + GitRemote, + GitRevision, + GitStash, + GitStatus, + GitStatusFile, + GitTag, + GitTree, + GitUser, + PullRequest, + PullRequestState, + Repository, + RepositoryChange, + RepositoryChangeComparisonMode, + RepositoryChangeEvent, + TagSortOptions, +} from '../../../git/models'; +import { + GitBlameParser, + GitBranchParser, + GitDiffParser, + GitLogParser, + GitReflogParser, + GitRemoteParser, + GitShortLogParser, + GitStashParser, + GitStatusParser, + GitTagParser, + GitTreeParser, +} from '../../../git/parsers'; +import { RemoteProviderFactory, RemoteProviders } from '../../../git/remotes/factory'; +import { RemoteProvider, RichRemoteProvider } from '../../../git/remotes/provider'; +import { SearchPattern } from '../../../git/search'; +import { LogCorrelationContext, Logger } from '../../../logger'; +import { Messages } from '../../../messages'; +import { Arrays, debug, Functions, gate, Iterables, log, Paths, Promises, Strings, Versions } from '../../../system'; +import { PromiseOrValue } from '../../../system/promise'; +import { + CachedBlame, + CachedDiff, + CachedLog, + GitDocumentState, + TrackedDocument, +} from '../../../trackers/gitDocumentTracker'; +import { Git, GitErrors, isFolderGlob, maxGitCliLength } from './git'; +import { findGitPath, GitLocation, InvalidGitConfigError, UnableToFindGitError } from './locator'; +import { fsExists, RunError } from './shell'; + +const emptyStr = ''; + +const RepoSearchWarnings = { + doesNotExist: /no such file or directory/i, +}; + +const doubleQuoteRegex = /"/g; +const driveLetterRegex = /(?<=^\/?)([a-zA-Z])(?=:\/)/; +const userConfigRegex = /^user\.(name|email) (.*)$/gm; +const mappedAuthorRegex = /(.+)\s<(.+)>/; + +const emptyPromise: Promise = Promise.resolve(undefined); +const reflogCommands = ['merge', 'pull']; + +export class LocalGitProvider implements GitProvider, Disposable { + descriptor = { id: GitProviderId.Git, name: 'Git' }; + + private _onDidChangeRepository = new EventEmitter(); + get onDidChangeRepository(): Event { + return this._onDidChangeRepository.event; + } + + private readonly _branchesCache = new Map>(); + private readonly _contributorsCache = new Map>(); + private readonly _mergeStatusCache = new Map(); + private readonly _rebaseStatusCache = new Map(); + private readonly _remotesWithApiProviderCache = new Map | null>(); + private readonly _stashesCache = new Map(); + private readonly _tagsCache = new Map>(); + private readonly _trackedCache = new Map>(); + private readonly _userMapCache = new Map(); + + constructor(private readonly container: Container) { + Git.setLocator(this.ensureGit.bind(this)); + } + + dispose() {} + + private get useCaching() { + return this.container.config.advanced.caching.enabled; + } + + private onRepositoryChanged(repo: Repository, e: RepositoryChangeEvent) { + if (e.changed(RepositoryChange.Config, RepositoryChangeComparisonMode.Any)) { + this._userMapCache.delete(repo.path); + } + + if (e.changed(RepositoryChange.Heads, RepositoryChange.Remotes, RepositoryChangeComparisonMode.Any)) { + this._branchesCache.delete(repo.path); + this._contributorsCache.delete(repo.path); + this._contributorsCache.delete(`stats|${repo.path}`); + } + + if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) { + this._remotesWithApiProviderCache.clear(); + } + + if (e.changed(RepositoryChange.Index, RepositoryChange.Unknown, RepositoryChangeComparisonMode.Any)) { + this._trackedCache.clear(); + } + + if (e.changed(RepositoryChange.Merge, RepositoryChangeComparisonMode.Any)) { + this._mergeStatusCache.delete(repo.path); + } + + if (e.changed(RepositoryChange.Rebase, RepositoryChangeComparisonMode.Any)) { + this._rebaseStatusCache.delete(repo.path); + } + + if (e.changed(RepositoryChange.Stash, RepositoryChangeComparisonMode.Any)) { + this._stashesCache.delete(repo.path); + } + + if (e.changed(RepositoryChange.Tags, RepositoryChangeComparisonMode.Any)) { + this._tagsCache.delete(repo.path); + } + + this._onDidChangeRepository.fire(e); + } + + private _gitLocator: Promise | undefined; + private async ensureGit(): Promise { + if (this._gitLocator == null) { + this._gitLocator = this.findGit(); + } + + return this._gitLocator; + } + + @log() + private async findGit(): Promise { + const cc = Logger.getCorrelationContext(); + + if (!configuration.getAny('git.enabled', null, true)) { + Logger.log(cc, 'Built-in Git is disabled ("git.enabled": false)'); + void Messages.showGitDisabledErrorMessage(); + + throw new UnableToFindGitError(); + } + + const scmPromise = this.getScmGitApi(); + + // Try to use the same git as the built-in vscode git extension, but only wait for a bit + const timeout = 100; + let gitPath; + try { + const gitApi = await Promises.cancellable(scmPromise, timeout); + gitPath = gitApi?.git.path; + } catch { + Logger.log(cc, `Stopped waiting for built-in Git, after ${timeout} ms...`); + } + + async function subscribeToScmOpenCloseRepository( + container: Container, + apiPromise: Promise, + ) { + const gitApi = await apiPromise; + if (gitApi == null) return; + + container.context.subscriptions.push( + gitApi.onDidCloseRepository(e => { + const repository = container.git.getCachedRepository(Strings.normalizePath(e.rootUri.fsPath)); + if (repository != null) { + repository.closed = true; + } + }), + gitApi.onDidOpenRepository(e => { + const repository = container.git.getCachedRepository(Strings.normalizePath(e.rootUri.fsPath)); + if (repository != null) { + repository.closed = false; + } + }), + ); + } + void subscribeToScmOpenCloseRepository(this.container, scmPromise); + + const start = hrtime(); + const location = await findGitPath(gitPath ?? configuration.getAny('git.path')); + + if (cc != null) { + cc.exitDetails = ` ${GlyphChars.Dot} Git found (${Strings.getDurationMilliseconds(start)} ms): ${ + location.version + } @ ${location.path === 'git' ? 'PATH' : location.path}`; + } else { + Logger.log( + cc, + `Git found: ${location.version} @ ${location.path === 'git' ? 'PATH' : location.path} ${ + GlyphChars.Dot + } ${Strings.getDurationMilliseconds(start)} ms`, + ); + } + + // Warn if git is less than v2.7.2 + if (Versions.compare(Versions.fromString(location.version), Versions.fromString('2.7.2')) === -1) { + Logger.log(cc, `Git version (${location.version}) is outdated`); + void Messages.showGitVersionUnsupportedErrorMessage(location.version, '2.7.2'); + } + + return location; + } + + async discoverRepositories(uri: Uri): Promise { + if (uri.scheme !== DocumentSchemes.File) return []; + + const autoRepositoryDetection = + configuration.getAny( + BuiltInGitConfiguration.AutoRepositoryDetection, + ) ?? true; + if (autoRepositoryDetection === false || autoRepositoryDetection === 'openEditors') return []; + + try { + void (await this.ensureGit()); + + const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!); + if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') { + for (const repository of repositories) { + void this.openScmRepository(repository.path); + } + } + + if (repositories.length > 0) { + this._trackedCache.clear(); + } + + return repositories; + } catch (ex) { + if (ex instanceof InvalidGitConfigError) { + void Messages.showGitInvalidConfigErrorMessage(); + } else if (ex instanceof UnableToFindGitError) { + void Messages.showGitMissingErrorMessage(); + } else { + const msg: string = ex?.message ?? ''; + if (msg) { + void window.showErrorMessage(`Unable to initialize Git; ${msg}`); + } + } + + throw ex; + } + } + + createRepository( + folder: WorkspaceFolder, + path: string, + root: boolean, + suspended?: boolean, + closed?: boolean, + ): Repository { + void this.openScmRepository(path); + return new Repository( + this.container, + this.onRepositoryChanged.bind(this), + this.descriptor, + folder, + path, + root, + suspended ?? !window.state.focused, + closed, + ); + } + + createRepositoryInitWatcher(): RepositoryInitWatcher { + const watcher = workspace.createFileSystemWatcher('**/.git', false, true, true); + return { + onDidCreate: watcher.onDidCreate, + dispose: () => watcher.dispose(), + }; + } + + @log({ + args: false, + singleLine: true, + prefix: (context, folder) => `${context.prefix}(${folder.uri.fsPath})`, + exit: result => + `returned ${result.length} repositories${ + result.length !== 0 ? ` (${result.map(r => r.path).join(', ')})` : emptyStr + }`, + }) + private async repositorySearch(folder: WorkspaceFolder): Promise { + const cc = Logger.getCorrelationContext(); + const { uri } = folder; + const depth = configuration.get('advanced.repositorySearchDepth', uri); + + Logger.log(cc, `searching (depth=${depth})...`); + + const repositories: Repository[] = []; + + const rootPath = await this.getRepoPath(uri.fsPath, true); + if (rootPath != null) { + Logger.log(cc, `found root repository in '${rootPath}'`); + repositories.push(this.createRepository(folder, rootPath, true)); + } + + if (depth <= 0) return repositories; + + // Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :) + let excludes = { + ...configuration.getAny>('files.exclude', uri, {}), + ...configuration.getAny>('search.exclude', uri, {}), + }; + + const excludedPaths = [ + ...Iterables.filterMap(Object.entries(excludes), ([key, value]) => { + if (!value) return undefined; + if (key.startsWith('**/')) return key.substring(3); + return key; + }), + ]; + + excludes = excludedPaths.reduce((accumulator, current) => { + accumulator[current] = true; + return accumulator; + }, Object.create(null) as Record); + + let repoPaths; + try { + repoPaths = await this.repositorySearchCore(uri.fsPath, depth, excludes); + } catch (ex) { + const msg: string = ex?.toString() ?? emptyStr; + if (RepoSearchWarnings.doesNotExist.test(msg)) { + Logger.log(cc, `FAILED${msg ? ` Error: ${msg}` : emptyStr}`); + } else { + Logger.error(ex, cc, 'FAILED'); + } + + return repositories; + } + + for (let p of repoPaths) { + p = paths.dirname(p); + // If we are the same as the root, skip it + if (Strings.normalizePath(p) === rootPath) continue; + + Logger.log(cc, `searching in '${p}'...`); + Logger.debug(cc, `normalizedRepoPath=${Strings.normalizePath(p)}, rootPath=${rootPath}`); + + const rp = await this.getRepoPath(p, true); + if (rp == null) continue; + + Logger.log(cc, `found repository in '${rp}'`); + repositories.push(this.createRepository(folder, rp, false)); + } + + return repositories; + } + + @debug({ args: { 2: false, 3: false } }) + private repositorySearchCore( + root: string, + depth: number, + excludes: Record, + repositories: string[] = [], + ): Promise { + const cc = Logger.getCorrelationContext(); + + return new Promise((resolve, reject) => { + fs.readdir(root, { withFileTypes: true }, async (err, files) => { + if (err != null) { + reject(err); + return; + } + + if (files.length === 0) { + resolve(repositories); + return; + } + + depth--; + + let f; + for (f of files) { + if (!f.isDirectory()) continue; + + if (f.name === '.git') { + repositories.push(paths.resolve(root, f.name)); + } else if (depth >= 0 && excludes[f.name] !== true) { + try { + await this.repositorySearchCore(paths.resolve(root, f.name), depth, excludes, repositories); + } catch (ex) { + Logger.error(ex, cc, 'FAILED'); + } + } + } + + resolve(repositories); + }); + }); + } + + @log() + async addRemote(repoPath: string, name: string, url: string): Promise { + await Git.remote__add(repoPath, name, url); + } + + @log() + async pruneRemote(repoPath: string, remoteName: string): Promise { + await Git.remote__prune(repoPath, remoteName); + } + + @log() + async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) { + const cc = Logger.getCorrelationContext(); + + ref1 = ref1 ?? uri.sha; + if (ref1 == null || uri.repoPath == null) return; + + if (ref2 == null) { + ref2 = ref1; + ref1 = `${ref1}^`; + } + + let patch; + try { + patch = await Git.diff(uri.repoPath, uri.fsPath, ref1, ref2); + void (await Git.apply(uri.repoPath, patch)); + } catch (ex) { + const msg: string = ex?.toString() ?? emptyStr; + if (patch && /patch does not apply/i.test(msg)) { + const result = await window.showWarningMessage( + 'Unable to apply changes cleanly. Retry and allow conflicts?', + { title: 'Yes' }, + { title: 'No', isCloseAffordance: true }, + ); + + if (result == null || result.title !== 'Yes') return; + + if (result.title === 'Yes') { + try { + void (await Git.apply(uri.repoPath, patch, { allowConflicts: true })); + + return; + } catch (e) { + // eslint-disable-next-line no-ex-assign + ex = e; + } + } + } + + Logger.error(ex, cc); + void Messages.showGenericErrorMessage('Unable to apply changes'); + } + } + + @log() + async branchContainsCommit(repoPath: string, name: string, ref: string): Promise { + let data = await Git.branch__containsOrPointsAt(repoPath, ref, { mode: 'contains', name: name }); + data = data?.trim(); + return Boolean(data); + } + + @log() + async checkout( + repoPath: string, + ref: string, + options?: { createBranch?: string } | { fileName?: string }, + ): Promise { + const cc = Logger.getCorrelationContext(); + + try { + await Git.checkout(repoPath, ref, options); + } catch (ex) { + const msg: string = ex?.toString() ?? emptyStr; + if (/overwritten by checkout/i.test(msg)) { + void Messages.showGenericErrorMessage( + `Unable to checkout '${ref}'. Please commit or stash your changes before switching branches`, + ); + return; + } + + Logger.error(ex, cc); + void void Messages.showGenericErrorMessage(`Unable to checkout '${ref}'`); + } + } + + @log() + resetCaches(...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]) { + if (cache.length === 0 || cache.includes('branches')) { + this._branchesCache.clear(); + } + + if (cache.length === 0 || cache.includes('contributors')) { + this._contributorsCache.clear(); + } + + if (cache.length === 0 || cache.includes('providers')) { + this._remotesWithApiProviderCache.clear(); + } + + if (cache.length === 0 || cache.includes('stashes')) { + this._stashesCache.clear(); + } + + if (cache.length === 0 || cache.includes('status')) { + this._mergeStatusCache.clear(); + this._rebaseStatusCache.clear(); + } + + if (cache.length === 0 || cache.includes('tags')) { + this._tagsCache.clear(); + } + + if (cache.length === 0) { + this._trackedCache.clear(); + this._userMapCache.clear(); + } + } + + @log({ args: { 1: uris => uris.length } }) + async excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise { + const paths = new Map(uris.map(u => [Strings.normalizePath(u.fsPath), u])); + + const data = await Git.check_ignore(repoPath, ...paths.keys()); + if (data == null) return uris; + + const ignored = data.split('\0').filter((i?: T): i is T => Boolean(i)); + if (ignored.length === 0) return uris; + + for (const file of ignored) { + paths.delete(file); + } + + return [...paths.values()]; + } + + @gate() + @log() + async fetch( + repoPath: string, + options: { all?: boolean; branch?: GitBranchReference; prune?: boolean; pull?: boolean; remote?: string } = {}, + ): Promise { + const { branch: branchRef, ...opts } = options; + if (GitReference.isBranch(branchRef)) { + const repo = await this.container.git.getRepository(repoPath); + const branch = await repo?.getBranch(branchRef?.name); + if (!branch?.remote && branch?.upstream == null) return undefined; + + return Git.fetch(repoPath, { + branch: branch.getNameWithoutRemote(), + remote: branch.getRemoteName()!, + upstream: branch.getTrackingWithoutRemote()!, + pull: options.pull, + }); + } + + return Git.fetch(repoPath, opts); + } + + @log() + async getBlameForFile(uri: GitUri): Promise { + const cc = Logger.getCorrelationContext(); + + let key = 'blame'; + if (uri.sha != null) { + key += `:${uri.sha}`; + } + + const doc = await this.container.tracker.getOrAdd(uri); + if (this.useCaching) { + if (doc.state != null) { + const cachedBlame = doc.state.get(key); + if (cachedBlame != null) { + Logger.debug(cc, `Cache hit: '${key}'`); + return cachedBlame.item; + } + } + + Logger.debug(cc, `Cache miss: '${key}'`); + + if (doc.state == null) { + doc.state = new GitDocumentState(doc.key); + } + } + + const promise = this.getBlameForFileCore(uri, doc, key, cc); + + if (doc.state != null) { + Logger.debug(cc, `Cache add: '${key}'`); + + const value: CachedBlame = { + item: promise as Promise, + }; + doc.state.set(key, value); + } + + return promise; + } + + private async getBlameForFileCore( + uri: GitUri, + document: TrackedDocument, + key: string, + cc: LogCorrelationContext | undefined, + ): Promise { + if (!(await this.isTracked(uri))) { + Logger.log(cc, `Skipping blame; '${uri.fsPath}' is not tracked`); + return emptyPromise as Promise; + } + + const [file, root] = Paths.splitPath(uri.fsPath, uri.repoPath, false); + + try { + const data = await Git.blame(root, file, uri.sha, { + args: this.container.config.advanced.blame.customArguments, + ignoreWhitespace: this.container.config.blame.ignoreWhitespace, + }); + const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); + return blame; + } catch (ex) { + // Trap and cache expected blame errors + if (document.state != null) { + const msg = ex?.toString() ?? ''; + Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); + + const value: CachedBlame = { + item: emptyPromise as Promise, + errorMessage: msg, + }; + document.state.set(key, value); + + document.setBlameFailure(); + + return emptyPromise as Promise; + } + + return undefined; + } + } + + @log({ args: { 1: '' } }) + async getBlameForFileContents(uri: GitUri, contents: string): Promise { + const cc = Logger.getCorrelationContext(); + + const key = `blame:${Strings.md5(contents)}`; + + const doc = await this.container.tracker.getOrAdd(uri); + if (this.useCaching) { + if (doc.state != null) { + const cachedBlame = doc.state.get(key); + if (cachedBlame != null) { + Logger.debug(cc, `Cache hit: ${key}`); + return cachedBlame.item; + } + } + + Logger.debug(cc, `Cache miss: ${key}`); + + if (doc.state == null) { + doc.state = new GitDocumentState(doc.key); + } + } + + const promise = this.getBlameForFileContentsCore(uri, contents, doc, key, cc); + + if (doc.state != null) { + Logger.debug(cc, `Cache add: '${key}'`); + + const value: CachedBlame = { + item: promise as Promise, + }; + doc.state.set(key, value); + } + + return promise; + } + + async getBlameForFileContentsCore( + uri: GitUri, + contents: string, + document: TrackedDocument, + key: string, + cc: LogCorrelationContext | undefined, + ): Promise { + if (!(await this.isTracked(uri))) { + Logger.log(cc, `Skipping blame; '${uri.fsPath}' is not tracked`); + return emptyPromise as Promise; + } + + const [file, root] = Paths.splitPath(uri.fsPath, uri.repoPath, false); + + try { + const data = await Git.blame__contents(root, file, contents, { + args: this.container.config.advanced.blame.customArguments, + correlationKey: `:${key}`, + ignoreWhitespace: this.container.config.blame.ignoreWhitespace, + }); + const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); + return blame; + } catch (ex) { + // Trap and cache expected blame errors + if (document.state != null) { + const msg = ex?.toString() ?? ''; + Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); + + const value: CachedBlame = { + item: emptyPromise as Promise, + errorMessage: msg, + }; + document.state.set(key, value); + + document.setBlameFailure(); + return emptyPromise as Promise; + } + + return undefined; + } + } + + @log() + async getBlameForLine( + uri: GitUri, + editorLine: number, // editor lines are 0-based + options: { skipCache?: boolean } = {}, + ): Promise { + if (!options.skipCache && this.useCaching) { + const blame = await this.getBlameForFile(uri); + if (blame == null) return undefined; + + let blameLine = blame.lines[editorLine]; + if (blameLine == null) { + if (blame.lines.length !== editorLine) return undefined; + blameLine = blame.lines[editorLine - 1]; + } + + const commit = blame.commits.get(blameLine.sha); + if (commit == null) return undefined; + + const author = blame.authors.get(commit.author)!; + return { + author: { ...author, lineCount: commit.lines.length }, + commit: commit, + line: blameLine, + }; + } + + const lineToBlame = editorLine + 1; + const fileName = uri.fsPath; + + try { + const data = await Git.blame(uri.repoPath, fileName, uri.sha, { + args: this.container.config.advanced.blame.customArguments, + ignoreWhitespace: this.container.config.blame.ignoreWhitespace, + startLine: lineToBlame, + endLine: lineToBlame, + }); + const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); + if (blame == null) return undefined; + + return { + author: Iterables.first(blame.authors.values()), + commit: Iterables.first(blame.commits.values()), + line: blame.lines[editorLine], + }; + } catch { + return undefined; + } + } + + @log({ args: { 2: '' } }) + async getBlameForLineContents( + uri: GitUri, + editorLine: number, // editor lines are 0-based + contents: string, + options: { skipCache?: boolean } = {}, + ): Promise { + if (!options.skipCache && this.useCaching) { + const blame = await this.getBlameForFileContents(uri, contents); + if (blame == null) return undefined; + + let blameLine = blame.lines[editorLine]; + if (blameLine == null) { + if (blame.lines.length !== editorLine) return undefined; + blameLine = blame.lines[editorLine - 1]; + } + + const commit = blame.commits.get(blameLine.sha); + if (commit == null) return undefined; + + const author = blame.authors.get(commit.author)!; + return { + author: { ...author, lineCount: commit.lines.length }, + commit: commit, + line: blameLine, + }; + } + + const lineToBlame = editorLine + 1; + const fileName = uri.fsPath; + + try { + const data = await Git.blame__contents(uri.repoPath, fileName, contents, { + args: this.container.config.advanced.blame.customArguments, + ignoreWhitespace: this.container.config.blame.ignoreWhitespace, + startLine: lineToBlame, + endLine: lineToBlame, + }); + const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); + if (blame == null) return undefined; + + return { + author: Iterables.first(blame.authors.values()), + commit: Iterables.first(blame.commits.values()), + line: blame.lines[editorLine], + }; + } catch { + return undefined; + } + } + + @log() + async getBlameForRange(uri: GitUri, range: Range): Promise { + const blame = await this.getBlameForFile(uri); + if (blame == null) return undefined; + + return this.getBlameForRangeSync(blame, uri, range); + } + + @log({ args: { 2: '' } }) + async getBlameForRangeContents(uri: GitUri, range: Range, contents: string): Promise { + const blame = await this.getBlameForFileContents(uri, contents); + if (blame == null) return undefined; + + return this.getBlameForRangeSync(blame, uri, range); + } + + @log({ args: { 0: '' } }) + getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined { + if (blame.lines.length === 0) return { allLines: blame.lines, ...blame }; + + if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { + return { allLines: blame.lines, ...blame }; + } + + const lines = blame.lines.slice(range.start.line, range.end.line + 1); + const shas = new Set(lines.map(l => l.sha)); + + // ranges are 0-based + const startLine = range.start.line + 1; + const endLine = range.end.line + 1; + + const authors = new Map(); + const commits = new Map(); + for (const c of blame.commits.values()) { + if (!shas.has(c.sha)) continue; + + const commit = c.with({ + lines: c.lines.filter(l => l.line >= startLine && l.line <= endLine), + }); + commits.set(c.sha, commit); + + let author = authors.get(commit.author); + if (author == null) { + author = { + name: commit.author, + lineCount: 0, + }; + authors.set(author.name, author); + } + + author.lineCount += commit.lines.length; + } + + const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); + + return { + repoPath: uri.repoPath!, + authors: sortedAuthors, + commits: commits, + lines: lines, + allLines: blame.lines, + }; + } + + @log() + async getBranch(repoPath: string): Promise { + let { + values: [branch], + } = await this.getBranches(repoPath, { filter: b => b.current }); + if (branch != null) return branch; + + const data = await Git.rev_parse__currentBranch(repoPath, this.container.config.advanced.commitOrdering); + if (data == null) return undefined; + + const [name, upstream] = data[0].split('\n'); + if (GitBranch.isDetached(name)) { + const [rebaseStatus, committerDate] = await Promise.all([ + this.getRebaseStatus(repoPath), + Git.log__recent_committerdate(repoPath, this.container.config.advanced.commitOrdering), + ]); + + branch = new GitBranch( + repoPath, + rebaseStatus?.incoming.name ?? name, + false, + true, + committerDate != null ? new Date(Number(committerDate) * 1000) : undefined, + data[1], + upstream ? { name: upstream, missing: false } : undefined, + undefined, + undefined, + undefined, + rebaseStatus != null, + ); + } + + return branch; + } + + // @log({ + // args: { + // 0: b => b.name, + // }, + // }) + // async getBranchAheadRange(branch: GitBranch) { + // if (branch.state.ahead > 0) { + // return GitRevision.createRange(branch.upstream?.name, branch.ref); + // } + + // if (branch.upstream == null) { + // // If we have no upstream branch, try to find a best guess branch to use as the "base" + // const { values: branches } = await this.getBranches(branch.repoPath, { + // filter: b => weightedDefaultBranches.has(b.name), + // }); + // if (branches.length > 0) { + // let weightedBranch: { weight: number; branch: GitBranch } | undefined; + // for (const branch of branches) { + // const weight = weightedDefaultBranches.get(branch.name)!; + // if (weightedBranch == null || weightedBranch.weight < weight) { + // weightedBranch = { weight: weight, branch: branch }; + // } + + // if (weightedBranch.weight === maxDefaultBranchWeight) break; + // } + + // const possibleBranch = weightedBranch!.branch.upstream?.name ?? weightedBranch!.branch.ref; + // if (possibleBranch !== branch.ref) { + // return GitRevision.createRange(possibleBranch, branch.ref); + // } + // } + // } + + // return undefined; + // } + + @log({ args: { 1: false } }) + async getBranches( + repoPath: string | undefined, + options: { + filter?: (b: GitBranch) => boolean; + sort?: boolean | BranchSortOptions; + } = {}, + ): Promise> { + if (repoPath == null) return { values: [] }; + + let branchesPromise = this.useCaching ? this._branchesCache.get(repoPath) : undefined; + if (branchesPromise == null) { + async function load(this: LocalGitProvider) { + try { + const data = await Git.for_each_ref__branch(repoPath!, { all: true }); + // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch + if (data == null || data.length === 0) { + let current; + + const data = await Git.rev_parse__currentBranch( + repoPath!, + this.container.config.advanced.commitOrdering, + ); + if (data != null) { + const [name, upstream] = data[0].split('\n'); + const [rebaseStatus, committerDate] = await Promise.all([ + GitBranch.isDetached(name) ? this.getRebaseStatus(repoPath!) : undefined, + Git.log__recent_committerdate(repoPath!, this.container.config.advanced.commitOrdering), + ]); + + current = new GitBranch( + repoPath!, + rebaseStatus?.incoming.name ?? name, + false, + true, + committerDate != null ? new Date(Number(committerDate) * 1000) : undefined, + data[1], + { name: upstream, missing: false }, + undefined, + undefined, + undefined, + rebaseStatus != null, + ); + } + + return current != null ? [current] : []; + } + + return GitBranchParser.parse(data, repoPath!); + } catch (ex) { + this._branchesCache.delete(repoPath!); + + return []; + } + } + + branchesPromise = load.call(this); + + if (this.useCaching) { + this._branchesCache.set(repoPath, branchesPromise); + + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { + this._branchesCache.delete(repoPath); + } + } + } + + let branches = await branchesPromise; + if (options.filter != null) { + branches = branches.filter(options.filter); + } + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (options.sort) { + GitBranch.sort(branches, typeof options.sort === 'boolean' ? undefined : options.sort); + } + + return { values: branches }; + } + + // @log() + // async getBranchesAndTagsTipsFn(repoPath: string | undefined, currentName?: string) { + // const [{ values: branches }, { values: tags }] = await Promise.all([ + // this.getBranches(repoPath), + // this.getTags(repoPath), + // ]); + + // const branchesAndTagsBySha = Arrays.groupByFilterMap( + // (branches as (GitBranch | GitTag)[]).concat(tags as (GitBranch | GitTag)[]), + // bt => bt.sha, + // bt => { + // if (currentName) { + // if (bt.name === currentName) return undefined; + // if (bt.refType === 'branch' && bt.getNameWithoutRemote() === currentName) { + // return { name: bt.name, compactName: bt.getRemoteName(), type: bt.refType }; + // } + // } + + // return { name: bt.name, compactName: undefined, type: bt.refType }; + // }, + // ); + + // return (sha: string, options?: { compact?: boolean; icons?: boolean }): string | undefined => { + // const branchesAndTags = branchesAndTagsBySha.get(sha); + // if (branchesAndTags == null || branchesAndTags.length === 0) return undefined; + + // if (!options?.compact) { + // return branchesAndTags + // .map( + // bt => `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${bt.name}`, + // ) + // .join(', '); + // } + + // if (branchesAndTags.length > 1) { + // const [bt] = branchesAndTags; + // return `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${ + // bt.compactName ?? bt.name + // }, ${GlyphChars.Ellipsis}`; + // } + + // return branchesAndTags + // .map( + // bt => + // `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${ + // bt.compactName ?? bt.name + // }`, + // ) + // .join(', '); + // }; + // } + + @log() + async getChangedFilesCount(repoPath: string, ref?: string): Promise { + const data = await Git.diff__shortstat(repoPath, ref); + if (!data) return undefined; + + return GitDiffParser.parseShortStat(data); + } + + @log() + async getCommit(repoPath: string, ref: string): Promise { + const log = await this.getLog(repoPath, { limit: 2, ref: ref }); + if (log == null) return undefined; + + return log.commits.get(ref) ?? Iterables.first(log.commits.values()); + } + + @log() + async getCommitBranches( + repoPath: string, + ref: string, + options?: { mode?: 'contains' | 'pointsAt'; remotes?: boolean }, + ): Promise { + 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)); + } + + @log({ args: { 1: refs => refs.join(',') } }) + getAheadBehindCommitCount( + repoPath: string, + refs: string[], + ): Promise<{ ahead: number; behind: number } | undefined> { + return Git.rev_list__left_right(repoPath, refs); + } + + @log() + getCommitCount(repoPath: string, ref: string): Promise { + return Git.rev_list__count(repoPath, ref); + } + + @log() + async getCommitForFile( + repoPath: string | undefined, + fileName: string, + options: { ref?: string; firstIfNotFound?: boolean; range?: Range; reverse?: boolean } = {}, + ): Promise { + const cc = Logger.getCorrelationContext(); + + try { + const log = await this.getLogForFile(repoPath, fileName, { + limit: 2, + ref: options.ref, + range: options.range, + reverse: options.reverse, + }); + if (log == null) return undefined; + + const commit = options.ref ? log.commits.get(options.ref) : undefined; + if (commit == null && !options.firstIfNotFound && options.ref) { + // If the ref isn't a valid sha we will never find it, so let it fall through so we return the first + if (GitRevision.isSha(options.ref) || GitRevision.isUncommitted(options.ref)) return undefined; + } + + return commit ?? Iterables.first(log.commits.values()); + } catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + @log() + async getOldestUnpushedRefForFile(repoPath: string, fileName: string): Promise { + const data = await Git.log__file(repoPath, fileName, '@{push}..', { + format: 'refs', + ordering: this.container.config.advanced.commitOrdering, + renames: true, + }); + if (data == null || data.length === 0) return undefined; + + return GitLogParser.parseLastRefOnly(data); + } + + @log() + getConfig(key: string, repoPath?: string): Promise { + return Git.config__get(key, repoPath); + } + + @log() + async getContributors( + repoPath: string, + options?: { all?: boolean; ref?: string; stats?: boolean }, + ): Promise { + if (repoPath == null) return []; + + const key = options?.stats ? `stats|${repoPath}` : repoPath; + + let contributors = this.useCaching ? this._contributorsCache.get(key) : undefined; + if (contributors == null) { + async function load(this: LocalGitProvider) { + try { + const currentUser = await this.getCurrentUser(repoPath); + + const data = await Git.log(repoPath, options?.ref, { + all: options?.all, + format: options?.stats ? 'shortlog+stats' : 'shortlog', + }); + const shortlog = GitShortLogParser.parseFromLog(data, repoPath, currentUser); + + return shortlog != null ? shortlog.contributors : []; + } catch (ex) { + this._contributorsCache.delete(key); + + return []; + } + } + + contributors = load.call(this); + + if (this.useCaching) { + this._contributorsCache.set(key, contributors); + + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { + this._contributorsCache.delete(key); + } + } + } + + return contributors; + } + + @gate() + @log() + async getCurrentUser(repoPath: string): Promise { + let user = this._userMapCache.get(repoPath); + if (user != null) return user; + // If we found the repo, but no user data was found just return + if (user === null) return undefined; + + user = { name: undefined, email: undefined }; + + const data = await Git.config__get_regex('^user\\.', repoPath, { local: true }); + if (data) { + let key: string; + let value: string; + + let match; + do { + match = userConfigRegex.exec(data); + if (match == null) break; + + [, key, value] = match; + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + user[key as 'name' | 'email'] = ` ${value}`.substr(1); + } while (true); + } else { + user.name = + process.env.GIT_AUTHOR_NAME || process.env.GIT_COMMITTER_NAME || os.userInfo()?.username || undefined; + if (!user.name) { + // If we found no user data, mark it so we won't bother trying again + this._userMapCache.set(repoPath, null); + return undefined; + } + + user.email = + process.env.GIT_AUTHOR_EMAIL || + process.env.GIT_COMMITTER_EMAIL || + process.env.EMAIL || + `${user.name}@${os.hostname()}`; + } + + const author = `${user.name} <${user.email}>`; + // Check if there is a mailmap for the current user + const mappedAuthor = await Git.check_mailmap(repoPath, author); + if (mappedAuthor != null && mappedAuthor.length !== 0 && author !== mappedAuthor) { + const match = mappedAuthorRegex.exec(mappedAuthor); + if (match != null) { + [, user.name, user.email] = match; + } + } + + this._userMapCache.set(repoPath, user); + return user; + } + + @log() + async getDefaultBranchName(repoPath: string | undefined, remote?: string): Promise { + if (repoPath == null) return undefined; + + if (!remote) { + try { + const data = await Git.symbolic_ref(repoPath, 'HEAD'); + if (data != null) return data.trim(); + } catch {} + } + + remote = remote ?? 'origin'; + try { + const data = await Git.ls_remote__HEAD(repoPath, remote); + if (data == null) return undefined; + + const match = /ref:\s(\S+)\s+HEAD/m.exec(data); + if (match == null) return undefined; + + const [, branch] = match; + return branch.substr('refs/heads/'.length); + } catch { + return undefined; + } + } + + @log() + async getDiffForFile( + uri: GitUri, + ref1: string | undefined, + ref2?: string, + originalFileName?: string, + ): Promise { + const cc = Logger.getCorrelationContext(); + + let key = 'diff'; + if (ref1 != null) { + key += `:${ref1}`; + } + if (ref2 != null) { + key += `:${ref2}`; + } + + const doc = await this.container.tracker.getOrAdd(uri); + if (this.useCaching) { + if (doc.state != null) { + const cachedDiff = doc.state.get(key); + if (cachedDiff != null) { + Logger.debug(cc, `Cache hit: '${key}'`); + return cachedDiff.item; + } + } + + Logger.debug(cc, `Cache miss: '${key}'`); + + if (doc.state == null) { + doc.state = new GitDocumentState(doc.key); + } + } + + const promise = this.getDiffForFileCore( + uri.repoPath, + uri.fsPath, + ref1, + ref2, + originalFileName, + { encoding: GitProviderService.getEncoding(uri) }, + doc, + key, + cc, + ); + + if (doc.state != null) { + Logger.debug(cc, `Cache add: '${key}'`); + + const value: CachedDiff = { + item: promise as Promise, + }; + doc.state.set(key, value); + } + + return promise; + } + + private async getDiffForFileCore( + repoPath: string | undefined, + fileName: string, + ref1: string | undefined, + ref2: string | undefined, + originalFileName: string | undefined, + options: { encoding?: string }, + document: TrackedDocument, + key: string, + cc: LogCorrelationContext | undefined, + ): Promise { + const [file, root] = Paths.splitPath(fileName, repoPath, false); + + try { + // let data; + // if (ref2 == null && ref1 != null && !GitRevision.isUncommittedStaged(ref1)) { + // data = await Git.show__diff(root, file, ref1, originalFileName, { + // similarityThreshold: this.container.config.advanced.similarityThreshold, + // }); + // } else { + const data = await Git.diff(root, file, ref1, ref2, { + ...options, + filters: ['M'], + linesOfContext: 0, + renames: true, + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + // } + + const diff = GitDiffParser.parse(data); + return diff; + } catch (ex) { + // Trap and cache expected diff errors + if (document.state != null) { + const msg = ex?.toString() ?? ''; + Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); + + const value: CachedDiff = { + item: emptyPromise as Promise, + errorMessage: msg, + }; + document.state.set(key, value); + + return emptyPromise as Promise; + } + + return undefined; + } + } + + @log({ args: { 1: '' } }) + async getDiffForFileContents( + uri: GitUri, + ref: string, + contents: string, + originalFileName?: string, + ): Promise { + const cc = Logger.getCorrelationContext(); + + const key = `diff:${Strings.md5(contents)}`; + + const doc = await this.container.tracker.getOrAdd(uri); + if (this.useCaching) { + if (doc.state != null) { + const cachedDiff = doc.state.get(key); + if (cachedDiff != null) { + Logger.debug(cc, `Cache hit: ${key}`); + return cachedDiff.item; + } + } + + Logger.debug(cc, `Cache miss: ${key}`); + + if (doc.state == null) { + doc.state = new GitDocumentState(doc.key); + } + } + + const promise = this.getDiffForFileContentsCore( + uri.repoPath, + uri.fsPath, + ref, + contents, + originalFileName, + { encoding: GitProviderService.getEncoding(uri) }, + doc, + key, + cc, + ); + + if (doc.state != null) { + Logger.debug(cc, `Cache add: '${key}'`); + + const value: CachedDiff = { + item: promise as Promise, + }; + doc.state.set(key, value); + } + + return promise; + } + + async getDiffForFileContentsCore( + repoPath: string | undefined, + fileName: string, + ref: string, + contents: string, + originalFileName: string | undefined, + options: { encoding?: string }, + document: TrackedDocument, + key: string, + cc: LogCorrelationContext | undefined, + ): Promise { + const [file, root] = Paths.splitPath(fileName, repoPath, false); + + try { + const data = await Git.diff__contents(root, file, ref, contents, { + ...options, + filters: ['M'], + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + + const diff = GitDiffParser.parse(data); + return diff; + } catch (ex) { + // Trap and cache expected diff errors + if (document.state != null) { + const msg = ex?.toString() ?? ''; + Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); + + const value: CachedDiff = { + item: emptyPromise as Promise, + errorMessage: msg, + }; + document.state.set(key, value); + + return emptyPromise as Promise; + } + + return undefined; + } + } + + @log() + async getDiffForLine( + uri: GitUri, + editorLine: number, // editor lines are 0-based + ref1: string | undefined, + ref2?: string, + originalFileName?: string, + ): Promise { + try { + const diff = await this.getDiffForFile(uri, ref1, ref2, originalFileName); + if (diff == null) return undefined; + + const line = editorLine + 1; + const hunk = diff.hunks.find(c => c.current.position.start <= line && c.current.position.end >= line); + if (hunk == null) return undefined; + + return hunk.lines[line - hunk.current.position.start]; + } catch (ex) { + return undefined; + } + } + + @log() + async getDiffStatus( + repoPath: string, + ref1?: string, + ref2?: string, + options: { filters?: GitDiffFilter[]; similarityThreshold?: number } = {}, + ): Promise { + try { + const data = await Git.diff__name_status(repoPath, ref1, ref2, { + similarityThreshold: this.container.config.advanced.similarityThreshold, + ...options, + }); + const files = GitDiffParser.parseNameStatus(data, repoPath); + return files == null || files.length === 0 ? undefined : files; + } catch (ex) { + return undefined; + } + } + + @log() + async getFileStatusForCommit(repoPath: string, fileName: string, ref: string): Promise { + if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return undefined; + + const data = await Git.show__name_status(repoPath, fileName, ref); + if (!data) return undefined; + + const files = GitDiffParser.parseNameStatus(data, repoPath); + if (files == null || files.length === 0) return undefined; + + return files[0]; + } + + @log() + async getLog( + repoPath: string, + { + ref, + ...options + }: { + all?: boolean; + authors?: string[]; + limit?: number; + merges?: boolean; + ordering?: string | null; + ref?: string; + reverse?: boolean; + since?: string; + } = {}, + ): Promise { + const limit = options.limit ?? this.container.config.advanced.maxListItems ?? 0; + + try { + const data = await Git.log(repoPath, ref, { + ...options, + limit: limit, + merges: options.merges == null ? true : options.merges, + ordering: options.ordering ?? this.container.config.advanced.commitOrdering, + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + const log = GitLogParser.parse( + data, + GitCommitType.Log, + repoPath, + undefined, + ref, + await this.getCurrentUser(repoPath), + limit, + options.reverse!, + undefined, + ); + + if (log != null) { + const opts = { ...options, ref: ref }; + log.query = (limit: number | undefined) => this.getLog(repoPath, { ...opts, limit: limit }); + if (log.hasMore) { + log.more = this.getLogMoreFn(log, opts); + } + } + + return log; + } catch (ex) { + return undefined; + } + } + + @log() + async getLogRefsOnly( + repoPath: string, + { + ref, + ...options + }: { + authors?: string[]; + limit?: number; + merges?: boolean; + ordering?: string | null; + ref?: string; + reverse?: boolean; + since?: string; + } = {}, + ): Promise | undefined> { + const limit = options.limit ?? this.container.config.advanced.maxListItems ?? 0; + + try { + const data = await Git.log(repoPath, ref, { + authors: options.authors, + format: 'refs', + limit: limit, + merges: options.merges == null ? true : options.merges, + reverse: options.reverse, + similarityThreshold: this.container.config.advanced.similarityThreshold, + since: options.since, + ordering: options.ordering ?? this.container.config.advanced.commitOrdering, + }); + const commits = GitLogParser.parseRefsOnly(data); + return new Set(commits); + } catch (ex) { + return undefined; + } + } + + private getLogMoreFn( + log: GitLog, + options: { + authors?: string[]; + limit?: number; + merges?: boolean; + ordering?: string | null; + ref?: string; + reverse?: boolean; + }, + ): (limit: number | { until: string } | undefined) => Promise { + return async (limit: number | { until: string } | undefined) => { + const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; + let moreLimit = typeof limit === 'number' ? limit : undefined; + + if (moreUntil && Iterables.some(log.commits.values(), c => c.ref === moreUntil)) { + return log; + } + + moreLimit = moreLimit ?? this.container.config.advanced.maxSearchItems ?? 0; + + // If the log is for a range, then just get everything prior + more + if (GitRevision.isRange(log.sha)) { + const moreLog = await this.getLog(log.repoPath, { + ...options, + limit: moreLimit === 0 ? 0 : (options.limit ?? 0) + moreLimit, + }); + // If we can't find any more, assume we have everything + if (moreLog == null) return { ...log, hasMore: false }; + + return moreLog; + } + + const ref = Iterables.last(log.commits.values())?.ref; + const moreLog = await this.getLog(log.repoPath, { + ...options, + limit: moreUntil == null ? moreLimit : 0, + ref: moreUntil == null ? `${ref}^` : `${moreUntil}^..${ref}^`, + }); + // If we can't find any more, assume we have everything + if (moreLog == null) return { ...log, hasMore: false }; + + // Merge authors + const authors = new Map([...log.authors]); + for (const [key, addAuthor] of moreLog.authors) { + const author = authors.get(key); + if (author == null) { + authors.set(key, addAuthor); + } else { + author.lineCount += addAuthor.lineCount; + } + } + + const commits = new Map([...log.commits, ...moreLog.commits]); + + const mergedLog: GitLog = { + repoPath: log.repoPath, + authors: authors, + commits: commits, + sha: log.sha, + range: undefined, + count: commits.size, + limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, + hasMore: moreUntil == null ? moreLog.hasMore : true, + query: (limit: number | undefined) => this.getLog(log.repoPath, { ...options, limit: limit }), + }; + mergedLog.more = this.getLogMoreFn(mergedLog, options); + + return mergedLog; + }; + } + + @log() + async getLogForSearch( + repoPath: string, + search: SearchPattern, + options: { limit?: number; ordering?: string | null; skip?: number } = {}, + ): Promise { + search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; + + try { + const limit = options.limit ?? this.container.config.advanced.maxSearchItems ?? 0; + const similarityThreshold = this.container.config.advanced.similarityThreshold; + + const operations = SearchPattern.parseSearchOperations(search.pattern); + + const searchArgs = new Set(); + const files: string[] = []; + + let useShow = false; + + let op; + let values = operations.get('commit:'); + if (values != null) { + useShow = true; + + searchArgs.add('-m'); + searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); + for (const value of values) { + searchArgs.add(value.replace(doubleQuoteRegex, '')); + } + } else { + searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); + searchArgs.add('--all'); + searchArgs.add('--full-history'); + searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings'); + if (search.matchRegex && !search.matchCase) { + searchArgs.add('--regexp-ignore-case'); + } + + for ([op, values] of operations.entries()) { + switch (op) { + case 'message:': + searchArgs.add('-m'); + if (search.matchAll) { + searchArgs.add('--all-match'); + } + for (const value of values) { + searchArgs.add( + `--grep=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, + ); + } + + break; + + case 'author:': + searchArgs.add('-m'); + for (const value of values) { + searchArgs.add( + `--author=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, + ); + } + + break; + + case 'change:': + for (const value of values) { + searchArgs.add( + search.matchRegex + ? `-G${value.replace(doubleQuoteRegex, '')}` + : `-S${value.replace(doubleQuoteRegex, '')}`, + ); + } + + break; + + case 'file:': + for (const value of values) { + files.push(value.replace(doubleQuoteRegex, '')); + } + + break; + } + } + } + + const args = [...searchArgs.values(), '--']; + if (files.length !== 0) { + args.push(...files); + } + + const data = await Git.log__search(repoPath, args, { + ordering: this.container.config.advanced.commitOrdering, + ...options, + limit: limit, + useShow: useShow, + }); + const log = GitLogParser.parse( + data, + GitCommitType.Log, + repoPath, + undefined, + undefined, + await this.getCurrentUser(repoPath), + limit, + false, + undefined, + ); + + if (log != null) { + log.query = (limit: number | undefined) => + this.getLogForSearch(repoPath, search, { ...options, limit: limit }); + if (log.hasMore) { + log.more = this.getLogForSearchMoreFn(log, search, options); + } + } + + return log; + } catch (ex) { + return undefined; + } + } + + private getLogForSearchMoreFn( + log: GitLog, + search: SearchPattern, + options: { limit?: number; ordering?: string | null }, + ): (limit: number | undefined) => Promise { + return async (limit: number | undefined) => { + limit = limit ?? this.container.config.advanced.maxSearchItems ?? 0; + + const moreLog = await this.getLogForSearch(log.repoPath, search, { + ...options, + limit: limit, + skip: log.count, + }); + if (moreLog == null) { + // If we can't find any more, assume we have everything + return { ...log, hasMore: false }; + } + + // Merge authors + const authors = new Map([...log.authors]); + for (const [key, addAuthor] of moreLog.authors) { + const author = authors.get(key); + if (author == null) { + authors.set(key, addAuthor); + } else { + author.lineCount += addAuthor.lineCount; + } + } + + const commits = new Map([...log.commits, ...moreLog.commits]); + + const mergedLog: GitLog = { + repoPath: log.repoPath, + authors: authors, + commits: commits, + sha: log.sha, + range: log.range, + count: commits.size, + limit: (log.limit ?? 0) + limit, + hasMore: moreLog.hasMore, + query: (limit: number | undefined) => + this.getLogForSearch(log.repoPath, search, { ...options, limit: limit }), + }; + mergedLog.more = this.getLogForSearchMoreFn(mergedLog, search, options); + + return mergedLog; + }; + } + @log() + async getLogForFile( + repoPath: string | undefined, + fileName: string, + options: { + all?: boolean; + limit?: number; + ordering?: string | null; + range?: Range; + ref?: string; + renames?: boolean; + reverse?: boolean; + since?: string; + skip?: number; + } = {}, + ): Promise { + if (repoPath != null && repoPath === Strings.normalizePath(fileName)) { + throw new Error(`File name cannot match the repository path; fileName=${fileName}`); + } + + const cc = Logger.getCorrelationContext(); + + options = { reverse: false, ...options }; + + if (options.renames == null) { + options.renames = this.container.config.advanced.fileHistoryFollowsRenames; + } + + let key = 'log'; + if (options.ref != null) { + key += `:${options.ref}`; + } + + if (options.all == null) { + options.all = this.container.config.advanced.fileHistoryShowAllBranches; + } + if (options.all) { + key += ':all'; + } + + options.limit = options.limit == null ? this.container.config.advanced.maxListItems || 0 : options.limit; + if (options.limit) { + key += `:n${options.limit}`; + } + + if (options.renames) { + key += ':follow'; + } + + if (options.reverse) { + key += ':reverse'; + } + + if (options.since) { + key += `:since=${options.since}`; + } + + if (options.skip) { + key += `:skip${options.skip}`; + } + + const doc = await this.container.tracker.getOrAdd(GitUri.fromFile(fileName, repoPath!, options.ref)); + if (this.useCaching && options.range == null) { + if (doc.state != null) { + const cachedLog = doc.state.get(key); + if (cachedLog != null) { + Logger.debug(cc, `Cache hit: '${key}'`); + return cachedLog.item; + } + + if (options.ref != null || options.limit != null) { + // Since we are looking for partial log, see if we have the log of the whole file + const cachedLog = doc.state.get( + `log${options.renames ? ':follow' : emptyStr}${options.reverse ? ':reverse' : emptyStr}`, + ); + if (cachedLog != null) { + if (options.ref == null) { + Logger.debug(cc, `Cache hit: ~'${key}'`); + return cachedLog.item; + } + + Logger.debug(cc, `Cache ?: '${key}'`); + let log = await cachedLog.item; + if (log != null && !log.hasMore && log.commits.has(options.ref)) { + Logger.debug(cc, `Cache hit: '${key}'`); + + // Create a copy of the log starting at the requested commit + let skip = true; + let i = 0; + const authors = new Map(); + const commits = new Map( + Iterables.filterMap<[string, GitLogCommit], [string, GitLogCommit]>( + log.commits.entries(), + ([ref, c]) => { + if (skip) { + if (ref !== options.ref) return undefined; + skip = false; + } + + i++; + if (options.limit != null && i > options.limit) { + return undefined; + } + + authors.set(c.author, log.authors.get(c.author)!); + return [ref, c]; + }, + ), + ); + + const opts = { ...options }; + log = { + ...log, + limit: options.limit, + count: commits.size, + commits: commits, + authors: authors, + query: (limit: number | undefined) => + this.getLogForFile(repoPath, fileName, { ...opts, limit: limit }), + }; + + return log; + } + } + } + } + + Logger.debug(cc, `Cache miss: '${key}'`); + + if (doc.state == null) { + doc.state = new GitDocumentState(doc.key); + } + } + + const promise = this.getLogForFileCore(repoPath, fileName, options, doc, key, cc); + + if (doc.state != null && options.range == null) { + Logger.debug(cc, `Cache add: '${key}'`); + + const value: CachedLog = { + item: promise as Promise, + }; + doc.state.set(key, value); + } + + return promise; + } + + private async getLogForFileCore( + repoPath: string | undefined, + fileName: string, + { + ref, + range, + ...options + }: { + all?: boolean; + limit?: number; + ordering?: string | null; + range?: Range; + ref?: string; + renames?: boolean; + reverse?: boolean; + since?: string; + skip?: number; + }, + document: TrackedDocument, + key: string, + cc: LogCorrelationContext | undefined, + ): Promise { + if (!(await this.isTracked(fileName, repoPath, ref))) { + Logger.log(cc, `Skipping log; '${fileName}' is not tracked`); + return emptyPromise as Promise; + } + + const [file, root] = Paths.splitPath(fileName, repoPath, false); + + try { + if (range != null && range.start.line > range.end.line) { + range = new Range(range.end, range.start); + } + + const data = await Git.log__file(root, file, ref, { + ordering: this.container.config.advanced.commitOrdering, + ...options, + firstParent: options.renames, + startLine: range == null ? undefined : range.start.line + 1, + endLine: range == null ? undefined : range.end.line + 1, + }); + const log = GitLogParser.parse( + data, + // 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, + await this.getCurrentUser(root), + options.limit, + options.reverse!, + range, + ); + + if (log != null) { + const opts = { ...options, ref: ref, range: range }; + log.query = (limit: number | undefined) => + this.getLogForFile(repoPath, fileName, { ...opts, limit: limit }); + if (log.hasMore) { + log.more = this.getLogForFileMoreFn(log, fileName, opts); + } + } + + return log; + } catch (ex) { + // Trap and cache expected log errors + if (document.state != null && range == null && !options.reverse) { + const msg: string = ex?.toString() ?? ''; + Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); + + const value: CachedLog = { + item: emptyPromise as Promise, + errorMessage: msg, + }; + document.state.set(key, value); + + return emptyPromise as Promise; + } + + return undefined; + } + } + + private getLogForFileMoreFn( + log: GitLog, + fileName: string, + options: { + all?: boolean; + limit?: number; + ordering?: string | null; + range?: Range; + ref?: string; + renames?: boolean; + reverse?: boolean; + }, + ): (limit: number | { until: string } | undefined) => Promise { + return async (limit: number | { until: string } | undefined) => { + const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; + let moreLimit = typeof limit === 'number' ? limit : undefined; + + if (moreUntil && Iterables.some(log.commits.values(), c => c.ref === moreUntil)) { + return log; + } + + moreLimit = moreLimit ?? this.container.config.advanced.maxSearchItems ?? 0; + + const ref = Iterables.last(log.commits.values())?.ref; + const moreLog = await this.getLogForFile(log.repoPath, fileName, { + ...options, + limit: moreUntil == null ? moreLimit : 0, + ref: options.all ? undefined : moreUntil == null ? `${ref}^` : `${moreUntil}^..${ref}^`, + skip: options.all ? log.count : undefined, + }); + if (moreLog == null) { + // If we can't find any more, assume we have everything + return { ...log, hasMore: false }; + } + + // Merge authors + const authors = new Map([...log.authors]); + for (const [key, addAuthor] of moreLog.authors) { + const author = authors.get(key); + if (author == null) { + authors.set(key, addAuthor); + } else { + author.lineCount += addAuthor.lineCount; + } + } + + const commits = new Map([...log.commits, ...moreLog.commits]); + + const mergedLog: GitLog = { + repoPath: log.repoPath, + authors: authors, + commits: commits, + sha: log.sha, + range: log.range, + count: commits.size, + limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, + hasMore: moreUntil == null ? moreLog.hasMore : true, + query: (limit: number | undefined) => + this.getLogForFile(log.repoPath, fileName, { ...options, limit: limit }), + }; + + if (options.renames) { + const renamed = Iterables.find( + moreLog.commits.values(), + c => Boolean(c.originalFileName) && c.originalFileName !== fileName, + ); + if (renamed != null) { + fileName = renamed.originalFileName!; + } + } + + mergedLog.more = this.getLogForFileMoreFn(mergedLog, fileName, options); + + return mergedLog; + }; + } + + @log() + async getMergeBase(repoPath: string, ref1: string, ref2: string, options: { forkPoint?: boolean } = {}) { + const cc = Logger.getCorrelationContext(); + + try { + const data = await Git.merge_base(repoPath, ref1, ref2, options); + if (data == null) return undefined; + + return data.split('\n')[0].trim() || undefined; + } catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + @gate() + @log() + async getMergeStatus(repoPath: string): Promise { + let status = this.useCaching ? this._mergeStatusCache.get(repoPath) : undefined; + if (status === undefined) { + const merge = await Git.rev_parse__verify(repoPath, 'MERGE_HEAD'); + if (merge != null) { + const [branch, mergeBase, possibleSourceBranches] = await Promise.all([ + this.getBranch(repoPath), + this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'), + this.getCommitBranches(repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }), + ]); + + status = { + type: 'merge', + repoPath: repoPath, + mergeBase: mergeBase, + HEAD: GitReference.create(merge, repoPath, { refType: 'revision' }), + current: GitReference.fromBranch(branch!), + incoming: + possibleSourceBranches?.length === 1 + ? GitReference.create(possibleSourceBranches[0], repoPath, { + refType: 'branch', + name: possibleSourceBranches[0], + remote: false, + }) + : undefined, + }; + } + + if (this.useCaching) { + this._mergeStatusCache.set(repoPath, status ?? null); + + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { + this._mergeStatusCache.delete(repoPath); + } + } + } + + return status ?? undefined; + } + + @gate() + @log() + async getRebaseStatus(repoPath: string): Promise { + let status = this.useCaching ? this._rebaseStatusCache.get(repoPath) : undefined; + if (status === undefined) { + const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD'); + if (rebase != null) { + let [mergeBase, branch, onto, stepsNumber, stepsMessage, stepsTotal] = await Promise.all([ + this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'), + Git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']), + Git.readDotGitFile(repoPath, ['rebase-merge', 'onto']), + Git.readDotGitFile(repoPath, ['rebase-merge', 'msgnum'], { numeric: true }), + Git.readDotGitFile(repoPath, ['rebase-merge', 'message'], { throw: true }).catch(() => + Git.readDotGitFile(repoPath, ['rebase-merge', 'message-squashed']), + ), + Git.readDotGitFile(repoPath, ['rebase-merge', 'end'], { numeric: true }), + ]); + + if (branch == null || onto == null) return undefined; + + if (branch.startsWith('refs/heads/')) { + branch = branch.substr(11).trim(); + } + + const possibleSourceBranches = await this.getCommitBranches(repoPath, onto, { mode: 'pointsAt' }); + + let possibleSourceBranch: string | undefined; + for (const b of possibleSourceBranches) { + if (b.startsWith('(no branch, rebasing')) continue; + + possibleSourceBranch = b; + break; + } + + status = { + type: 'rebase', + repoPath: repoPath, + mergeBase: mergeBase, + HEAD: GitReference.create(rebase, repoPath, { refType: 'revision' }), + onto: GitReference.create(onto, repoPath, { refType: 'revision' }), + current: + possibleSourceBranch != null + ? GitReference.create(possibleSourceBranch, repoPath, { + refType: 'branch', + name: possibleSourceBranch, + remote: false, + }) + : undefined, + + incoming: GitReference.create(branch, repoPath, { + refType: 'branch', + name: branch, + remote: false, + }), + steps: { + current: { + number: stepsNumber ?? 0, + commit: GitReference.create(rebase, repoPath, { + refType: 'revision', + message: stepsMessage, + }), + }, + total: stepsTotal ?? 0, + }, + }; + } + + if (this.useCaching) { + this._rebaseStatusCache.set(repoPath, status ?? null); + + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { + this._rebaseStatusCache.delete(repoPath); + } + } + } + + return status ?? undefined; + } + + @log() + async getNextDiffUris( + repoPath: string, + uri: Uri, + ref: string | undefined, + skip: number = 0, + ): Promise<{ current: GitUri; next: GitUri | undefined; deleted?: boolean } | undefined> { + // If we have no ref (or staged ref) there is no next commit + if (ref == null || ref.length === 0) return undefined; + + const fileName = GitUri.relativeTo(uri, repoPath); + + if (GitRevision.isUncommittedStaged(ref)) { + return { + current: GitUri.fromFile(fileName, repoPath, ref), + next: GitUri.fromFile(fileName, repoPath, undefined), + }; + } + + const next = await this.getNextUri(repoPath, uri, ref, skip); + if (next == null) { + const status = await this.getStatusForFile(repoPath, fileName); + if (status != null) { + // If the file is staged, diff with the staged version + if (status.indexStatus != null) { + return { + current: GitUri.fromFile(fileName, repoPath, ref), + next: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), + }; + } + } + + return { + current: GitUri.fromFile(fileName, repoPath, ref), + next: GitUri.fromFile(fileName, repoPath, undefined), + }; + } + + return { + current: + skip === 0 + ? GitUri.fromFile(fileName, repoPath, ref) + : (await this.getNextUri(repoPath, uri, ref, skip - 1))!, + next: next, + }; + } + + @log() + async getNextUri( + repoPath: string, + uri: Uri, + ref?: string, + skip: number = 0, + // editorLine?: number + ): Promise { + // If we have no ref (or staged ref) there is no next commit + if (ref == null || ref.length === 0 || GitRevision.isUncommittedStaged(ref)) return undefined; + + let filters: GitDiffFilter[] | undefined; + if (ref === GitRevision.deletedOrMissing) { + // If we are trying to move next from a deleted or missing ref then get the first commit + ref = undefined; + filters = ['A']; + } + + const fileName = GitUri.relativeTo(uri, repoPath); + let data = await Git.log__file(repoPath, fileName, ref, { + filters: filters, + format: 'simple', + limit: skip + 1, + ordering: this.container.config.advanced.commitOrdering, + reverse: true, + // startLine: editorLine != null ? editorLine + 1 : undefined, + }); + if (data == null || data.length === 0) return undefined; + + const [nextRef, file, status] = GitLogParser.parseSimple(data, skip); + // If the file was deleted, check for a possible rename + if (status === 'D') { + data = await Git.log__file(repoPath, '.', nextRef, { + filters: ['R', 'C'], + format: 'simple', + limit: 1, + ordering: this.container.config.advanced.commitOrdering, + // startLine: editorLine != null ? editorLine + 1 : undefined + }); + if (data == null || data.length === 0) { + return GitUri.fromFile(file ?? fileName, repoPath, nextRef); + } + + const [nextRenamedRef, renamedFile] = GitLogParser.parseSimpleRenamed(data, file ?? fileName); + return GitUri.fromFile( + renamedFile ?? file ?? fileName, + repoPath, + nextRenamedRef ?? nextRef ?? GitRevision.deletedOrMissing, + ); + } + + return GitUri.fromFile(file ?? fileName, repoPath, nextRef); + } + + @log() + async getPreviousDiffUris( + repoPath: string, + uri: Uri, + ref: string | undefined, + skip: number = 0, + firstParent: boolean = false, + ): Promise<{ current: GitUri; previous: GitUri | undefined } | undefined> { + if (ref === GitRevision.deletedOrMissing) return undefined; + + const fileName = GitUri.relativeTo(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) { + // First, check the file status to see if there is anything staged + const status = await this.getStatusForFile(repoPath, fileName); + 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 + if (status.indexStatus != null) { + // Backs up to get to HEAD + if (status.workingTreeStatus == null) { + skip++; + } + + if (skip === 0) { + // Diff working with staged + return { + current: GitUri.fromFile(fileName, repoPath, undefined), + previous: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), + }; + } + + return { + // Diff staged with HEAD (or prior if more skips) + current: GitUri.fromFile(fileName, 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), + previous: await this.getPreviousUri(repoPath, uri, undefined, skip, undefined, firstParent), + }; + } + } + } else if (skip === 0) { + skip++; + } + } + // If we are at the index (staged), diff staged with HEAD + else if (GitRevision.isUncommittedStaged(ref)) { + const current = + skip === 0 + ? GitUri.fromFile(fileName, repoPath, ref) + : (await this.getPreviousUri(repoPath, uri, undefined, skip - 1, undefined, firstParent))!; + if (current == null || current.sha === GitRevision.deletedOrMissing) return undefined; + + return { + current: current, + previous: await this.getPreviousUri(repoPath, uri, undefined, skip, undefined, firstParent), + }; + } + + // If we are at a commit, diff commit with previous + const current = + skip === 0 + ? GitUri.fromFile(fileName, repoPath, ref) + : (await this.getPreviousUri(repoPath, uri, ref, skip - 1, undefined, firstParent))!; + if (current == null || current.sha === GitRevision.deletedOrMissing) return undefined; + + return { + current: current, + previous: await this.getPreviousUri(repoPath, uri, ref, skip, undefined, firstParent), + }; + } + + @log() + async getPreviousLineDiffUris( + repoPath: string, + uri: Uri, + editorLine: number, + ref: string | undefined, + skip: number = 0, + ): Promise<{ current: GitUri; previous: GitUri | undefined; line: number } | undefined> { + if (ref === GitRevision.deletedOrMissing) return undefined; + + let fileName = GitUri.relativeTo(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) { + // First, check the blame on the current line to see if there are any working/staged changes + const gitUri = new GitUri(uri, repoPath); + + const document = await workspace.openTextDocument(uri); + const blameLine = document.isDirty + ? await this.getBlameForLineContents(gitUri, editorLine, document.getText()) + : await this.getBlameForLine(gitUri, editorLine); + if (blameLine == null) return undefined; + + // If line is uncommitted, we need to dig deeper to figure out where to go (because blame can't be trusted) + if (blameLine.commit.isUncommitted) { + // 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); + 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), + line: editorLine, + }; + } + } + + // Diff working with HEAD (or prior if more skips) + return { + current: GitUri.fromFile(fileName, repoPath, undefined), + previous: await this.getPreviousUri(repoPath, uri, undefined, skip, editorLine), + line: editorLine, + }; + } + + // First, check if we have a diff in the working tree + let hunkLine = await this.getDiffForLine(gitUri, editorLine, undefined); + if (hunkLine == null) { + // Next, check if we have a diff in the index (staged) + hunkLine = await this.getDiffForLine(gitUri, editorLine, undefined, GitRevision.uncommittedStaged); + + if (hunkLine != null) { + ref = GitRevision.uncommittedStaged; + } else { + skip++; + } + } + } + // 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.resolveToUri(fileName, repoPath); + editorLine = blameLine.line.originalLine - 1; + + if (skip === 0 && blameLine.commit.previousSha) { + previous = GitUri.fromFile(fileName, repoPath, blameLine.commit.previousSha); + } + } + } else { + if (GitRevision.isUncommittedStaged(ref)) { + const current = + skip === 0 + ? GitUri.fromFile(fileName, repoPath, ref) + : (await this.getPreviousUri(repoPath, uri, undefined, skip - 1, editorLine))!; + if (current.sha === GitRevision.deletedOrMissing) return undefined; + + return { + current: current, + previous: await this.getPreviousUri(repoPath, uri, undefined, skip, editorLine), + line: editorLine, + }; + } + + const gitUri = new GitUri(uri, { repoPath: repoPath, sha: ref }); + const blameLine = await this.getBlameForLine(gitUri, editorLine); + if (blameLine == null) return undefined; + + // Diff with line ref with previous + ref = blameLine.commit.sha; + fileName = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? fileName); + uri = GitUri.resolveToUri(fileName, repoPath); + editorLine = blameLine.line.originalLine - 1; + + if (skip === 0 && blameLine.commit.previousSha) { + previous = GitUri.fromFile(fileName, repoPath, blameLine.commit.previousSha); + } + } + + const current = + skip === 0 + ? GitUri.fromFile(fileName, repoPath, ref) + : (await this.getPreviousUri(repoPath, uri, ref, skip - 1, editorLine))!; + if (current.sha === GitRevision.deletedOrMissing) return undefined; + + return { + current: current, + previous: previous ?? (await this.getPreviousUri(repoPath, uri, ref, skip, editorLine)), + line: editorLine, + }; + } + + @log() + async getPreviousUri( + repoPath: string, + uri: Uri, + ref?: string, + skip: number = 0, + editorLine?: number, + firstParent: boolean = false, + ): Promise { + if (ref === GitRevision.deletedOrMissing) return undefined; + + const cc = Logger.getCorrelationContext(); + + if (ref === GitRevision.uncommitted) { + ref = undefined; + } + + const fileName = GitUri.relativeTo(uri, repoPath); + // TODO: Add caching + let data; + try { + data = await Git.log__file(repoPath, fileName, ref, { + firstParent: firstParent, + format: 'simple', + limit: skip + 2, + ordering: this.container.config.advanced.commitOrdering, + startLine: editorLine != null ? editorLine + 1 : undefined, + }); + } catch (ex) { + const msg: string = ex?.toString() ?? emptyStr; + // 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); + if (status?.indexStatus != null) { + return GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged); + } + } + + ref = await Git.log__file_recent(repoPath, fileName, { + ordering: this.container.config.advanced.commitOrdering, + }); + return GitUri.fromFile(fileName, repoPath, ref ?? GitRevision.deletedOrMissing); + } + + Logger.error(ex, cc); + throw ex; + } + if (data == null || data.length === 0) return undefined; + + const [previousRef, file] = GitLogParser.parseSimple(data, skip, ref); + // 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); + } + + async getPullRequestForBranch( + branch: string, + remote: GitRemote, + options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, + ): Promise; + async getPullRequestForBranch( + branch: string, + provider: RichRemoteProvider, + options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, + ): Promise; + @gate((ref, remoteOrProvider, options) => { + const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; + return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}${ + options != null ? `|${options.limit ?? -1}:${options.include?.join(',')}` : '' + }`; + }) + @debug({ args: { 1: remoteOrProvider => remoteOrProvider.name } }) + async getPullRequestForBranch( + branch: string, + remoteOrProvider: GitRemote | RichRemoteProvider, + { + timeout, + ...options + }: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number } = {}, + ): Promise { + let provider; + if (GitRemote.is(remoteOrProvider)) { + ({ provider } = remoteOrProvider); + if (!provider?.hasApi()) return undefined; + } else { + provider = remoteOrProvider; + } + + let promiseOrPR = provider.getPullRequestForBranch(branch, options); + if (promiseOrPR == null || !Promises.is(promiseOrPR)) { + return promiseOrPR; + } + + if (timeout != null && timeout > 0) { + promiseOrPR = Promises.cancellable(promiseOrPR, timeout); + } + + try { + return await promiseOrPR; + } catch (ex) { + if (ex instanceof Promises.CancellationError) { + throw ex; + } + + return undefined; + } + } + + async getPullRequestForCommit( + ref: string, + remote: GitRemote, + options?: { timeout?: number }, + ): Promise; + async getPullRequestForCommit( + ref: string, + provider: RichRemoteProvider, + options?: { timeout?: number }, + ): Promise; + @gate((ref, remoteOrProvider, options) => { + const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; + return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}|${ + options?.timeout + }`; + }) + @debug({ args: { 1: remoteOrProvider => remoteOrProvider.name } }) + async getPullRequestForCommit( + ref: string, + remoteOrProvider: GitRemote | RichRemoteProvider, + { timeout }: { timeout?: number } = {}, + ): Promise { + if (GitRevision.isUncommitted(ref)) return undefined; + + let provider; + if (GitRemote.is(remoteOrProvider)) { + ({ provider } = remoteOrProvider); + if (!provider?.hasApi()) return undefined; + } else { + provider = remoteOrProvider; + } + + let promiseOrPR = provider.getPullRequestForCommit(ref); + if (promiseOrPR == null || !Promises.is(promiseOrPR)) { + return promiseOrPR; + } + + if (timeout != null && timeout > 0) { + promiseOrPR = Promises.cancellable(promiseOrPR, timeout); + } + + try { + return await promiseOrPR; + } catch (ex) { + if (ex instanceof Promises.CancellationError) { + throw ex; + } + + return undefined; + } + } + + @log() + async getIncomingActivity( + repoPath: string, + { + limit, + ...options + }: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number } = {}, + ): Promise { + const cc = Logger.getCorrelationContext(); + + limit = limit ?? this.container.config.advanced.maxListItems ?? 0; + try { + // Pass a much larger limit to reflog, because we aggregate the data and we won't know how many lines we'll need + const data = await Git.reflog(repoPath, { + ordering: this.container.config.advanced.commitOrdering, + ...options, + limit: limit * 100, + }); + if (data == null) return undefined; + + const reflog = GitReflogParser.parse(data, repoPath, reflogCommands, limit, limit * 100); + if (reflog?.hasMore) { + reflog.more = this.getReflogMoreFn(reflog, options); + } + + return reflog; + } catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + private getReflogMoreFn( + reflog: GitReflog, + options: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number }, + ): (limit: number) => Promise { + return async (limit: number | undefined) => { + limit = limit ?? this.container.config.advanced.maxSearchItems ?? 0; + + const moreLog = await this.getIncomingActivity(reflog.repoPath, { + ...options, + limit: limit, + skip: reflog.total, + }); + if (moreLog == null) { + // If we can't find any more, assume we have everything + return { ...reflog, hasMore: false }; + } + + const mergedLog: GitReflog = { + repoPath: reflog.repoPath, + records: [...reflog.records, ...moreLog.records], + count: reflog.count + moreLog.count, + total: reflog.total + moreLog.total, + limit: (reflog.limit ?? 0) + limit, + hasMore: moreLog.hasMore, + }; + mergedLog.more = this.getReflogMoreFn(mergedLog, options); + + return mergedLog; + }; + } + + async getRichRemoteProvider( + repoPath: string | undefined, + options?: { includeDisconnected?: boolean }, + ): Promise | undefined>; + async getRichRemoteProvider( + remotes: GitRemote[], + options?: { includeDisconnected?: boolean }, + ): Promise | undefined>; + @gate( + (remotesOrRepoPath, options) => + `${typeof remotesOrRepoPath === 'string' ? remotesOrRepoPath : remotesOrRepoPath[0]?.repoPath}:${ + options?.includeDisconnected ?? false + }`, + ) + @log({ + args: { + 0: remotesOrRepoPath => + Array.isArray(remotesOrRepoPath) ? remotesOrRepoPath.map(r => r.name).join(',') : remotesOrRepoPath, + }, + }) + async getRichRemoteProvider( + remotesOrRepoPath: GitRemote[] | string | undefined, + { includeDisconnected }: { includeDisconnected?: boolean } = {}, + ): Promise | undefined> { + if (remotesOrRepoPath == null) return undefined; + + const cacheKey = `${includeDisconnected ? 'disconnected|' : ''}${ + typeof remotesOrRepoPath === 'string' ? remotesOrRepoPath : remotesOrRepoPath[0]?.repoPath + }`; + if (cacheKey != null) { + const remote = this._remotesWithApiProviderCache.get(cacheKey); + if (remote !== undefined) return remote ?? undefined; + } + + const remotes = ( + typeof remotesOrRepoPath === 'string' ? await this.getRemotes(remotesOrRepoPath) : remotesOrRepoPath + ).filter(r => r.provider != null); + + if (remotes.length === 0) return undefined; + + let remote; + if (remotes.length === 1) { + remote = remotes[0]; + } else { + const weightedRemotes = new Map([ + ['upstream', 15], + ['origin', 10], + ]); + + const branch = await this.getBranch(remotes[0].repoPath); + const branchRemote = branch?.getRemoteName(); + + if (branchRemote != null) { + weightedRemotes.set(branchRemote, 100); + } + + let bestRemote; + let weight = 0; + for (const r of remotes) { + if (r.default) { + bestRemote = r; + break; + } + + // Don't choose a remote unless its weighted above + const matchedWeight = weightedRemotes.get(r.name) ?? -1; + if (matchedWeight > weight) { + bestRemote = r; + weight = matchedWeight; + } + } + + remote = bestRemote ?? null; + } + + if (!remote?.provider?.hasApi()) { + if (cacheKey != null) { + this._remotesWithApiProviderCache.set(cacheKey, null); + } + return undefined; + } + + const { provider } = remote; + if (!includeDisconnected) { + const connected = provider.maybeConnected ?? (await provider.isConnected()); + if (!connected) { + if (cacheKey != null) { + this._remotesWithApiProviderCache.set(cacheKey, null); + } + return undefined; + } + } + + if (cacheKey != null) { + this._remotesWithApiProviderCache.set(cacheKey, remote as GitRemote); + } + return remote as GitRemote; + } + + @log() + async getRemotes( + repoPath: string | undefined, + options: { sort?: boolean } = {}, + ): Promise[]> { + if (repoPath == null) return []; + + const repository = await this.container.git.getRepository(repoPath); + const remotes = await (repository != null + ? repository.getRemotes({ sort: options.sort }) + : this.getRemotesCore(repoPath, undefined, { sort: options.sort })); + + return remotes.filter(r => r.provider != null) as GitRemote[]; + } + + async getRemotesCore( + repoPath: string | undefined, + providers?: RemoteProviders, + options: { sort?: boolean } = {}, + ): Promise { + if (repoPath == null) return []; + + providers = providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null)); + + try { + const data = await Git.remote(repoPath); + const remotes = GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providers)); + if (remotes == null) return []; + + if (options.sort) { + GitRemote.sort(remotes); + } + + return remotes; + } catch (ex) { + Logger.error(ex); + return []; + } + } + + @gate() + @debug() + async getRepoPath(filePath: string, isDirectory?: boolean): Promise { + const cc = Logger.getCorrelationContext(); + + let repoPath: string | undefined; + try { + let path: string; + if (isDirectory) { + path = filePath; + } else { + const stat = await new Promise(resolve => + fs.stat(filePath, (err, stat) => resolve(err == null ? stat : undefined)), + ); + path = stat?.isDirectory() ? filePath : paths.dirname(filePath); + } + + repoPath = await Git.rev_parse__show_toplevel(path); + if (repoPath == null) return repoPath; + + if (isWindows) { + // On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped drive path back, you get the UNC path for the mapped drive. + // So try to normalize it back to the mapped drive path, if possible + + const repoUri = Uri.file(repoPath); + const pathUri = Uri.file(path); + if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) { + const match = driveLetterRegex.exec(pathUri.path); + if (match != null) { + const [, letter] = match; + + try { + const networkPath = await new Promise(resolve => + fs.realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) => + resolve(err != null ? undefined : resolvedPath), + ), + ); + if (networkPath != null) { + repoPath = Strings.normalizePath( + repoUri.fsPath.replace( + networkPath, + `${letter.toLowerCase()}:${networkPath.endsWith('\\') ? '\\' : ''}`, + ), + ); + return repoPath; + } + } catch {} + } + + repoPath = Strings.normalizePath(pathUri.fsPath); + } + + return repoPath; + } + + // If we are not on Windows (symlinks don't seem to have the same issue on Windows), check if we are a symlink and if so, use the symlink path (not its resolved path) + // This is because VS Code will provide document Uris using the symlinked path + repoPath = await new Promise(resolve => { + fs.realpath(path, { encoding: 'utf8' }, (err, resolvedPath) => { + if (err != null) { + Logger.debug(cc, `fs.realpath failed; repoPath=${repoPath}`); + resolve(repoPath); + return; + } + + if (Strings.equalsIgnoreCase(path, resolvedPath)) { + Logger.debug(cc, `No symlink detected; repoPath=${repoPath}`); + resolve(repoPath); + return; + } + + const linkPath = Strings.normalizePath(resolvedPath, { stripTrailingSlash: true }); + repoPath = repoPath!.replace(linkPath, path); + Logger.debug( + cc, + `Symlink detected; repoPath=${repoPath}, path=${path}, resolvedPath=${resolvedPath}`, + ); + resolve(repoPath); + }); + }); + + return repoPath; + } catch (ex) { + Logger.error(ex, cc); + repoPath = undefined; + return repoPath; + } finally { + if (repoPath) { + void this.ensureProperWorkspaceCasing(repoPath, filePath); + } + } + } + + @gate(() => '') + private async ensureProperWorkspaceCasing(repoPath: string, filePath: string) { + if (this.container.config.advanced.messages.suppressImproperWorkspaceCasingWarning) return; + + filePath = filePath.replace(/\\/g, '/'); + + let regexPath; + let testPath; + if (filePath > repoPath) { + regexPath = filePath; + testPath = repoPath; + } else { + testPath = filePath; + regexPath = repoPath; + } + + let pathRegex = new RegExp(`^${regexPath}`); + if (!pathRegex.test(testPath)) { + pathRegex = new RegExp(pathRegex, 'i'); + if (pathRegex.test(testPath)) { + await Messages.showIncorrectWorkspaceCasingWarningMessage(); + } + } + } + + @gate() + @log() + async getStash(repoPath: string | undefined): Promise { + if (repoPath == null) return undefined; + + let stash = this.useCaching ? this._stashesCache.get(repoPath) : undefined; + if (stash === undefined) { + const data = await Git.stash__list(repoPath, { + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + stash = GitStashParser.parse(data, repoPath); + + if (this.useCaching) { + this._stashesCache.set(repoPath, stash ?? null); + + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { + this._stashesCache.delete(repoPath); + } + } + } + + return stash ?? undefined; + } + + @log() + async getStatusForFile(repoPath: string, fileName: string): Promise { + const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; + + const data = await Git.status__file(repoPath, fileName, porcelainVersion, { + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + if (status == null || !status.files.length) return undefined; + + return status.files[0]; + } + + @log() + async getStatusForFiles(repoPath: string, path: string): Promise { + const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; + + const data = await Git.status__file(repoPath, path, porcelainVersion, { + similarityThreshold: this.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 { + if (repoPath == null) return undefined; + + const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; + + const data = await Git.status(repoPath, porcelainVersion, { + similarityThreshold: this.container.config.advanced.similarityThreshold, + }); + const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + + if (status?.detached) { + const rebaseStatus = await this.getRebaseStatus(repoPath); + if (rebaseStatus != null) { + return new GitStatus( + repoPath, + rebaseStatus.incoming.name, + status.sha, + status.files, + status.state, + status.upstream, + true, + ); + } + } + return status; + } + + @log({ args: { 1: false } }) + async getTags( + repoPath: string | undefined, + options: { filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions } = {}, + ): Promise> { + if (repoPath == null) return { values: [] }; + + let tagsPromise = this.useCaching ? this._tagsCache.get(repoPath) : undefined; + if (tagsPromise == null) { + async function load(this: LocalGitProvider) { + try { + const data = await Git.tag(repoPath!); + return GitTagParser.parse(data, repoPath!) ?? []; + } catch (ex) { + this._tagsCache.delete(repoPath!); + + return []; + } + } + + tagsPromise = load.call(this); + + if (this.useCaching) { + this._tagsCache.set(repoPath, tagsPromise); + + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { + this._tagsCache.delete(repoPath); + } + } + } + + let tags = await tagsPromise; + if (options.filter != null) { + tags = tags.filter(options.filter); + } + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (options.sort) { + GitTag.sort(tags, typeof options.sort === 'boolean' ? undefined : options.sort); + } + + return { values: tags }; + } + + @log() + async getTreeFileForRevision(repoPath: string, fileName: string, ref: string): Promise { + if (repoPath == null || fileName == null || fileName.length === 0) return undefined; + + const data = await Git.ls_tree(repoPath, ref, { fileName: fileName }); + const trees = GitTreeParser.parse(data); + return trees?.length ? trees[0] : undefined; + } + + @log() + async getTreeForRevision(repoPath: string, ref: string): Promise { + if (repoPath == null) return []; + + const data = await Git.ls_tree(repoPath, ref); + return GitTreeParser.parse(data) ?? []; + } + + @log() + getVersionedFileBuffer(repoPath: string, fileName: string, ref: string): Promise { + return Git.show(repoPath, fileName, ref, { encoding: 'buffer' }); + } + + @log() + async getVersionedUri( + repoPath: string | undefined, + 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.resolveToUri(fileName, repoPath); + return (await fsExists(uri.fsPath)) ? uri : undefined; + } + + @log({ args: { 1: false } }) + async hasBranchOrTag( + repoPath: string | undefined, + { + filter, + }: { + filter?: { branches?: (b: GitBranch) => boolean; tags?: (t: GitTag) => boolean }; + } = {}, + ) { + const [{ values: branches }, { values: tags }] = await Promise.all([ + this.getBranches(repoPath, { + filter: filter?.branches, + sort: false, + }), + this.getTags(repoPath, { + filter: filter?.tags, + sort: false, + }), + ]); + + return branches.length !== 0 || tags.length !== 0; + } + + @log() + async hasRemotes(repoPath: string | undefined): Promise { + if (repoPath == null) return false; + + const repository = await this.container.git.getRepository(repoPath); + if (repository == null) return false; + + return repository.hasRemotes(); + } + + @log() + async hasTrackingBranch(repoPath: string | undefined): Promise { + if (repoPath == null) return false; + + const repository = await this.container.git.getRepository(repoPath); + if (repository == null) return false; + + return repository.hasUpstreamBranch(); + } + + @log({ + args: { 1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) }, + }) + async isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise { + if (repoPath == null) return false; + + editor = editor ?? window.activeTextEditor; + if (editor == null) return false; + + const doc = await this.container.tracker.getOrAdd(editor.document.uri); + return repoPath === doc?.uri.repoPath; + } + + isTrackable(uri: Uri): boolean { + const { scheme } = uri; + return ( + scheme === DocumentSchemes.File || + scheme === DocumentSchemes.Git || + scheme === DocumentSchemes.GitLens || + scheme === DocumentSchemes.PRs || + scheme === DocumentSchemes.Vsls + ); + } + + private async isTracked(filePath: string, repoPath?: string, ref?: string): Promise; + private async isTracked(uri: GitUri): Promise; + @log({ exit: tracked => `returned ${tracked}` /*, singleLine: true }*/ }) + private async isTracked(filePathOrUri: string | GitUri, repoPath?: string, ref?: string): Promise { + let cacheKey: string; + let relativeFilePath: string; + + if (typeof filePathOrUri === 'string') { + if (ref === GitRevision.deletedOrMissing) return false; + + cacheKey = ref ? `${ref}:${Strings.normalizePath(filePathOrUri)}` : Strings.normalizePath(filePathOrUri); + [relativeFilePath, repoPath] = Paths.splitPath(filePathOrUri, repoPath); + } else { + if (!this.isTrackable(filePathOrUri)) return false; + + // Always use the ref of the GitUri + ref = filePathOrUri.sha; + cacheKey = ref + ? `${ref}:${Strings.normalizePath(filePathOrUri.fsPath)}` + : Strings.normalizePath(filePathOrUri.fsPath); + relativeFilePath = filePathOrUri.fsPath; + repoPath = filePathOrUri.repoPath; + } + + if (ref != null) { + cacheKey = `${ref}:${cacheKey}`; + } + + let tracked = this._trackedCache.get(cacheKey); + if (tracked != null) return tracked; + + tracked = this.isTrackedCore(relativeFilePath, repoPath ?? emptyStr, ref); + this._trackedCache.set(cacheKey, tracked); + + tracked = await tracked; + this._trackedCache.set(cacheKey, tracked); + return tracked; + } + + @debug() + private async isTrackedCore(fileName: string, repoPath: string, ref?: string) { + if (ref === GitRevision.deletedOrMissing) return false; + + try { + // Even if we have a ref, check first to see if the file exists (that way the cache will be better reused) + let tracked = Boolean(await Git.ls_files(repoPath, fileName)); + if (!tracked && ref != null && !GitRevision.isUncommitted(ref)) { + tracked = Boolean(await Git.ls_files(repoPath, fileName, { ref: ref })); + // If we still haven't found this file, make sure it wasn't deleted in that ref (i.e. check the previous) + if (!tracked) { + tracked = Boolean(await Git.ls_files(repoPath, fileName, { ref: `${ref}^` })); + } + } + return tracked; + } catch (ex) { + Logger.error(ex); + return false; + } + } + + @log() + async getDiffTool(repoPath?: string): Promise { + return ( + (await Git.config__get('diff.guitool', repoPath, { local: true })) ?? + Git.config__get('diff.tool', repoPath, { local: true }) + ); + } + + @log() + async openDiffTool( + repoPath: string, + uri: Uri, + options: { ref1?: string; ref2?: string; staged?: boolean; tool?: string } = {}, + ): Promise { + try { + if (!options.tool) { + const cc = Logger.getCorrelationContext(); + + options.tool = this.container.config.advanced.externalDiffTool || (await this.getDiffTool(repoPath)); + if (options.tool == null) throw new Error('No diff tool found'); + + Logger.log(cc, `Using tool=${options.tool}`); + } + + const { tool, ...opts } = options; + await Git.difftool(repoPath, uri.fsPath, tool, opts); + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) { + const viewDocs = 'View Git Docs'; + const result = await window.showWarningMessage( + 'Unable to open changes because the specified diff tool cannot be found or no Git diff tool is configured', + viewDocs, + ); + if (result === viewDocs) { + void env.openExternal( + Uri.parse('https://git-scm.com/docs/git-config#Documentation/git-config.txt-difftool'), + ); + } + + return; + } + + Logger.error(ex, 'openDiffTool'); + void Messages.showGenericErrorMessage('Unable to open compare'); + } + } + + @log() + async openDirectoryCompare(repoPath: string, ref1: string, ref2?: string, tool?: string): Promise { + try { + if (!tool) { + const cc = Logger.getCorrelationContext(); + + tool = this.container.config.advanced.externalDirectoryDiffTool || (await this.getDiffTool(repoPath)); + if (tool == null) throw new Error('No diff tool found'); + + Logger.log(cc, `Using tool=${tool}`); + } + + await Git.difftool__dir_diff(repoPath, tool, ref1, ref2); + } catch (ex) { + const msg: string = ex?.toString() ?? ''; + if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) { + const viewDocs = 'View Git Docs'; + const result = await window.showWarningMessage( + 'Unable to open directory compare because the specified diff tool cannot be found or no Git diff tool is configured', + viewDocs, + ); + if (result === viewDocs) { + void env.openExternal( + Uri.parse('https://git-scm.com/docs/git-config#Documentation/git-config.txt-difftool'), + ); + } + + return; + } + + Logger.error(ex, 'openDirectoryCompare'); + void Messages.showGenericErrorMessage('Unable to open directory compare'); + } + } + + async resolveReference( + repoPath: string, + ref: string, + fileName?: string, + options?: { timeout?: number }, + ): Promise; + async resolveReference(repoPath: string, ref: string, uri?: Uri, options?: { timeout?: number }): Promise; + @log() + async resolveReference( + repoPath: string, + ref: string, + fileNameOrUri?: string | Uri, + options?: { timeout?: number }, + ) { + if (ref == null || ref.length === 0 || ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) { + return ref; + } + + if (fileNameOrUri == null) { + if (GitRevision.isSha(ref) || !GitRevision.isShaLike(ref) || ref.endsWith('^3')) return ref; + + return (await Git.rev_parse__verify(repoPath, ref)) ?? ref; + } + + const fileName = + typeof fileNameOrUri === 'string' + ? fileNameOrUri + : Strings.normalizePath(paths.relative(repoPath, fileNameOrUri.fsPath)); + + const blob = await Git.rev_parse__verify(repoPath, ref, fileName); + if (blob == null) return GitRevision.deletedOrMissing; + + let promise: Promise = Git.log__find_object( + repoPath, + blob, + ref, + this.container.config.advanced.commitOrdering, + fileName, + ); + if (options?.timeout != null) { + promise = Promise.race([promise, Functions.wait(options.timeout)]); + } + + return (await promise) ?? ref; + } + + @log() + validateBranchOrTagName(repoPath: string, ref: string): Promise { + return Git.check_ref_format(ref, repoPath); + } + + @log() + async validateReference(repoPath: string, ref: string): Promise { + if (ref == null || ref.length === 0) return false; + if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return true; + + return (await Git.rev_parse__verify(repoPath, ref)) != null; + } + + stageFile(repoPath: string, fileName: string): Promise; + stageFile(repoPath: string, uri: Uri): Promise; + @log() + async stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { + await Git.add( + repoPath, + typeof fileNameOrUri === 'string' ? fileNameOrUri : Paths.splitPath(fileNameOrUri.fsPath, repoPath)[0], + ); + } + + stageDirectory(repoPath: string, directory: string): Promise; + stageDirectory(repoPath: string, uri: Uri): Promise; + @log() + async stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { + await Git.add( + repoPath, + typeof directoryOrUri === 'string' ? directoryOrUri : Paths.splitPath(directoryOrUri.fsPath, repoPath)[0], + ); + } + + unStageFile(repoPath: string, fileName: string): Promise; + unStageFile(repoPath: string, uri: Uri): Promise; + @log() + async unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { + await Git.reset( + repoPath, + typeof fileNameOrUri === 'string' ? fileNameOrUri : Paths.splitPath(fileNameOrUri.fsPath, repoPath)[0], + ); + } + + unStageDirectory(repoPath: string, directory: string): Promise; + unStageDirectory(repoPath: string, uri: Uri): Promise; + @log() + async unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { + await Git.reset( + repoPath, + typeof directoryOrUri === 'string' ? directoryOrUri : Paths.splitPath(directoryOrUri.fsPath, repoPath)[0], + ); + } + + @log() + async stashApply( + repoPath: string, + stashName: string, + { deleteAfter }: { deleteAfter?: boolean } = {}, + ): Promise { + try { + await Git.stash__apply(repoPath, stashName, Boolean(deleteAfter)); + } catch (ex) { + if (ex instanceof Error) { + const msg: string = ex.message ?? ''; + if (msg.includes('Your local changes to the following files would be overwritten by merge')) { + throw new StashApplyError( + 'Unable to apply stash. Your working tree changes would be overwritten. Please commit or stash your changes before trying again', + StashApplyErrorReason.WorkingChanges, + ex, + ); + } + + if ( + (msg.includes('Auto-merging') && msg.includes('CONFLICT')) || + (ex instanceof RunError && + ((ex.stdout.includes('Auto-merging') && ex.stdout.includes('CONFLICT')) || + ex.stdout.includes('needs merge'))) + ) { + void window.showInformationMessage('Stash applied with conflicts'); + + return; + } + + throw new StashApplyError( + `Unable to apply stash \u2014 ${msg.trim().replace(/\n+?/g, '; ')}`, + undefined, + ex, + ); + } + + throw new StashApplyError(`Unable to apply stash \u2014 ${String(ex)}`, undefined, ex); + } + } + + @log() + async stashDelete(repoPath: string, stashName: string, ref?: string): Promise { + await Git.stash__delete(repoPath, stashName, ref); + } + + @log({ args: { 2: uris => uris?.length } }) + async stashSave( + repoPath: string, + message?: string, + uris?: Uri[], + options: { includeUntracked?: boolean; keepIndex?: boolean } = {}, + ): Promise { + if (uris == null) return Git.stash__push(repoPath, message, options); + + await this.ensureGitVersion( + '2.13.2', + 'Stashing individual files', + ' Please retry by stashing everything or install a more recent version of Git and try again.', + ); + + const pathspecs = uris.map(u => `./${Paths.splitPath(u.fsPath, repoPath)[0]}`); + + const stdinVersion = '2.30.0'; + const stdin = await Git.isAtLeastVersion(stdinVersion); + // If we don't support stdin, then error out if we are over the maximum allowed git cli length + if (!stdin && Arrays.countStringLength(pathspecs) > maxGitCliLength) { + await this.ensureGitVersion( + stdinVersion, + `Stashing so many files (${pathspecs.length}) at once`, + ' Please retry by stashing fewer files or install a more recent version of Git and try again.', + ); + } + + return Git.stash__push(repoPath, message, { + ...options, + pathspecs: pathspecs, + stdin: stdin, + }); + } + + private _scmGitApi: Promise | undefined; + private async getScmGitApi(): Promise { + return this._scmGitApi ?? (this._scmGitApi = this.getScmGitApiCore()); + } + + @log() + private async getScmGitApiCore(): Promise { + try { + const extension = extensions.getExtension('vscode.git'); + if (extension == null) return undefined; + + const gitExtension = extension.isActive ? extension.exports : await extension.activate(); + return gitExtension?.getAPI(1); + } catch { + return undefined; + } + } + + @log() + async getOpenScmRepositories(): Promise { + const cc = Logger.getCorrelationContext(); + try { + const gitApi = await this.getScmGitApi(); + return gitApi?.repositories ?? []; + } catch (ex) { + Logger.error(ex, cc); + return []; + } + } + + @log() + async getOrOpenScmRepository(repoPath: string): Promise { + const cc = Logger.getCorrelationContext(); + try { + const gitApi = await this.getScmGitApi(); + if (gitApi?.openRepository != null) { + return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined; + } + + return gitApi?.getRepository(Uri.file(repoPath)) ?? undefined; + } catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + @log() + private async openScmRepository(repoPath: string): Promise { + const cc = Logger.getCorrelationContext(); + try { + const gitApi = await this.getScmGitApi(); + return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined; + } catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + private async ensureGitVersion(version: string, prefix: string, suffix: string): Promise { + if (await Git.isAtLeastVersion(version)) return; + + throw new Error( + `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${await Git.version()}).${suffix}`, + ); + } +} diff --git a/src/env/node/git/locator.ts b/src/env/node/git/locator.ts new file mode 100644 index 0000000..bb544f4 --- /dev/null +++ b/src/env/node/git/locator.ts @@ -0,0 +1,141 @@ +'use strict'; +import * as paths from 'path'; +import { GlyphChars } from '../../../constants'; +import { LogLevel } from '../../../logger'; +import { Stopwatch } from '../../../system'; +import { findExecutable, run } from './shell'; + +export class UnableToFindGitError extends Error { + constructor(public readonly original?: Error) { + super('Unable to find git'); + + Error.captureStackTrace?.(this, UnableToFindGitError); + } +} + +export class InvalidGitConfigError extends Error { + constructor(public readonly original: Error) { + super('Invalid Git configuration'); + + Error.captureStackTrace?.(this, InvalidGitConfigError); + } +} + +export interface GitLocation { + path: string; + version: string; +} + +function parseVersion(raw: string): string { + return raw?.replace(/^git version /, ''); +} + +async function findSpecificGit(path: string): Promise { + const sw = new Stopwatch(`findSpecificGit(${path})`, { logLevel: LogLevel.Debug }); + + let version; + try { + version = await run(path, ['--version'], 'utf8'); + } catch (ex) { + sw.stop({ message: ` ${GlyphChars.Dot} Unable to find git` }); + + if (/bad config/i.test(ex.message)) throw new InvalidGitConfigError(ex); + throw ex; + } + + // If needed, let's update our path to avoid the search on every command + if (!path || path === 'git') { + const foundPath = findExecutable(path, ['--version']).cmd; + + // Ensure that the path we found works + try { + version = await run(foundPath, ['--version'], 'utf8'); + } catch (ex) { + sw.stop({ message: ` ${GlyphChars.Dot} Unable to find git` }); + + if (/bad config/i.test(ex.message)) throw new InvalidGitConfigError(ex); + throw ex; + } + + path = foundPath; + } + + sw.stop({ message: ` ${GlyphChars.Dot} Found git @ ${path}` }); + + return { + path: path, + version: parseVersion(version.trim()), + }; +} + +async function findGitDarwin(): Promise { + try { + let path = await run('which', ['git'], 'utf8'); + path = path.replace(/^\s+|\s+$/g, ''); + + if (path !== '/usr/bin/git') { + return findSpecificGit(path); + } + + try { + await run('xcode-select', ['-p'], 'utf8'); + return findSpecificGit(path); + } catch (ex) { + if (ex.code === 2) { + return Promise.reject(new UnableToFindGitError(ex)); + } + return findSpecificGit(path); + } + } catch (ex) { + return Promise.reject( + ex instanceof InvalidGitConfigError || ex instanceof UnableToFindGitError + ? ex + : new UnableToFindGitError(ex), + ); + } +} + +function findSystemGitWin32(basePath: string | null | undefined): Promise { + if (basePath == null || basePath.length === 0) return Promise.reject(new UnableToFindGitError()); + return findSpecificGit(paths.join(basePath, 'Git', 'cmd', 'git.exe')); +} + +function findGitWin32(): Promise { + return findSystemGitWin32(process.env['ProgramW6432']) + .then(null, () => findSystemGitWin32(process.env['ProgramFiles(x86)'])) + .then(null, () => findSystemGitWin32(process.env['ProgramFiles'])) + .then(null, () => findSpecificGit('git')); +} + +export async function findGitPath(paths?: string | string[]): Promise { + try { + if (paths == null || typeof paths === 'string') { + return await findSpecificGit(paths ?? 'git'); + } + + for (const path of paths) { + try { + return await findSpecificGit(path); + } catch {} + } + + throw new UnableToFindGitError(); + } catch { + try { + switch (process.platform) { + case 'darwin': + return await findGitDarwin(); + case 'win32': + return await findGitWin32(); + default: + return Promise.reject(new UnableToFindGitError()); + } + } catch (ex) { + return Promise.reject( + ex instanceof InvalidGitConfigError || ex instanceof UnableToFindGitError + ? ex + : new UnableToFindGitError(ex), + ); + } + } +} diff --git a/src/env/node/git/shell.ts b/src/env/node/git/shell.ts new file mode 100644 index 0000000..7687503 --- /dev/null +++ b/src/env/node/git/shell.ts @@ -0,0 +1,223 @@ +'use strict'; +import { ExecException, execFile } from 'child_process'; +import * as fs from 'fs'; +import * as paths from 'path'; +import * as iconv from 'iconv-lite'; +import { Logger } from '../../../logger'; + +export const isWindows = process.platform === 'win32'; + +const slashesRegex = /[\\/]/; +const ps1Regex = /\.ps1$/i; +const batOrCmdRegex = /\.(bat|cmd)$/i; +const jsRegex = /\.(js)$/i; + +/** + * Search PATH to see if a file exists in any of the path folders. + * + * @param {string} exe The file to search for + * @return {string} A fully qualified path, or the original path if nothing + * is found + * + * @private + */ +function runDownPath(exe: string): string { + // NB: Windows won't search PATH looking for executables in spawn like + // Posix does + + // Files with any directory path don't get this applied + if (slashesRegex.test(exe)) return exe; + + const target = paths.join('.', exe); + try { + const stats = fs.statSync(target); + if (stats?.isFile() && isExecutable(stats)) return target; + } catch {} + + const path = process.env.PATH; + if (path != null && path.length !== 0) { + const haystack = path.split(isWindows ? ';' : ':'); + let stats; + for (const p of haystack) { + const needle = paths.join(p, exe); + try { + stats = fs.statSync(needle); + if (stats?.isFile() && isExecutable(stats)) return needle; + } catch {} + } + } + + return exe; +} + +function isExecutable(stats: fs.Stats) { + if (isWindows) return true; + + const isGroup = stats.gid ? process.getgid != null && stats.gid === process.getgid() : true; + const isUser = stats.uid ? process.getuid != null && stats.uid === process.getuid() : true; + + return Boolean(stats.mode & 0o0001 || (stats.mode & 0o0010 && isGroup) || (stats.mode & 0o0100 && isUser)); +} + +/** + * Finds the executable and parameters to run on Windows. This method + * mimics the POSIX behavior of being able to run scripts as executables by + * replacing the passed-in executable with the script runner, for PowerShell, + * CMD, and node scripts. + * + * This method also does the work of running down PATH, which spawn on Windows + * also doesn't do, unlike on POSIX. + */ +export function findExecutable(exe: string, args: string[]): { cmd: string; args: string[] } { + // POSIX can just execute scripts directly, no need for silly goosery + if (!isWindows) return { cmd: runDownPath(exe), args: args }; + + if (!fs.existsSync(exe)) { + // NB: When you write something like `surf-client ... -- surf-build` on Windows, + // a shell would normally convert that to surf-build.cmd, but since it's passed + // in as an argument, it doesn't happen + const possibleExts = ['.exe', '.bat', '.cmd', '.ps1']; + for (const ext of possibleExts) { + const possibleFullPath = runDownPath(`${exe}${ext}`); + + if (fs.existsSync(possibleFullPath)) return findExecutable(possibleFullPath, args); + } + } + + if (ps1Regex.test(exe)) { + const cmd = paths.join( + process.env.SYSTEMROOT ?? 'C:\\WINDOWS', + 'System32', + 'WindowsPowerShell', + 'v1.0', + 'PowerShell.exe', + ); + const psargs = ['-ExecutionPolicy', 'Unrestricted', '-NoLogo', '-NonInteractive', '-File', exe]; + + return { cmd: cmd, args: psargs.concat(args) }; + } + + if (batOrCmdRegex.test(exe)) { + const cmd = paths.join(process.env.SYSTEMROOT ?? 'C:\\WINDOWS', 'System32', 'cmd.exe'); + const cmdArgs = ['/C', exe, ...args]; + + return { cmd: cmd, args: cmdArgs }; + } + + if (jsRegex.test(exe)) { + const cmd = process.execPath; + const nodeArgs = [exe]; + + return { cmd: cmd, args: nodeArgs.concat(args) }; + } + + return { cmd: exe, args: args }; +} + +export interface RunOptions { + cwd?: string; + readonly env?: Record; + readonly encoding?: TEncoding; + /** + * The size the output buffer to allocate to the spawned process. Set this + * if you are anticipating a large amount of output. + * + * If not specified, this will be 10MB (10485760 bytes) which should be + * enough for most Git operations. + */ + readonly maxBuffer?: number; + /** + * An optional string or buffer which will be written to + * the child process stdin stream immediately immediately + * after spawning the process. + */ + readonly stdin?: string | Buffer; + /** + * The encoding to use when writing to stdin, if the stdin + * parameter is a string. + */ + readonly stdinEncoding?: string; +} + +const bufferExceededRegex = /stdout maxBuffer( length)? exceeded/; + +export class RunError extends Error { + constructor( + private readonly original: ExecException, + public readonly stdout: string, + public readonly stderr: string, + ) { + super(original.message); + + stdout = stdout.trim(); + stderr = stderr.trim(); + Error.captureStackTrace?.(this, RunError); + } + + get cmd(): string | undefined { + return this.original.cmd; + } + + get killed(): boolean | undefined { + return this.original.killed; + } + + get code(): number | undefined { + return this.original.code; + } + + get signal(): NodeJS.Signals | undefined { + return this.original.signal; + } +} + +export function run( + command: string, + args: any[], + encoding: BufferEncoding | 'buffer' | string, + options: RunOptions = {}, +): Promise { + const { stdin, stdinEncoding, ...opts }: RunOptions = { maxBuffer: 100 * 1024 * 1024, ...options }; + + return new Promise((resolve, reject) => { + const proc = execFile(command, args, opts, (error: ExecException | null, stdout, stderr) => { + if (error != null) { + if (bufferExceededRegex.test(error.message)) { + error.message = `Command output exceeded the allocated stdout buffer. Set 'options.maxBuffer' to a larger value than ${opts.maxBuffer} bytes`; + } + + reject( + new RunError( + error, + encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer' + ? stdout + : iconv.decode(Buffer.from(stdout, 'binary'), encoding), + encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer' + ? stderr + : iconv.decode(Buffer.from(stderr, 'binary'), encoding), + ), + ); + + return; + } + + if (stderr) { + Logger.warn(`Warning(${command} ${args.join(' ')}): ${stderr}`); + } + + resolve( + encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer' + ? (stdout as TOut) + : (iconv.decode(Buffer.from(stdout, 'binary'), encoding) as TOut), + ); + }); + + if (stdin != null) { + proc.stdin?.end(stdin, (stdinEncoding ?? 'utf8') as BufferEncoding); + } + }); +} + +export function fsExists(path: string) { + return new Promise(resolve => fs.exists(path, exists => resolve(exists))); +} diff --git a/src/env/node/platform.ts b/src/env/node/platform.ts new file mode 100644 index 0000000..de9c4a6 --- /dev/null +++ b/src/env/node/platform.ts @@ -0,0 +1,4 @@ +import { env, UIKind } from 'vscode'; + +export const isWeb = env.uiKind === UIKind.Web; +export const isWindows = process.platform === 'win32'; diff --git a/src/extension.ts b/src/extension.ts index d09db39..49e1aef 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,14 +1,14 @@ 'use strict'; import { version as codeVersion, commands, env, ExtensionContext, extensions, window, workspace } from 'vscode'; -import type { CreatePullRequestActionContext, GitLensApi, OpenPullRequestActionContext } from '../src/api/gitlens'; import { Api } from './api/api'; +import type { CreatePullRequestActionContext, GitLensApi, OpenPullRequestActionContext } from './api/gitlens'; import { Commands, executeCommand, OpenPullRequestOnRemoteCommandArgs, registerCommands } from './commands'; import { CreatePullRequestOnRemoteCommandArgs } from './commands/createPullRequestOnRemote'; import { configuration, Configuration, OutputLevel } from './configuration'; import { ContextKeys, GlobalState, setContext, SyncedState } from './constants'; import { Container } from './container'; -import { GitBranch, GitCommit } from './git/git'; import { GitUri } from './git/gitUri'; +import { GitBranch, GitCommit } from './git/models'; import { Logger, LogLevel } from './logger'; import { Messages } from './messages'; import { registerPartnerActionRunners } from './partners'; diff --git a/src/git/commandOptions.ts b/src/git/commandOptions.ts new file mode 100644 index 0000000..35ea154 --- /dev/null +++ b/src/git/commandOptions.ts @@ -0,0 +1,23 @@ +'use strict'; + +export const enum GitErrorHandling { + Throw = 0, + Ignore = 1, +} + +export interface GitCommandOptions { + // extends RunOptions { + configs?: string[]; + readonly correlationKey?: string; + errors?: GitErrorHandling; + // Specifies that this command should always be executed locally if possible + local?: boolean; + + // Below options comes from RunOptions + cwd?: string; + readonly env?: Record; + readonly encoding?: BufferEncoding | 'buffer' | string; + readonly maxBuffer?: number; + readonly stdin?: string | Buffer; + readonly stdinEncoding?: string; +} diff --git a/src/git/errors.ts b/src/git/errors.ts index 34b5561..cd6467e 100644 --- a/src/git/errors.ts +++ b/src/git/errors.ts @@ -1,3 +1,4 @@ +'use strict'; import { Uri } from 'vscode'; import { GitProviderId, GitProviderService } from './gitProviderService'; @@ -15,3 +16,19 @@ export class ProviderNotFoundError extends Error { Error.captureStackTrace?.(this, ProviderNotFoundError); } } + +export const enum StashApplyErrorReason { + WorkingChanges = 1, +} + +export class StashApplyError extends Error { + constructor( + message: string, + public readonly reason: StashApplyErrorReason | undefined, + public readonly original?: Error, + ) { + super(message); + + Error.captureStackTrace?.(this, StashApplyError); + } +} diff --git a/src/git/formatters.ts b/src/git/formatters.ts new file mode 100644 index 0000000..ab07b49 --- /dev/null +++ b/src/git/formatters.ts @@ -0,0 +1,4 @@ +'use strict'; + +export * from './formatters/commitFormatter'; +export * from './formatters/statusFormatter'; diff --git a/src/git/formatters/commitFormatter.ts b/src/git/formatters/commitFormatter.ts index e534acb..474eff3 100644 --- a/src/git/formatters/commitFormatter.ts +++ b/src/git/formatters/commitFormatter.ts @@ -18,16 +18,9 @@ import { Container } from '../../container'; import { emojify } from '../../emojis'; import { Iterables, Promises, Strings } from '../../system'; import { ContactPresence } from '../../vsls/vsls'; -import { - GitCommit, - GitLogCommit, - GitRemote, - GitRevision, - IssueOrPullRequest, - PullRequest, - RemoteProvider, -} from '../git'; import { GitUri } from '../gitUri'; +import { GitCommit, GitLogCommit, GitRemote, GitRevision, IssueOrPullRequest, PullRequest } from '../models'; +import { RemoteProvider } from '../remotes/provider'; import { FormatOptions, Formatter } from './formatter'; const emptyStr = ''; diff --git a/src/git/formatters/formatters.ts b/src/git/formatters/formatters.ts deleted file mode 100644 index ff1a2c6..0000000 --- a/src/git/formatters/formatters.ts +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -export * from './commitFormatter'; -export * from './statusFormatter'; diff --git a/src/git/fsProvider.ts b/src/git/fsProvider.ts index 8fcfa64..e8763ae 100644 --- a/src/git/fsProvider.ts +++ b/src/git/fsProvider.ts @@ -14,9 +14,9 @@ import { } from 'vscode'; import { DocumentSchemes } from '../constants'; import { Container } from '../container'; -import { GitRevision, GitTree } from '../git/git'; import { GitUri } from '../git/gitUri'; import { debug, Iterables, Strings, TernarySearchTree } from '../system'; +import { GitRevision, GitTree } from './models'; const emptyArray = new Uint8Array(0); diff --git a/src/git/git.ts b/src/git/git.ts deleted file mode 100644 index 3587d84..0000000 --- a/src/git/git.ts +++ /dev/null @@ -1,1569 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -'use strict'; -import * as paths from 'path'; -import { Uri, window, workspace } from 'vscode'; -import { hrtime } from '@env/hrtime'; -import { GlyphChars } from '../constants'; -import { Container } from '../container'; -import { Logger } from '../logger'; -import { Paths, Strings, Versions } from '../system'; -import { GitLocation } from './locator'; -import { GitRevision } from './models/models'; -import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from './parsers/parsers'; -import { fsExists, run, RunError, RunOptions } from './shell'; - -export * from './models/models'; -export * from './parsers/parsers'; -export * from './formatters/formatters'; -export * from './remotes/provider'; -export * from './search'; -export { RunError } from './shell'; - -export type GitDiffFilter = 'A' | 'C' | 'D' | 'M' | 'R' | 'T' | 'U' | 'X' | 'B' | '*'; - -const emptyArray = Object.freeze([]) as unknown as any[]; -const emptyObj = Object.freeze({}); -const emptyStr = ''; -export const maxGitCliLength = 30000; - -const textDecoder = new TextDecoder('utf8'); - -// This is a root sha of all git repo's if using sha1 -const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; - -export const GitErrors = { - badRevision: /bad revision '(.*?)'/i, - noFastForward: /\(non-fast-forward\)/i, - noMergeBase: /no merge base/i, - notAValidObjectName: /Not a valid object name/i, - invalidLineCount: /file .+? has only \d+ lines/i, -}; - -const GitWarnings = { - notARepository: /Not a git repository/i, - outsideRepository: /is outside repository/i, - noPath: /no such path/i, - noCommits: /does not have any commits/i, - notFound: /Path '.*?' does not exist in/i, - foundButNotInRevision: /Path '.*?' exists on disk, but not in/i, - headNotABranch: /HEAD does not point to a branch/i, - noUpstream: /no upstream configured for branch '(.*?)'/i, - unknownRevision: - /ambiguous argument '.*?': unknown revision or path not in the working tree|not stored as a remote-tracking branch/i, - mustRunInWorkTree: /this operation must be run in a work tree/i, - patchWithConflicts: /Applied patch to '.*?' with conflicts/i, - noRemoteRepositorySpecified: /No remote repository specified\./i, - remoteConnectionError: /Could not read from remote repository/i, - notAGitCommand: /'.+' is not a git command/i, -}; - -export const enum GitErrorHandling { - Ignore = 'ignore', - Throw = 'throw', -} - -export interface GitCommandOptions extends RunOptions { - configs?: string[]; - readonly correlationKey?: string; - errors?: GitErrorHandling; - // Specifies that this command should always be executed locally if possible - local?: boolean; -} - -// A map of running git commands -- avoids running duplicate overlaping commands -const pendingCommands = new Map>(); - -export async function git(options: GitCommandOptions, ...args: any[]): Promise { - if (Container.instance.vsls.isMaybeGuest) { - if (options.local !== true) { - const guest = await Container.instance.vsls.guest(); - if (guest !== undefined) { - return guest.git(options, ...args); - } - } else { - // Since we will have a live share path here, just blank it out - options.cwd = emptyStr; - } - } - - const start = hrtime(); - - const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options; - - const runOpts: RunOptions = { - ...opts, - encoding: (encoding ?? 'utf8') === 'utf8' ? 'utf8' : 'buffer', - // Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581 - // Shouldn't *really* be needed but better safe than sorry - env: { - ...process.env, - ...(options.env ?? emptyObj), - GCM_INTERACTIVE: 'NEVER', - GCM_PRESERVE_CREDS: 'TRUE', - LC_ALL: 'C', - }, - }; - - const gitCommand = `[${runOpts.cwd}] git ${args.join(' ')}`; - - const command = `${correlationKey !== undefined ? `${correlationKey}:` : emptyStr}${gitCommand}`; - - let waiting; - let promise = pendingCommands.get(command); - if (promise === undefined) { - waiting = false; - - // Fixes https://github.com/eamodio/vscode-gitlens/issues/73 & https://github.com/eamodio/vscode-gitlens/issues/161 - // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x - args.splice( - 0, - 0, - '-c', - 'core.quotepath=false', - '-c', - 'color.ui=false', - ...(configs !== undefined ? configs : emptyArray), - ); - - if (process.platform === 'win32') { - args.splice(0, 0, '-c', 'core.longpaths=true'); - } - - promise = run(await Git.path(), args, encoding ?? 'utf8', runOpts); - - pendingCommands.set(command, promise); - } else { - waiting = true; - Logger.debug(`${gitCommand} ${GlyphChars.Dot} waiting...`); - } - - let exception: Error | undefined; - try { - return (await promise) as TOut; - } catch (ex) { - exception = ex; - - switch (errorHandling) { - case GitErrorHandling.Ignore: - exception = undefined; - return emptyStr as TOut; - - case GitErrorHandling.Throw: - throw ex; - - default: { - const result = defaultExceptionHandler(ex, options.cwd, start); - exception = undefined; - return result as TOut; - } - } - } finally { - pendingCommands.delete(command); - - const duration = Strings.getDurationMilliseconds(start); - const elapsed = `${duration} ms ${waiting ? '(waited) ' : emptyStr}`; - if (exception != null) { - Logger.error( - '', - `[${runOpts.cwd}] Git ${(exception.message || exception.toString() || emptyStr) - .trim() - .replace(/fatal: /g, '') - .replace(/\r?\n|\r/g, ` ${GlyphChars.Dot} `)} ${GlyphChars.Dot} ${elapsed}`, - ); - } else if (duration > Logger.slowCallWarningThreshold) { - Logger.warn(`${gitCommand} ${GlyphChars.Dot} ${elapsed} (slow)`); - } else { - Logger.log(`${gitCommand} ${GlyphChars.Dot} ${elapsed}`); - } - Logger.logGitCommand( - `${gitCommand} ${GlyphChars.Dot} ${exception !== undefined ? 'FAILED ' : emptyStr}${elapsed}`, - duration, - exception, - ); - } -} - -function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [number, number]): string { - const msg = ex.message || ex.toString(); - if (msg != null && msg.length !== 0) { - for (const warning of Object.values(GitWarnings)) { - if (warning.test(msg)) { - const duration = start !== undefined ? `${Strings.getDurationMilliseconds(start)} ms` : emptyStr; - Logger.warn( - `[${cwd}] Git ${msg - .trim() - .replace(/fatal: /g, '') - .replace(/\r?\n|\r/g, ` ${GlyphChars.Dot} `)} ${GlyphChars.Dot} ${duration}`, - ); - return emptyStr; - } - } - - const match = GitErrors.badRevision.exec(msg); - if (match != null) { - const [, ref] = match; - - // Since looking up a ref with ^3 (e.g. looking for untracked files in a stash) can error on some versions of git just ignore it - if (ref?.endsWith('^3')) return emptyStr; - } - } - - throw ex; -} - -export namespace Git { - let gitLocator!: () => Promise; - export function setLocator(locator: () => Promise): void { - gitLocator = locator; - } - - export async function path(): Promise { - return (await gitLocator()).path; - } - - export async function version(): Promise { - return (await gitLocator()).version; - } - - export async function isAtLeastVersion(minimum: string): Promise { - const result = Versions.compare(Versions.fromString(await Git.version()), Versions.fromString(minimum)); - return result !== -1; - } - - // Git commands - - export function add(repoPath: string | undefined, pathspec: string) { - return git({ cwd: repoPath }, 'add', '-A', '--', pathspec); - } - - export function apply(repoPath: string | undefined, patch: string, options: { allowConflicts?: boolean } = {}) { - const params = ['apply', '--whitespace=warn']; - if (options.allowConflicts) { - params.push('-3'); - } - return git({ cwd: repoPath, stdin: patch }, ...params); - } - - const ignoreRevsFileMap = new Map(); - - export async function blame( - repoPath: string | undefined, - fileName: string, - ref?: string, - options: { args?: string[] | null; ignoreWhitespace?: boolean; startLine?: number; endLine?: number } = {}, - ) { - const [file, root] = Paths.splitPath(fileName, repoPath); - - const params = ['blame', '--root', '--incremental']; - - if (options.ignoreWhitespace) { - params.push('-w'); - } - if (options.startLine != null && options.endLine != null) { - params.push(`-L ${options.startLine},${options.endLine}`); - } - if (options.args != null) { - params.push(...options.args); - - const index = params.indexOf('--ignore-revs-file'); - if (index !== -1) { - // Ensure the version of Git supports the --ignore-revs-file flag, otherwise the blame will fail - let supported = await Git.isAtLeastVersion('2.23'); - if (supported) { - let ignoreRevsFile = params[index + 1]; - if (!paths.isAbsolute(ignoreRevsFile)) { - ignoreRevsFile = paths.join(repoPath ?? emptyStr, ignoreRevsFile); - } - - const exists = ignoreRevsFileMap.get(ignoreRevsFile); - if (exists !== undefined) { - supported = exists; - } else { - // Ensure the specified --ignore-revs-file exists, otherwise the blame will fail - try { - supported = await fsExists(ignoreRevsFile); - } catch { - supported = false; - } - - ignoreRevsFileMap.set(ignoreRevsFile, supported); - } - } - - if (!supported) { - params.splice(index, 2); - } - } - } - - let stdin; - if (ref) { - if (GitRevision.isUncommittedStaged(ref)) { - // Pipe the blame contents to stdin - params.push('--contents', '-'); - - // Get the file contents for the staged version using `:` - stdin = await Git.show(repoPath, fileName, ':'); - } else { - params.push(ref); - } - } - - return git({ cwd: root, stdin: stdin }, ...params, '--', file); - } - - export function blame__contents( - repoPath: string | undefined, - fileName: string, - contents: string, - options: { - args?: string[] | null; - correlationKey?: string; - ignoreWhitespace?: boolean; - startLine?: number; - endLine?: number; - } = {}, - ) { - const [file, root] = Paths.splitPath(fileName, repoPath); - - const params = ['blame', '--root', '--incremental']; - - if (options.ignoreWhitespace) { - params.push('-w'); - } - if (options.startLine != null && options.endLine != null) { - params.push(`-L ${options.startLine},${options.endLine}`); - } - if (options.args != null) { - params.push(...options.args); - } - - // Pipe the blame contents to stdin - params.push('--contents', '-'); - - return git( - { cwd: root, stdin: contents, correlationKey: options.correlationKey }, - ...params, - '--', - file, - ); - } - - export function branch__containsOrPointsAt( - repoPath: string, - ref: string, - { - mode = 'contains', - name = undefined, - remotes = false, - }: { mode?: 'contains' | 'pointsAt'; name?: string; remotes?: boolean } = {}, - ) { - const params = ['branch']; - if (remotes) { - params.push('-r'); - } - params.push(mode === 'pointsAt' ? `--points-at=${ref}` : `--contains=${ref}`, '--format=%(refname:short)'); - if (name != null) { - params.push(name); - } - - return git( - { cwd: repoPath, configs: ['-c', 'color.branch=false'], errors: GitErrorHandling.Ignore }, - ...params, - ); - } - - export function check_ignore(repoPath: string, ...files: string[]) { - return git( - { cwd: repoPath, errors: GitErrorHandling.Ignore, stdin: files.join('\0') }, - 'check-ignore', - '-z', - '--stdin', - ); - } - - export function check_mailmap(repoPath: string, author: string) { - return git({ cwd: repoPath, errors: GitErrorHandling.Ignore, local: true }, 'check-mailmap', author); - } - - export async function check_ref_format( - ref: string, - repoPath?: string, - options: { branch?: boolean } = { branch: true }, - ) { - const params = ['check-ref-format']; - if (options.branch) { - params.push('--branch'); - } else { - params.push('--normalize'); - } - - try { - const data = await git( - { cwd: repoPath ?? emptyStr, errors: GitErrorHandling.Throw, local: true }, - ...params, - ref, - ); - return Boolean(data.trim()); - } catch { - return false; - } - } - - export function checkout( - repoPath: string, - ref: string, - { createBranch, fileName }: { createBranch?: string; fileName?: string } = {}, - ) { - const params = ['checkout']; - if (createBranch) { - params.push('-b', createBranch, ref, '--'); - } else { - params.push(ref, '--'); - - if (fileName) { - [fileName, repoPath] = Paths.splitPath(fileName, repoPath); - - params.push(fileName); - } - } - - return git({ cwd: repoPath }, ...params); - } - - export async function config__get(key: string, repoPath?: string, options: { local?: boolean } = {}) { - const data = await git( - { cwd: repoPath ?? emptyStr, errors: GitErrorHandling.Ignore, local: options.local }, - 'config', - '--get', - key, - ); - return data.length === 0 ? undefined : data.trim(); - } - - export async function config__get_regex(pattern: string, repoPath?: string, options: { local?: boolean } = {}) { - const data = await git( - { cwd: repoPath ?? emptyStr, errors: GitErrorHandling.Ignore, local: options.local }, - 'config', - '--get-regex', - pattern, - ); - return data.length === 0 ? undefined : data.trim(); - } - - export async function diff( - repoPath: string, - fileName: string, - ref1?: string, - ref2?: string, - options: { - encoding?: string; - filters?: GitDiffFilter[]; - linesOfContext?: number; - renames?: boolean; - similarityThreshold?: number | null; - } = {}, - ): Promise { - const params = ['diff', '--no-ext-diff', '--minimal']; - - if (options.linesOfContext != null) { - params.push(`-U${options.linesOfContext}`); - } - - if (options.renames) { - params.push(`-M${options.similarityThreshold == null ? '' : `${options.similarityThreshold}%`}`); - } - - if (options.filters != null && options.filters.length !== 0) { - params.push(`--diff-filter=${options.filters.join(emptyStr)}`); - } - - if (ref1) { - // ^3 signals an untracked file in a stash and if we are trying to find its parent, use the root sha - if (ref1.endsWith('^3^')) { - ref1 = rootSha; - } - params.push(GitRevision.isUncommittedStaged(ref1) ? '--staged' : ref1); - } - if (ref2) { - params.push(GitRevision.isUncommittedStaged(ref2) ? '--staged' : ref2); - } - - try { - return await git( - { - cwd: repoPath, - configs: ['-c', 'color.diff=false'], - encoding: options.encoding, - }, - ...params, - '--', - fileName, - ); - } catch (ex) { - const match = GitErrors.badRevision.exec(ex.message); - if (match !== null) { - const [, ref] = match; - - // If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha - if (ref === ref1 && ref != null && ref.endsWith('^')) { - return Git.diff(repoPath, fileName, rootSha, ref2, options); - } - } - - throw ex; - } - } - - export async function diff__contents( - repoPath: string, - fileName: string, - ref: string, - contents: string, - options: { encoding?: string; filters?: GitDiffFilter[]; similarityThreshold?: number | null } = {}, - ): Promise { - const params = [ - 'diff', - `-M${options.similarityThreshold == null ? '' : `${options.similarityThreshold}%`}`, - '--no-ext-diff', - '-U0', - '--minimal', - ]; - - if (options.filters != null && options.filters.length !== 0) { - params.push(`--diff-filter=${options.filters.join(emptyStr)}`); - } - - // // ^3 signals an untracked file in a stash and if we are trying to find its parent, use the root sha - // if (ref.endsWith('^3^')) { - // ref = rootSha; - // } - // params.push(GitRevision.isUncommittedStaged(ref) ? '--staged' : ref); - - params.push('--no-index'); - - try { - return await git( - { - cwd: repoPath, - configs: ['-c', 'color.diff=false'], - encoding: options.encoding, - stdin: contents, - }, - ...params, - '--', - fileName, - // Pipe the contents to stdin - '-', - ); - } catch (ex) { - if (ex instanceof RunError && ex.stdout) { - return ex.stdout; - } - - const match = GitErrors.badRevision.exec(ex.message); - if (match !== null) { - const [, matchedRef] = match; - - // If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha - if (matchedRef === ref && matchedRef != null && matchedRef.endsWith('^')) { - return Git.diff__contents(repoPath, fileName, rootSha, contents, options); - } - } - - throw ex; - } - } - - export function diff__name_status( - repoPath: string, - ref1?: string, - ref2?: string, - { filters, similarityThreshold }: { filters?: GitDiffFilter[]; similarityThreshold?: number | null } = {}, - ) { - const params = [ - 'diff', - '--name-status', - `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, - '--no-ext-diff', - ]; - if (filters != null && filters.length !== 0) { - params.push(`--diff-filter=${filters.join(emptyStr)}`); - } - if (ref1) { - params.push(ref1); - } - if (ref2) { - params.push(ref2); - } - - return git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); - } - - export async function diff__shortstat(repoPath: string, ref?: string) { - const params = ['diff', '--shortstat', '--no-ext-diff']; - if (ref) { - params.push(ref); - } - - try { - return await git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); - } catch (ex) { - const msg: string = ex?.toString() ?? ''; - if (GitErrors.noMergeBase.test(msg)) { - return undefined; - } - - throw ex; - } - } - - export function difftool( - repoPath: string, - fileName: string, - tool: string, - options: { ref1?: string; ref2?: string; staged?: boolean } = {}, - ) { - const params = ['difftool', '--no-prompt', `--tool=${tool}`]; - if (options.staged) { - params.push('--staged'); - } - if (options.ref1) { - params.push(options.ref1); - } - if (options.ref2) { - params.push(options.ref2); - } - - return git({ cwd: repoPath }, ...params, '--', fileName); - } - - export function difftool__dir_diff(repoPath: string, tool: string, ref1: string, ref2?: string) { - const params = ['difftool', '--dir-diff', `--tool=${tool}`, ref1]; - if (ref2) { - params.push(ref2); - } - - return git({ cwd: repoPath }, ...params); - } - - export async function fetch( - repoPath: string, - options: - | { all?: boolean; branch?: undefined; prune?: boolean; remote?: string } - | { - all?: undefined; - branch: string; - prune?: undefined; - pull?: boolean; - remote: string; - upstream: string; - } = {}, - ): Promise { - const params = ['fetch']; - - if (options.prune) { - params.push('--prune'); - } - - if (options.branch && options.remote) { - if (options.upstream && options.pull) { - params.push('-u', options.remote, `${options.upstream}:${options.branch}`); - - try { - void (await git({ cwd: repoPath }, ...params)); - return; - } catch (ex) { - const msg: string = ex?.toString() ?? ''; - if (GitErrors.noFastForward.test(msg)) { - void window.showErrorMessage( - `Unable to pull the '${options.branch}' branch, as it can't be fast-forwarded.`, - ); - - return; - } - - throw ex; - } - } else { - params.push(options.remote, options.branch); - } - } else if (options.remote) { - params.push(options.remote); - } else if (options.all) { - params.push('--all'); - } - - void (await git({ cwd: repoPath }, ...params)); - } - - export function for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) { - const params = ['for-each-ref', `--format=${GitBranchParser.defaultFormat}`, 'refs/heads']; - if (options.all) { - params.push('refs/remotes'); - } - - return git({ cwd: repoPath }, ...params); - } - - export function log( - repoPath: string, - ref: string | undefined, - { - all, - authors, - format = 'default', - limit, - merges, - ordering, - reverse, - similarityThreshold, - since, - }: { - all?: boolean; - authors?: string[]; - format?: 'default' | 'refs' | 'shortlog' | 'shortlog+stats'; - limit?: number; - merges?: boolean; - ordering?: string | null; - reverse?: boolean; - similarityThreshold?: number | null; - since?: string; - }, - ) { - const params = [ - 'log', - `--format=${ - format === 'refs' - ? GitLogParser.simpleRefs - : format === 'shortlog' || format === 'shortlog+stats' - ? GitLogParser.shortlog - : GitLogParser.defaultFormat - }`, - '--full-history', - `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, - '-m', - ]; - - if (format === 'default') { - params.push('--name-status'); - } else if (format === 'shortlog+stats') { - params.push('--shortstat'); - } - - if (ordering) { - params.push(`--${ordering}-order`); - } - - if (limit && !reverse) { - params.push(`-n${limit + 1}`); - } - - if (since) { - params.push(`--since="${since}"`); - } - - if (!merges) { - params.push('--first-parent'); - } - - if (authors != null && authors.length !== 0) { - params.push('--use-mailmap', ...authors.map(a => `--author=${a}`)); - } else if (format === 'shortlog') { - params.push('--use-mailmap'); - } - - if (all) { - params.push('--all'); - } - - if (ref && !GitRevision.isUncommittedStaged(ref)) { - // If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking - if (reverse) { - params.push('--reverse', '--ancestry-path', `${ref}..HEAD`); - } else { - params.push(ref); - } - } - - return git( - { cwd: repoPath, configs: ['-c', 'diff.renameLimit=0', '-c', 'log.showSignature=false'] }, - ...params, - '--', - ); - } - - export function log__file( - repoPath: string, - fileName: string, - ref: string | undefined, - { - all, - filters, - firstParent = false, - format = 'default', - limit, - ordering, - renames = true, - reverse = false, - since, - skip, - startLine, - endLine, - }: { - all?: boolean; - filters?: GitDiffFilter[]; - firstParent?: boolean; - format?: 'default' | 'refs' | 'simple'; - limit?: number; - ordering?: string | null; - renames?: boolean; - reverse?: boolean; - since?: string; - skip?: number; - startLine?: number; - endLine?: number; - } = {}, - ) { - const [file, root] = Paths.splitPath(fileName, repoPath); - - const params = [ - 'log', - `--format=${format === 'default' ? GitLogParser.defaultFormat : GitLogParser.simpleFormat}`, - ]; - - if (ordering) { - params.push(`--${ordering}-order`); - } - - if (limit && !reverse) { - params.push(`-n${limit + 1}`); - } - - if (skip) { - params.push(`--skip=${skip}`); - } - - if (since) { - params.push(`--since="${since}"`); - } - - if (all) { - params.push('--all'); - } - - // Can't allow rename detection (`--follow`) if `all` or a `startLine` is specified - if (renames && (all || startLine != null)) { - renames = false; - } - - params.push(renames ? '--follow' : '-m'); - if (/*renames ||*/ firstParent) { - params.push('--first-parent'); - // In Git >= 2.29.0 `--first-parent` implies `-m`, so lets include it for consistency - if (renames) { - params.push('-m'); - } - } - - if (filters != null && filters.length !== 0) { - params.push(`--diff-filter=${filters.join(emptyStr)}`); - } - - if (format !== 'refs') { - if (startLine == null) { - // 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'); - } - } else { - // Don't include `--name-status`, `--numstat`, or `--summary` because they aren't supported with `-L` - params.push(`-L ${startLine},${endLine == null ? startLine : endLine}:${file}`); - } - } - - if (ref && !GitRevision.isUncommittedStaged(ref)) { - // If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking - if (reverse) { - params.push('--reverse', '--ancestry-path', `${ref}..HEAD`); - } else { - params.push(ref); - } - } - - // Don't specify a file spec when using a line number (so say the git docs) - if (startLine == null) { - params.push('--', file); - } - - return git({ cwd: root, configs: ['-c', 'log.showSignature=false'] }, ...params); - } - - export async function log__file_recent( - repoPath: string, - fileName: string, - { - ordering, - ref, - similarityThreshold, - }: { ordering?: string | null; ref?: string; similarityThreshold?: number | null } = {}, - ) { - const params = [ - 'log', - `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, - '-n1', - '--format=%H', - ]; - - if (ordering) { - params.push(`--${ordering}-order`); - } - - if (ref) { - params.push(ref); - } - - const data = await git( - { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, - ...params, - '--', - fileName, - ); - return data.length === 0 ? undefined : data.trim(); - } - - export async function log__find_object( - repoPath: string, - objectId: string, - ref: string, - ordering: string | null, - file?: string, - ) { - const params = ['log', '-n1', '--no-renames', '--format=%H', `--find-object=${objectId}`, ref]; - - if (ordering) { - params.push(`--${ordering}-order`); - } - - if (file) { - params.push('--', file); - } - - const data = await git( - { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, - ...params, - ); - return data.length === 0 ? undefined : data.trim(); - } - - export async function log__recent(repoPath: string, ordering?: string | null) { - const params = ['log', '-n1', '--format=%H']; - - if (ordering) { - params.push(`--${ordering}-order`); - } - - const data = await git( - { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, - ...params, - '--', - ); - - return data.length === 0 ? undefined : data.trim(); - } - - export async function log__recent_committerdate(repoPath: string, ordering?: string | null) { - const params = ['log', '-n1', '--format=%ct']; - - if (ordering) { - params.push(`--${ordering}-order`); - } - - const data = await git( - { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, - ...params, - '--', - ); - - return data.length === 0 ? undefined : data.trim(); - } - - export function log__search( - repoPath: string, - search: string[] = emptyArray, - { - limit, - ordering, - skip, - useShow, - }: { limit?: number; ordering?: string | null; skip?: number; useShow?: boolean } = {}, - ) { - const params = [ - useShow ? 'show' : 'log', - '--name-status', - `--format=${GitLogParser.defaultFormat}`, - '--use-mailmap', - ]; - - if (limit && !useShow) { - params.push(`-n${limit + 1}`); - } - - if (skip && !useShow) { - params.push(`--skip=${skip}`); - } - - if (ordering && !useShow) { - params.push(`--${ordering}-order`); - } - - return git( - { cwd: repoPath, configs: useShow ? undefined : ['-c', 'log.showSignature=false'] }, - ...params, - ...search, - ); - } - - // export function log__shortstat(repoPath: string, options: { ref?: string }) { - // const params = ['log', '--shortstat', '--oneline']; - // if (options.ref && !GitRevision.isUncommittedStaged(options.ref)) { - // params.push(options.ref); - // } - // return git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); - // } - - export async function ls_files( - repoPath: string, - fileName: string, - { ref, untracked }: { ref?: string; untracked?: boolean } = {}, - ): Promise { - const params = ['ls-files']; - if (ref && !GitRevision.isUncommitted(ref)) { - params.push(`--with-tree=${ref}`); - } - - if (!ref && untracked) { - params.push('-o'); - } - - const data = await git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params, '--', fileName); - return data.length === 0 ? undefined : data.trim(); - } - - export function ls_remote(repoPath: string, remote: string, ref?: string) { - return git({ cwd: repoPath }, 'ls-remote', remote, ref); - } - - export function ls_remote__HEAD(repoPath: string, remote: string) { - return git({ cwd: repoPath }, 'ls-remote', '--symref', remote, 'HEAD'); - } - - export async function ls_tree(repoPath: string, ref: string, { fileName }: { fileName?: string } = {}) { - const params = ['ls-tree']; - if (fileName) { - params.push('-l', ref, '--', fileName); - } else { - params.push('-lrt', ref, '--'); - } - const data = await git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params); - return data.length === 0 ? undefined : data.trim(); - } - - export function merge_base( - repoPath: string, - ref1: string, - ref2: string, - { forkPoint }: { forkPoint?: boolean } = {}, - ) { - const params = ['merge-base']; - if (forkPoint) { - params.push('--fork-point'); - } - - return git({ cwd: repoPath }, ...params, ref1, ref2); - } - - export function reflog( - repoPath: string, - { - all, - branch, - limit, - ordering, - skip, - }: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number } = {}, - ): Promise { - const params = ['log', '--walk-reflogs', `--format=${GitReflogParser.defaultFormat}`, '--date=iso8601']; - - if (ordering) { - params.push(`--${ordering}-order`); - } - - if (all) { - params.push('--all'); - } - - if (limit) { - params.push(`-n${limit}`); - } - - if (skip) { - params.push(`--skip=${skip}`); - } - - if (branch) { - params.push(branch); - } - - return git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); - } - - export function remote(repoPath: string): Promise { - return git({ cwd: repoPath }, 'remote', '-v'); - } - - export function remote__add(repoPath: string, name: string, url: string) { - return git({ cwd: repoPath }, 'remote', 'add', name, url); - } - - export function remote__prune(repoPath: string, remoteName: string) { - return git({ cwd: repoPath }, 'remote', 'prune', remoteName); - } - - export function remote__get_url(repoPath: string, remote: string): Promise { - return git({ cwd: repoPath }, 'remote', 'get-url', remote); - } - - export function reset(repoPath: string | undefined, fileName: string) { - return git({ cwd: repoPath }, 'reset', '-q', '--', fileName); - } - - export async function rev_list__count(repoPath: string, ref: string): Promise { - let data = await git( - { cwd: repoPath, errors: GitErrorHandling.Ignore }, - 'rev-list', - '--count', - ref, - '--', - ); - data = data.trim(); - if (data.length === 0) return undefined; - - const result = parseInt(data, 10); - return isNaN(result) ? undefined : result; - } - - export async function rev_list__left_right( - repoPath: string, - refs: string[], - ): Promise<{ ahead: number; behind: number } | undefined> { - const data = await git( - { cwd: repoPath, errors: GitErrorHandling.Ignore }, - 'rev-list', - '--left-right', - '--count', - ...refs, - '--', - ); - if (data.length === 0) return undefined; - - const parts = data.split('\t'); - if (parts.length !== 2) return undefined; - - const [ahead, behind] = parts; - const result = { - ahead: parseInt(ahead, 10), - behind: parseInt(behind, 10), - }; - - if (isNaN(result.ahead) || isNaN(result.behind)) return undefined; - - return result; - } - - export async function rev_parse__currentBranch( - repoPath: string, - ordering: string | null, - ): Promise<[string, string | undefined] | undefined> { - try { - const data = await git( - { cwd: repoPath, errors: GitErrorHandling.Throw }, - 'rev-parse', - '--abbrev-ref', - '--symbolic-full-name', - '@', - '@{u}', - '--', - ); - return [data, undefined]; - } catch (ex) { - const msg: string = ex?.toString() ?? ''; - if (GitErrors.badRevision.test(msg) || GitWarnings.noUpstream.test(msg)) { - if (ex.stdout != null && ex.stdout.length !== 0) { - return [ex.stdout, undefined]; - } - - try { - const data = await symbolic_ref(repoPath, 'HEAD'); - if (data != null) return [data.trim(), undefined]; - } catch {} - - try { - const data = await symbolic_ref(repoPath, 'refs/remotes/origin/HEAD'); - if (data != null) return [data.trim().substr('origin/'.length), undefined]; - } catch (ex) { - if (/is not a symbolic ref/.test(ex.stderr)) { - try { - const data = await ls_remote__HEAD(repoPath, 'origin'); - if (data != null) { - const match = /ref:\s(\S+)\s+HEAD/m.exec(data); - if (match != null) { - const [, branch] = match; - return [branch.substr('refs/heads/'.length), undefined]; - } - } - } catch {} - } - } - - const defaultBranch = (await config__get('init.defaultBranch', repoPath, { local: true })) ?? 'main'; - const branchConfig = await config__get_regex(`branch\\.${defaultBranch}\\.+`, repoPath, { - local: true, - }); - - let remote; - let remoteBranch; - - if (branchConfig) { - let match = /^branch\..+\.remote\s(.+)$/m.exec(branchConfig); - if (match != null) { - remote = match[1]; - } - - match = /^branch\..+\.merge\srefs\/heads\/(.+)$/m.exec(branchConfig); - if (match != null) { - remoteBranch = match[1]; - } - } - return [`${defaultBranch}${remote && remoteBranch ? `\n${remote}/${remoteBranch}` : ''}`, undefined]; - } - - if (GitWarnings.headNotABranch.test(msg)) { - const sha = await log__recent(repoPath, ordering); - if (sha === undefined) return undefined; - - return [`(HEAD detached at ${GitRevision.shorten(sha)})`, sha]; - } - - defaultExceptionHandler(ex, repoPath); - return undefined; - } - } - - export async function rev_parse__show_toplevel(cwd: string): Promise { - try { - const data = await git( - { cwd: cwd, errors: GitErrorHandling.Throw }, - 'rev-parse', - '--show-toplevel', - ); - // Make sure to normalize: https://github.com/git-for-windows/git/issues/2478 - // Keep trailing spaces which are part of the directory name - return data.length === 0 ? undefined : Strings.normalizePath(data.trimLeft().replace(/[\r|\n]+$/, '')); - } catch (ex) { - const inDotGit = /this operation must be run in a work tree/.test(ex.stderr); - if (inDotGit || ex.code === 'ENOENT') { - // If the `cwd` doesn't exist, walk backward to see if any parent folder exists - let exists = inDotGit ? false : await fsExists(cwd); - if (!exists) { - do { - const parent = paths.dirname(cwd); - if (parent === cwd || parent.length === 0) return undefined; - - cwd = parent; - exists = await fsExists(cwd); - } while (!exists); - - return rev_parse__show_toplevel(cwd); - } - } - return undefined; - } - } - - export async function rev_parse__verify( - repoPath: string, - ref: string, - fileName?: string, - ): Promise { - const data = await git( - { cwd: repoPath, errors: GitErrorHandling.Ignore }, - 'rev-parse', - '--verify', - fileName ? `${ref}:./${fileName}` : `${ref}^{commit}`, - ); - return data.length === 0 ? undefined : data.trim(); - } - - export function shortlog(repoPath: string) { - return git({ cwd: repoPath }, 'shortlog', '-sne', '--all', '--no-merges', 'HEAD'); - } - - export async function show( - repoPath: string | undefined, - fileName: string, - ref: string, - options: { - encoding?: 'binary' | 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'hex' | 'buffer'; - } = {}, - ): Promise { - const [file, root] = Paths.splitPath(fileName, repoPath); - - if (GitRevision.isUncommittedStaged(ref)) { - ref = ':'; - } - if (GitRevision.isUncommitted(ref)) throw new Error(`ref=${ref} is uncommitted`); - - const opts: GitCommandOptions = { - configs: ['-c', 'log.showSignature=false'], - cwd: root, - encoding: options.encoding ?? 'utf8', - errors: GitErrorHandling.Throw, - }; - const args = ref.endsWith(':') ? `${ref}./${file}` : `${ref}:./${file}`; - - try { - const data = await git(opts, 'show', '--textconv', args, '--'); - return data; - } catch (ex) { - const msg: string = ex?.toString() ?? ''; - if (ref === ':' && GitErrors.badRevision.test(msg)) { - return Git.show(repoPath, fileName, 'HEAD:', options); - } - - if ( - GitErrors.badRevision.test(msg) || - GitWarnings.notFound.test(msg) || - GitWarnings.foundButNotInRevision.test(msg) - ) { - return undefined; - } - - return defaultExceptionHandler(ex, opts.cwd) as TOut; - } - } - - export function show__diff( - repoPath: string, - fileName: string, - ref: string, - originalFileName?: string, - { similarityThreshold }: { similarityThreshold?: number | null } = {}, - ) { - const params = [ - 'show', - `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, - '--format=', - '--minimal', - '-U0', - ref, - '--', - fileName, - ]; - if (originalFileName != null && originalFileName.length !== 0) { - params.push(originalFileName); - } - - return git({ cwd: repoPath }, ...params); - } - - export function show__name_status(repoPath: string, fileName: string, ref: string) { - return git({ cwd: repoPath }, 'show', '--name-status', '--format=', ref, '--', fileName); - } - - export function show_ref__tags(repoPath: string) { - return git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'show-ref', '--tags'); - } - - export function stash__apply( - repoPath: string, - stashName: string, - deleteAfter: boolean, - ): Promise { - if (!stashName) return Promise.resolve(undefined); - return git({ cwd: repoPath }, 'stash', deleteAfter ? 'pop' : 'apply', stashName); - } - - export async function stash__delete(repoPath: string, stashName: string, ref?: string) { - if (!stashName) return undefined; - - if (ref) { - const stashRef = await git( - { cwd: repoPath, errors: GitErrorHandling.Ignore }, - 'show', - '--format=%H', - '--no-patch', - stashName, - ); - if (stashRef?.trim() !== ref) { - throw new Error('Unable to delete stash; mismatch with stash number'); - } - } - - return git({ cwd: repoPath }, 'stash', 'drop', stashName); - } - - export function stash__list( - repoPath: string, - { - format = GitStashParser.defaultFormat, - similarityThreshold, - }: { format?: string; similarityThreshold?: number | null } = {}, - ) { - return git( - { cwd: repoPath }, - 'stash', - 'list', - '--name-status', - `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, - `--format=${format}`, - ); - } - - export async function stash__push( - repoPath: string, - message?: string, - { - includeUntracked, - keepIndex, - pathspecs, - stdin, - }: { includeUntracked?: boolean; keepIndex?: boolean; pathspecs?: string[]; stdin?: boolean } = {}, - ): Promise { - const params = ['stash', 'push']; - - if (includeUntracked || (pathspecs != null && pathspecs.length !== 0)) { - params.push('-u'); - } - - if (keepIndex) { - params.push('-k'); - } - - if (message) { - params.push('-m', message); - } - - if (stdin && pathspecs != null && pathspecs.length !== 0) { - void (await git( - { cwd: repoPath, stdin: pathspecs.join('\0') }, - ...params, - '--pathspec-from-file=-', - '--pathspec-file-nul', - )); - - return; - } - - params.push('--'); - if (pathspecs != null && pathspecs.length !== 0) { - params.push(...pathspecs); - } - - void (await git({ cwd: repoPath }, ...params)); - } - - export async function status( - repoPath: string, - porcelainVersion: number = 1, - { similarityThreshold }: { similarityThreshold?: number | null } = {}, - ): Promise { - const params = [ - 'status', - porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain', - '--branch', - '-u', - ]; - if (await Git.isAtLeastVersion('2.18')) { - params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); - } - - return git( - { cwd: repoPath, configs: ['-c', 'color.status=false'], env: { GIT_OPTIONAL_LOCKS: '0' } }, - ...params, - '--', - ); - } - - export async function status__file( - repoPath: string, - fileName: string, - porcelainVersion: number = 1, - { similarityThreshold }: { similarityThreshold?: number | null } = {}, - ): Promise { - const [file, root] = Paths.splitPath(fileName, repoPath); - - const params = ['status', porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain']; - if (await Git.isAtLeastVersion('2.18')) { - params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); - } - - return git( - { cwd: root, configs: ['-c', 'color.status=false'], env: { GIT_OPTIONAL_LOCKS: '0' } }, - ...params, - '--', - file, - ); - } - - export function symbolic_ref(repoPath: string, ref: string) { - return git({ cwd: repoPath }, 'symbolic-ref', '--short', ref); - } - - export function tag(repoPath: string) { - return git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); - } - - export async function readDotGitFile( - repoPath: string, - paths: string[], - options?: { numeric?: false; throw?: boolean; trim?: boolean }, - ): Promise; - export async function readDotGitFile( - repoPath: string, - path: string[], - options?: { numeric: true; throw?: boolean; trim?: boolean }, - ): Promise; - export async function readDotGitFile( - repoPath: string, - pathParts: string[], - options?: { numeric?: boolean; throw?: boolean; trim?: boolean }, - ): Promise { - try { - const bytes = await workspace.fs.readFile(Uri.file(paths.join(...[repoPath, '.git', ...pathParts]))); - let contents = textDecoder.decode(bytes); - contents = options?.trim ?? true ? contents.trim() : contents; - - if (options?.numeric) { - const number = Number.parseInt(contents, 10); - return isNaN(number) ? undefined : number; - } - - return contents; - } catch (ex) { - if (options?.throw) throw ex; - - return undefined; - } - } -} - -export function isFolderGlob(path: string) { - return paths.basename(path) === '*'; -} - -export function toFolderGlob(fsPath: string) { - return paths.join(fsPath, '*'); -} diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 6595df5..6da8ede 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -1,5 +1,6 @@ import { Disposable, Event, Range, TextEditor, Uri, WorkspaceFolder } from 'vscode'; import { Commit, InputBox } from '../@types/vscode.git'; +import { GitUri } from './gitUri'; import { BranchSortOptions, GitBlame, @@ -25,14 +26,12 @@ import { GitTag, GitTree, GitUser, - RemoteProvider, Repository, RepositoryChangeEvent, - RichRemoteProvider, TagSortOptions, -} from './git'; -import { GitUri } from './gitUri'; +} from './models'; import { RemoteProviders } from './remotes/factory'; +import { RemoteProvider, RichRemoteProvider } from './remotes/provider'; import { SearchPattern } from './search'; export const enum GitProviderId { diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index b18597a..45fb329 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -31,8 +31,9 @@ import { Arrays, debug, gate, Iterables, log, Paths, Promises, Strings } from '. import { PromiseOrValue } from '../system/promise'; import { vslsUriPrefixRegex } from '../vsls/vsls'; import { ProviderNotFoundError } from './errors'; +import { GitProvider, GitProviderDescriptor, GitProviderId, PagedResult, ScmRepository } from './gitProvider'; +import { GitUri } from './gitUri'; import { - Authentication, BranchDateFormatting, BranchSortOptions, CommitDateFormatting, @@ -68,12 +69,11 @@ import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, - SearchPattern, TagSortOptions, -} from './git'; -import { GitProvider, GitProviderDescriptor, GitProviderId, PagedResult, ScmRepository } from './gitProvider'; -import { GitUri } from './gitUri'; -import { RemoteProvider, RemoteProviders, RichRemoteProvider } from './remotes/factory'; +} from './models'; +import { RemoteProviders } from './remotes/factory'; +import { Authentication, RemoteProvider, RichRemoteProvider } from './remotes/provider'; +import { SearchPattern } from './search'; export { type GitProviderDescriptor, GitProviderId }; diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 6109046..f4e1708 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -4,9 +4,9 @@ import { Uri } from 'vscode'; import { UriComparer } from '../comparers'; import { DocumentSchemes } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitFile, GitRevision } from '../git/git'; import { Logger } from '../logger'; import { debug, memoize, Strings } from '../system'; +import { GitCommit, GitFile, GitRevision } from './models'; const emptyStr = ''; const slash = '/'; diff --git a/src/git/locator.ts b/src/git/locator.ts deleted file mode 100644 index 4b17759..0000000 --- a/src/git/locator.ts +++ /dev/null @@ -1,141 +0,0 @@ -'use strict'; -import * as paths from 'path'; -import { GlyphChars } from '../constants'; -import { LogLevel } from '../logger'; -import { Stopwatch } from '../system'; -import { findExecutable, run } from './shell'; - -export class UnableToFindGitError extends Error { - constructor(public readonly original?: Error) { - super('Unable to find git'); - - Error.captureStackTrace?.(this, UnableToFindGitError); - } -} - -export class InvalidGitConfigError extends Error { - constructor(public readonly original: Error) { - super('Invalid Git configuration'); - - Error.captureStackTrace?.(this, InvalidGitConfigError); - } -} - -export interface GitLocation { - path: string; - version: string; -} - -function parseVersion(raw: string): string { - return raw?.replace(/^git version /, ''); -} - -async function findSpecificGit(path: string): Promise { - const sw = new Stopwatch(`findSpecificGit(${path})`, { logLevel: LogLevel.Debug }); - - let version; - try { - version = await run(path, ['--version'], 'utf8'); - } catch (ex) { - sw.stop({ message: ` ${GlyphChars.Dot} Unable to find git` }); - - if (/bad config/i.test(ex.message)) throw new InvalidGitConfigError(ex); - throw ex; - } - - // If needed, let's update our path to avoid the search on every command - if (!path || path === 'git') { - const foundPath = findExecutable(path, ['--version']).cmd; - - // Ensure that the path we found works - try { - version = await run(foundPath, ['--version'], 'utf8'); - } catch (ex) { - sw.stop({ message: ` ${GlyphChars.Dot} Unable to find git` }); - - if (/bad config/i.test(ex.message)) throw new InvalidGitConfigError(ex); - throw ex; - } - - path = foundPath; - } - - sw.stop({ message: ` ${GlyphChars.Dot} Found git @ ${path}` }); - - return { - path: path, - version: parseVersion(version.trim()), - }; -} - -async function findGitDarwin(): Promise { - try { - let path = await run('which', ['git'], 'utf8'); - path = path.replace(/^\s+|\s+$/g, ''); - - if (path !== '/usr/bin/git') { - return findSpecificGit(path); - } - - try { - await run('xcode-select', ['-p'], 'utf8'); - return findSpecificGit(path); - } catch (ex) { - if (ex.code === 2) { - return Promise.reject(new UnableToFindGitError(ex)); - } - return findSpecificGit(path); - } - } catch (ex) { - return Promise.reject( - ex instanceof InvalidGitConfigError || ex instanceof UnableToFindGitError - ? ex - : new UnableToFindGitError(ex), - ); - } -} - -function findSystemGitWin32(basePath: string | null | undefined): Promise { - if (basePath == null || basePath.length === 0) return Promise.reject(new UnableToFindGitError()); - return findSpecificGit(paths.join(basePath, 'Git', 'cmd', 'git.exe')); -} - -function findGitWin32(): Promise { - return findSystemGitWin32(process.env['ProgramW6432']) - .then(null, () => findSystemGitWin32(process.env['ProgramFiles(x86)'])) - .then(null, () => findSystemGitWin32(process.env['ProgramFiles'])) - .then(null, () => findSpecificGit('git')); -} - -export async function findGitPath(paths?: string | string[]): Promise { - try { - if (paths == null || typeof paths === 'string') { - return await findSpecificGit(paths ?? 'git'); - } - - for (const path of paths) { - try { - return await findSpecificGit(path); - } catch {} - } - - throw new UnableToFindGitError(); - } catch { - try { - switch (process.platform) { - case 'darwin': - return await findGitDarwin(); - case 'win32': - return await findGitWin32(); - default: - return Promise.reject(new UnableToFindGitError()); - } - } catch (ex) { - return Promise.reject( - ex instanceof InvalidGitConfigError || ex instanceof UnableToFindGitError - ? ex - : new UnableToFindGitError(ex), - ); - } - } -} diff --git a/src/git/models.ts b/src/git/models.ts new file mode 100644 index 0000000..b2f717a --- /dev/null +++ b/src/git/models.ts @@ -0,0 +1,28 @@ +'use strict'; +export * from './models/author'; +export * from './models/blame'; +export * from './models/blameCommit'; +export * from './models/branch'; +export * from './models/commit'; +export * from './models/contributor'; +export * from './models/defaultBranch'; +export * from './models/diff'; +export * from './models/file'; +export * from './models/issue'; +export * from './models/log'; +export * from './models/logCommit'; +export * from './models/merge'; +export * from './models/pullRequest'; +export * from './models/reference'; +export * from './models/rebase'; +export * from './models/reflog'; +export * from './models/remote'; +export * from './models/remoteProvider'; +export * from './models/repository'; +export * from './models/shortlog'; +export * from './models/stash'; +export * from './models/stashCommit'; +export * from './models/status'; +export * from './models/tag'; +export * from './models/tree'; +export * from './models/user'; diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 887b16a..a823d03 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -3,8 +3,9 @@ import { BranchSorting, configuration, DateStyle } from '../../configuration'; import { Starred, WorkspaceState } from '../../constants'; import { Container } from '../../container'; import { Dates, debug, memoize, Strings } from '../../system'; -import { GitRemote, GitRevision } from '../git'; -import { GitBranchReference, GitReference, PullRequest, PullRequestState } from './models'; +import { GitBranchReference, GitReference, GitRevision } from '../models'; +import { PullRequest, PullRequestState } from './pullRequest'; +import { GitRemote } from './remote'; import { GitStatus } from './status'; const whitespaceRegex = /\s/; diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index 2600463..df7b2b3 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -4,9 +4,9 @@ import { getAvatarUri } from '../../avatars'; import { configuration, DateSource, DateStyle, GravatarDefaultStyle } from '../../configuration'; import { Container } from '../../container'; import { Dates, memoize } from '../../system'; -import { CommitFormatter } from '../formatters/formatters'; +import { CommitFormatter } from '../formatters'; import { GitUri } from '../gitUri'; -import { GitReference, GitRevision, GitRevisionReference, PullRequest } from './models'; +import { GitReference, GitRevision, GitRevisionReference, PullRequest } from '../models'; export interface GitAuthor { name: string; diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts index e8cc438..80da856 100644 --- a/src/git/models/diff.ts +++ b/src/git/models/diff.ts @@ -53,3 +53,5 @@ export interface GitDiffShortStat { readonly insertions: number; readonly deletions: number; } + +export type GitDiffFilter = 'A' | 'C' | 'D' | 'M' | 'R' | 'T' | 'U' | 'X' | 'B' | '*'; diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index cb999b5..edf3864 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -2,9 +2,9 @@ import { Uri } from 'vscode'; import { memoize, Strings } from '../../system'; import { GitUri } from '../gitUri'; +import { GitReference } from '../models'; import { GitCommit, GitCommitType } from './commit'; import { GitFile, GitFileStatus } from './file'; -import { GitReference } from './models'; const emptyStats = Object.freeze({ added: 0, diff --git a/src/git/models/merge.ts b/src/git/models/merge.ts index 838e8ff..847686a 100644 --- a/src/git/models/merge.ts +++ b/src/git/models/merge.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitBranchReference, GitRevisionReference } from './models'; +import { GitBranchReference, GitRevisionReference } from '../models'; export interface GitMergeStatus { type: 'merge'; diff --git a/src/git/models/models.ts b/src/git/models/models.ts deleted file mode 100644 index 30008ca..0000000 --- a/src/git/models/models.ts +++ /dev/null @@ -1,368 +0,0 @@ -'use strict'; -import { GlyphChars } from '../../constants'; -import { Container } from '../../container'; -import { GitBranch } from './branch'; - -const emptyStr = ''; - -const rangeRegex = /^(\S*?)(\.\.\.?)(\S*)\s*$/; -const shaLikeRegex = /(^[0-9a-f]{40}([\^@~:]\S*)?$)|(^[0]{40}(:|-)$)/; -const shaRegex = /(^[0-9a-f]{40}$)|(^[0]{40}(:|-)$)/; -const shaParentRegex = /(^[0-9a-f]{40})\^[0-3]?$/; -const shaShortenRegex = /^(.*?)([\^@~:].*)?$/; -const uncommittedRegex = /^[0]{40}(?:[\^@~:]\S*)?:?$/; -const uncommittedStagedRegex = /^[0]{40}([\^@~]\S*)?:$/; - -function isMatch(regex: RegExp, ref: string | undefined) { - return ref == null || ref.length === 0 ? false : regex.test(ref); -} - -export namespace GitRevision { - export const deletedOrMissing = '0000000000000000000000000000000000000000-'; - export const uncommitted = '0000000000000000000000000000000000000000'; - export const uncommittedStaged = '0000000000000000000000000000000000000000:'; - - export function createRange( - ref1: string | undefined, - ref2: string | undefined, - notation: '..' | '...' = '..', - ): string { - return `${ref1 ?? ''}${notation}${ref2 ?? ''}`; - } - - export function isRange(ref: string | undefined) { - return ref?.includes('..') ?? false; - } - - export function isSha(ref: string) { - return isMatch(shaRegex, ref); - } - - export function isShaLike(ref: string) { - return isMatch(shaLikeRegex, ref); - } - - export function isShaParent(ref: string) { - return isMatch(shaParentRegex, ref); - } - - export function isUncommitted(ref: string | undefined) { - return isMatch(uncommittedRegex, ref); - } - - export function isUncommittedStaged(ref: string | undefined): boolean { - return isMatch(uncommittedStagedRegex, ref); - } - - export function shorten( - ref: string | undefined, - { - force, - strings = {}, - }: { - force?: boolean; - strings?: { uncommitted?: string; uncommittedStaged?: string; working?: string }; - } = {}, - ) { - if (ref === deletedOrMissing) return '(deleted)'; - - if (ref == null || ref.length === 0) return strings.working ?? emptyStr; - if (isUncommitted(ref)) { - return isUncommittedStaged(ref) - ? strings.uncommittedStaged ?? 'Index' - : strings.uncommitted ?? 'Working Tree'; - } - - if (GitRevision.isRange(ref)) return ref; - if (!force && !isShaLike(ref)) return ref; - - // Don't allow shas to be shortened to less than 5 characters - const len = Math.max(5, Container.instance.config.advanced.abbreviatedShaLength); - - // If we have a suffix, append it - const match = shaShortenRegex.exec(ref); - if (match != null) { - const [, rev, suffix] = match; - - if (suffix != null) { - return `${rev.substr(0, len)}${suffix}`; - } - } - - return ref.substr(0, len); - } - - export function splitRange(ref: string): { ref1: string; ref2: string; notation: '..' | '...' } | undefined { - const match = rangeRegex.exec(ref); - if (match == null) return undefined; - - const [, ref1, notation, ref2] = match; - return { - ref1: ref1, - notation: notation as '..' | '...', - ref2: ref2, - }; - } -} - -export interface GitBranchReference { - readonly refType: 'branch'; - name: string; - ref: string; - readonly remote: boolean; - readonly upstream?: { name: string; missing: boolean }; - repoPath: string; -} - -export interface GitRevisionReference { - readonly refType: 'revision' | 'stash'; - name: string; - ref: string; - repoPath: string; - - number?: string | undefined; - message?: string; -} - -export interface GitStashReference { - readonly refType: 'stash'; - name: string; - ref: string; - repoPath: string; - number: string | undefined; - - message?: string; -} - -export interface GitTagReference { - readonly refType: 'tag'; - name: string; - ref: string; - repoPath: string; -} - -export type GitReference = GitBranchReference | GitRevisionReference | GitStashReference | GitTagReference; - -export namespace GitReference { - export function create( - ref: string, - repoPath: string, - options: { refType: 'branch'; name: string; remote: boolean; upstream?: { name: string; missing: boolean } }, - ): GitBranchReference; - export function create( - ref: string, - repoPath: string, - options?: { refType: 'revision'; name?: string; message?: string }, - ): GitRevisionReference; - export function create( - ref: string, - repoPath: string, - options: { refType: 'stash'; name: string; number: string | undefined; message?: string }, - ): GitStashReference; - export function create(ref: string, repoPath: string, options: { refType: 'tag'; name: string }): GitTagReference; - export function create( - ref: string, - repoPath: string, - options: - | { refType: 'branch'; name: string; remote: boolean } - | { refType?: 'revision'; name?: string; message?: string } - | { refType: 'stash'; name: string; number: string | undefined; message?: string } - | { refType: 'tag'; name: string } = { refType: 'revision' }, - ): GitReference { - switch (options.refType) { - case 'branch': - return { - name: options.name, - ref: ref, - refType: 'branch', - remote: options.remote, - repoPath: repoPath, - }; - case 'stash': - return { - name: options.name, - ref: ref, - refType: 'stash', - repoPath: repoPath, - number: options.number, - message: options.message, - }; - case 'tag': - return { - name: options.name, - ref: ref, - refType: 'tag', - repoPath: repoPath, - }; - default: - return { - name: - options.name ?? GitRevision.shorten(ref, { force: true, strings: { working: 'Working Tree' } }), - ref: ref, - refType: 'revision', - repoPath: repoPath, - message: options.message, - }; - } - } - - export function fromBranch(branch: GitBranchReference) { - return create(branch.ref, branch.repoPath, { - refType: branch.refType, - name: branch.name, - remote: branch.remote, - upstream: branch.upstream, - }); - } - - export function fromTag(tag: GitTagReference) { - return create(tag.ref, tag.repoPath, { - refType: tag.refType, - name: tag.name, - }); - } - - export function getNameWithoutRemote(ref: GitReference) { - if (ref.refType === 'branch') { - return ref.remote ? GitBranch.getNameWithoutRemote(ref.name) : ref.name; - } - return ref.name; - } - - export function isBranch(ref: GitReference | undefined): ref is GitBranchReference { - return ref?.refType === 'branch'; - } - - export function isRevision(ref: GitReference | undefined): ref is GitRevisionReference { - return ref?.refType === 'revision'; - } - - export function isRevisionRange(ref: GitReference | undefined): ref is GitRevisionReference { - return ref?.refType === 'revision' && GitRevision.isRange(ref.ref); - } - - export function isStash(ref: GitReference | undefined): ref is GitStashReference { - return ref?.refType === 'stash' || (ref?.refType === 'revision' && (ref as any)?.stashName); - } - - export function isTag(ref: GitReference | undefined): ref is GitTagReference { - return ref?.refType === 'tag'; - } - - export function toString( - refs: GitReference | GitReference[] | undefined, - options?: { capitalize?: boolean; expand?: boolean; icon?: boolean; label?: boolean; quoted?: boolean } | false, - ) { - if (refs == null) return ''; - - options = - options === false - ? {} - : { expand: true, icon: true, label: options?.label ?? options?.expand ?? true, ...options }; - - let result; - if (!Array.isArray(refs) || refs.length === 1) { - const ref = Array.isArray(refs) ? refs[0] : refs; - let refName = options?.quoted ? `'${ref.name}'` : ref.name; - switch (ref.refType) { - case 'branch': - result = `${options.label ? `${ref.remote ? 'remote ' : ''}branch ` : ''}${ - options.icon ? `$(git-branch)${GlyphChars.Space}${refName}${GlyphChars.Space}` : refName - }`; - break; - case 'tag': - result = `${options.label ? 'tag ' : ''}${ - options.icon ? `$(tag)${GlyphChars.Space}${refName}${GlyphChars.Space}` : refName - }`; - break; - default: { - if (GitReference.isStash(ref)) { - let message; - if (options.expand && ref.message) { - message = `${ref.number != null ? `${ref.number}: ` : ''}${ - ref.message.length > 20 - ? `${ref.message.substring(0, 20).trimRight()}${GlyphChars.Ellipsis}` - : ref.message - }`; - } - - result = `${options.label ? 'stash ' : ''}${ - options.icon - ? `$(archive)${GlyphChars.Space}${message ?? ref.name}${GlyphChars.Space}` - : `${message ?? ref.number ?? ref.name}` - }`; - } else if (GitRevision.isRange(ref.ref)) { - result = refName; - } else { - let message; - if (options.expand && ref.message) { - message = - ref.message.length > 20 - ? ` (${ref.message.substring(0, 20).trimRight()}${GlyphChars.Ellipsis})` - : ` (${ref.message})`; - } - - let prefix; - if (options.expand && options.label && GitRevision.isShaParent(ref.ref)) { - refName = ref.name.endsWith('^') ? ref.name.substr(0, ref.name.length - 1) : ref.name; - if (options?.quoted) { - refName = `'${refName}'`; - } - prefix = 'before '; - } else { - prefix = ''; - } - - result = `${options.label ? `${prefix}commit ` : ''}${ - options.icon - ? `$(git-commit)${GlyphChars.Space}${refName}${message ?? ''}${GlyphChars.Space}` - : `${refName}${message ?? ''}` - }`; - } - break; - } - } - - return options.capitalize && options.expand && options.label !== false - ? `${result[0].toLocaleUpperCase()}${result.substring(1)}` - : result; - } - - const expanded = options.expand ? ` (${refs.map(r => r.name).join(', ')})` : ''; - switch (refs[0].refType) { - case 'branch': - return `${refs.length} branches${expanded}`; - case 'tag': - return `${refs.length} tags${expanded}`; - default: - return `${refs.length} ${GitReference.isStash(refs[0]) ? 'stashes' : 'commits'}${expanded}`; - } - } -} - -export * from './author'; -export * from './blame'; -export * from './blameCommit'; -export * from './branch'; -export * from './commit'; -export * from './contributor'; -export * from './defaultBranch'; -export * from './diff'; -export * from './file'; -export * from './issue'; -export * from './log'; -export * from './logCommit'; -export * from './merge'; -export * from './pullRequest'; -export * from './rebase'; -export * from './reflog'; -export * from './remote'; -export * from './remoteProvider'; -export * from './repository'; -export * from './shortlog'; -export * from './stash'; -export * from './stashCommit'; -export * from './status'; -export * from './tag'; -export * from './tree'; -export * from './user'; diff --git a/src/git/models/rebase.ts b/src/git/models/rebase.ts index 2bd9906..60ea24d 100644 --- a/src/git/models/rebase.ts +++ b/src/git/models/rebase.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitBranchReference, GitRevisionReference } from './models'; +import { GitBranchReference, GitRevisionReference } from '../models'; export interface GitRebaseStatus { type: 'rebase'; diff --git a/src/git/models/reference.ts b/src/git/models/reference.ts new file mode 100644 index 0000000..f2a184c --- /dev/null +++ b/src/git/models/reference.ts @@ -0,0 +1,341 @@ +'use strict'; +import { GlyphChars } from '../../constants'; +import { Container } from '../../container'; +import { GitBranch } from './branch'; + +const emptyStr = ''; + +const rangeRegex = /^(\S*?)(\.\.\.?)(\S*)\s*$/; +const shaLikeRegex = /(^[0-9a-f]{40}([\^@~:]\S*)?$)|(^[0]{40}(:|-)$)/; +const shaRegex = /(^[0-9a-f]{40}$)|(^[0]{40}(:|-)$)/; +const shaParentRegex = /(^[0-9a-f]{40})\^[0-3]?$/; +const shaShortenRegex = /^(.*?)([\^@~:].*)?$/; +const uncommittedRegex = /^[0]{40}(?:[\^@~:]\S*)?:?$/; +const uncommittedStagedRegex = /^[0]{40}([\^@~]\S*)?:$/; + +function isMatch(regex: RegExp, ref: string | undefined) { + return ref == null || ref.length === 0 ? false : regex.test(ref); +} + +export namespace GitRevision { + export const deletedOrMissing = '0000000000000000000000000000000000000000-'; + export const uncommitted = '0000000000000000000000000000000000000000'; + export const uncommittedStaged = '0000000000000000000000000000000000000000:'; + + export function createRange( + ref1: string | undefined, + ref2: string | undefined, + notation: '..' | '...' = '..', + ): string { + return `${ref1 ?? ''}${notation}${ref2 ?? ''}`; + } + + export function isRange(ref: string | undefined) { + return ref?.includes('..') ?? false; + } + + export function isSha(ref: string) { + return isMatch(shaRegex, ref); + } + + export function isShaLike(ref: string) { + return isMatch(shaLikeRegex, ref); + } + + export function isShaParent(ref: string) { + return isMatch(shaParentRegex, ref); + } + + export function isUncommitted(ref: string | undefined) { + return isMatch(uncommittedRegex, ref); + } + + export function isUncommittedStaged(ref: string | undefined): boolean { + return isMatch(uncommittedStagedRegex, ref); + } + + export function shorten( + ref: string | undefined, + { + force, + strings = {}, + }: { + force?: boolean; + strings?: { uncommitted?: string; uncommittedStaged?: string; working?: string }; + } = {}, + ) { + if (ref === deletedOrMissing) return '(deleted)'; + + if (ref == null || ref.length === 0) return strings.working ?? emptyStr; + if (isUncommitted(ref)) { + return isUncommittedStaged(ref) + ? strings.uncommittedStaged ?? 'Index' + : strings.uncommitted ?? 'Working Tree'; + } + + if (GitRevision.isRange(ref)) return ref; + if (!force && !isShaLike(ref)) return ref; + + // Don't allow shas to be shortened to less than 5 characters + const len = Math.max(5, Container.instance.config.advanced.abbreviatedShaLength); + + // If we have a suffix, append it + const match = shaShortenRegex.exec(ref); + if (match != null) { + const [, rev, suffix] = match; + + if (suffix != null) { + return `${rev.substr(0, len)}${suffix}`; + } + } + + return ref.substr(0, len); + } + + export function splitRange(ref: string): { ref1: string; ref2: string; notation: '..' | '...' } | undefined { + const match = rangeRegex.exec(ref); + if (match == null) return undefined; + + const [, ref1, notation, ref2] = match; + return { + ref1: ref1, + notation: notation as '..' | '...', + ref2: ref2, + }; + } +} + +export interface GitBranchReference { + readonly refType: 'branch'; + name: string; + ref: string; + readonly remote: boolean; + readonly upstream?: { name: string; missing: boolean }; + repoPath: string; +} + +export interface GitRevisionReference { + readonly refType: 'revision' | 'stash'; + name: string; + ref: string; + repoPath: string; + + number?: string | undefined; + message?: string; +} + +export interface GitStashReference { + readonly refType: 'stash'; + name: string; + ref: string; + repoPath: string; + number: string | undefined; + + message?: string; +} + +export interface GitTagReference { + readonly refType: 'tag'; + name: string; + ref: string; + repoPath: string; +} + +export type GitReference = GitBranchReference | GitRevisionReference | GitStashReference | GitTagReference; + +export namespace GitReference { + export function create( + ref: string, + repoPath: string, + options: { refType: 'branch'; name: string; remote: boolean; upstream?: { name: string; missing: boolean } }, + ): GitBranchReference; + export function create( + ref: string, + repoPath: string, + options?: { refType: 'revision'; name?: string; message?: string }, + ): GitRevisionReference; + export function create( + ref: string, + repoPath: string, + options: { refType: 'stash'; name: string; number: string | undefined; message?: string }, + ): GitStashReference; + export function create(ref: string, repoPath: string, options: { refType: 'tag'; name: string }): GitTagReference; + export function create( + ref: string, + repoPath: string, + options: + | { refType: 'branch'; name: string; remote: boolean } + | { refType?: 'revision'; name?: string; message?: string } + | { refType: 'stash'; name: string; number: string | undefined; message?: string } + | { refType: 'tag'; name: string } = { refType: 'revision' }, + ): GitReference { + switch (options.refType) { + case 'branch': + return { + name: options.name, + ref: ref, + refType: 'branch', + remote: options.remote, + repoPath: repoPath, + }; + case 'stash': + return { + name: options.name, + ref: ref, + refType: 'stash', + repoPath: repoPath, + number: options.number, + message: options.message, + }; + case 'tag': + return { + name: options.name, + ref: ref, + refType: 'tag', + repoPath: repoPath, + }; + default: + return { + name: + options.name ?? GitRevision.shorten(ref, { force: true, strings: { working: 'Working Tree' } }), + ref: ref, + refType: 'revision', + repoPath: repoPath, + message: options.message, + }; + } + } + + export function fromBranch(branch: GitBranchReference) { + return create(branch.ref, branch.repoPath, { + refType: branch.refType, + name: branch.name, + remote: branch.remote, + upstream: branch.upstream, + }); + } + + export function fromTag(tag: GitTagReference) { + return create(tag.ref, tag.repoPath, { + refType: tag.refType, + name: tag.name, + }); + } + + export function getNameWithoutRemote(ref: GitReference) { + if (ref.refType === 'branch') { + return ref.remote ? GitBranch.getNameWithoutRemote(ref.name) : ref.name; + } + return ref.name; + } + + export function isBranch(ref: GitReference | undefined): ref is GitBranchReference { + return ref?.refType === 'branch'; + } + + export function isRevision(ref: GitReference | undefined): ref is GitRevisionReference { + return ref?.refType === 'revision'; + } + + export function isRevisionRange(ref: GitReference | undefined): ref is GitRevisionReference { + return ref?.refType === 'revision' && GitRevision.isRange(ref.ref); + } + + export function isStash(ref: GitReference | undefined): ref is GitStashReference { + return ref?.refType === 'stash' || (ref?.refType === 'revision' && (ref as any)?.stashName); + } + + export function isTag(ref: GitReference | undefined): ref is GitTagReference { + return ref?.refType === 'tag'; + } + + export function toString( + refs: GitReference | GitReference[] | undefined, + options?: { capitalize?: boolean; expand?: boolean; icon?: boolean; label?: boolean; quoted?: boolean } | false, + ) { + if (refs == null) return ''; + + options = + options === false + ? {} + : { expand: true, icon: true, label: options?.label ?? options?.expand ?? true, ...options }; + + let result; + if (!Array.isArray(refs) || refs.length === 1) { + const ref = Array.isArray(refs) ? refs[0] : refs; + let refName = options?.quoted ? `'${ref.name}'` : ref.name; + switch (ref.refType) { + case 'branch': + result = `${options.label ? `${ref.remote ? 'remote ' : ''}branch ` : ''}${ + options.icon ? `$(git-branch)${GlyphChars.Space}${refName}${GlyphChars.Space}` : refName + }`; + break; + case 'tag': + result = `${options.label ? 'tag ' : ''}${ + options.icon ? `$(tag)${GlyphChars.Space}${refName}${GlyphChars.Space}` : refName + }`; + break; + default: { + if (GitReference.isStash(ref)) { + let message; + if (options.expand && ref.message) { + message = `${ref.number != null ? `${ref.number}: ` : ''}${ + ref.message.length > 20 + ? `${ref.message.substring(0, 20).trimRight()}${GlyphChars.Ellipsis}` + : ref.message + }`; + } + + result = `${options.label ? 'stash ' : ''}${ + options.icon + ? `$(archive)${GlyphChars.Space}${message ?? ref.name}${GlyphChars.Space}` + : `${message ?? ref.number ?? ref.name}` + }`; + } else if (GitRevision.isRange(ref.ref)) { + result = refName; + } else { + let message; + if (options.expand && ref.message) { + message = + ref.message.length > 20 + ? ` (${ref.message.substring(0, 20).trimRight()}${GlyphChars.Ellipsis})` + : ` (${ref.message})`; + } + + let prefix; + if (options.expand && options.label && GitRevision.isShaParent(ref.ref)) { + refName = ref.name.endsWith('^') ? ref.name.substr(0, ref.name.length - 1) : ref.name; + if (options?.quoted) { + refName = `'${refName}'`; + } + prefix = 'before '; + } else { + prefix = ''; + } + + result = `${options.label ? `${prefix}commit ` : ''}${ + options.icon + ? `$(git-commit)${GlyphChars.Space}${refName}${message ?? ''}${GlyphChars.Space}` + : `${refName}${message ?? ''}` + }`; + } + break; + } + } + + return options.capitalize && options.expand && options.label !== false + ? `${result[0].toLocaleUpperCase()}${result.substring(1)}` + : result; + } + + const expanded = options.expand ? ` (${refs.map(r => r.name).join(', ')})` : ''; + switch (refs[0].refType) { + case 'branch': + return `${refs.length} branches${expanded}`; + case 'tag': + return `${refs.length} tags${expanded}`; + default: + return `${refs.length} ${GitReference.isStash(refs[0]) ? 'stashes' : 'commits'}${expanded}`; + } + } +} diff --git a/src/git/models/reflog.ts b/src/git/models/reflog.ts index 061704a..55abc07 100644 --- a/src/git/models/reflog.ts +++ b/src/git/models/reflog.ts @@ -1,7 +1,7 @@ 'use strict'; import { DateStyle } from '../../config'; import { Dates, memoize } from '../../system'; -import { CommitDateFormatting, GitRevision } from '../git'; +import { CommitDateFormatting, GitRevision } from '../models'; export interface GitReflog { readonly repoPath: string; diff --git a/src/git/models/remote.ts b/src/git/models/remote.ts index e004da6..e6b2d86 100644 --- a/src/git/models/remote.ts +++ b/src/git/models/remote.ts @@ -2,7 +2,7 @@ import { WorkspaceState } from '../../constants'; import { Container } from '../../container'; import { Strings } from '../../system'; -import { RemoteProvider, RichRemoteProvider } from '../remotes/factory'; +import { RemoteProvider, RichRemoteProvider } from '../remotes/provider'; export const enum GitRemoteType { Fetch = 'fetch', diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 944ec58..444de04 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -22,30 +22,23 @@ import { Logger, LogLevel } from '../../logger'; import { Messages } from '../../messages'; import { Arrays, Dates, debug, Functions, gate, Iterables, log, logName } from '../../system'; import { runGitCommandInTerminal } from '../../terminal'; -import { - GitBranch, - GitContributor, - GitDiffShortStat, - GitRemote, - GitStash, - GitStatus, - GitTag, - SearchPattern, -} from '../git'; import { GitProviderDescriptor } from '../gitProvider'; import { GitUri } from '../gitUri'; -import { RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory'; -import { - BranchSortOptions, - GitBranchReference, - GitLog, - GitLogCommit, - GitMergeStatus, - GitRebaseStatus, - GitReference, - GitTagReference, - TagSortOptions, -} from './models'; +import { RemoteProviderFactory, RemoteProviders } from '../remotes/factory'; +import { RichRemoteProvider } from '../remotes/provider'; +import { SearchPattern } from '../search'; +import { BranchSortOptions, GitBranch } from './branch'; +import { GitContributor } from './contributor'; +import { GitDiffShortStat } from './diff'; +import { GitLog } from './log'; +import { GitLogCommit } from './logCommit'; +import { GitMergeStatus } from './merge'; +import { GitRebaseStatus } from './rebase'; +import { GitBranchReference, GitReference, GitTagReference } from './reference'; +import { GitRemote } from './remote'; +import { GitStash } from './stash'; +import { GitStatus } from './status'; +import { GitTag, TagSortOptions } from './tag'; export const enum RepositoryChange { // FileSystem = 'filesystem', diff --git a/src/git/models/stashCommit.ts b/src/git/models/stashCommit.ts index 53825af..6a9b90a 100644 --- a/src/git/models/stashCommit.ts +++ b/src/git/models/stashCommit.ts @@ -1,10 +1,10 @@ 'use strict'; import { Container } from '../../container'; import { gate, memoize } from '../../system'; +import { GitReference } from '../models'; import { GitCommitType } from './commit'; import { GitFile, GitFileWorkingTreeStatus } from './file'; import { GitLogCommit } from './logCommit'; -import { GitReference } from './models'; const stashNumberRegex = /stash@{(\d+)}/; diff --git a/src/git/models/status.ts b/src/git/models/status.ts index 9bd7cf6..1f831a0 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -4,9 +4,9 @@ import { GlyphChars } from '../../constants'; import { Container } from '../../container'; import { memoize, Strings } from '../../system'; import { GitUri } from '../gitUri'; +import { GitCommitType, GitLogCommit, GitRemote, GitRevision, GitUser } from '../models'; import { GitBranch, GitTrackingState } from './branch'; import { GitFile, GitFileConflictStatus, GitFileIndexStatus, GitFileStatus, GitFileWorkingTreeStatus } from './file'; -import { GitCommitType, GitLogCommit, GitRemote, GitRevision, GitUser } from './models'; export interface ComputedWorkingTreeGitStatus { staged: number; diff --git a/src/git/models/tag.ts b/src/git/models/tag.ts index f33370c..499492f 100644 --- a/src/git/models/tag.ts +++ b/src/git/models/tag.ts @@ -1,7 +1,7 @@ 'use strict'; import { configuration, DateStyle, TagSorting } from '../../configuration'; import { Dates, memoize, Strings } from '../../system'; -import { GitReference, GitTagReference } from './models'; +import { GitReference, GitTagReference } from '../models'; export const TagDateFormatting = { dateFormat: undefined! as string | null, diff --git a/src/git/parsers.ts b/src/git/parsers.ts new file mode 100644 index 0000000..7329399 --- /dev/null +++ b/src/git/parsers.ts @@ -0,0 +1,13 @@ +'use strict'; + +export * from './parsers/blameParser'; +export * from './parsers/branchParser'; +export * from './parsers/diffParser'; +export * from './parsers/logParser'; +export * from './parsers/reflogParser'; +export * from './parsers/remoteParser'; +export * from './parsers/shortlogParser'; +export * from './parsers/stashParser'; +export * from './parsers/statusParser'; +export * from './parsers/tagParser'; +export * from './parsers/treeParser'; diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index 2461022..ac7d1d4 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -1,7 +1,7 @@ 'use strict'; import * as paths from 'path'; import { debug, Strings } from '../../system'; -import { GitAuthor, GitBlame, GitBlameCommit, GitCommitLine, GitRevision, GitUser } from '../git'; +import { GitAuthor, GitBlame, GitBlameCommit, GitCommitLine, GitRevision, GitUser } from '../models'; const emptyStr = ''; const slash = '/'; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index adc24eb..4fd9f3a 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -12,7 +12,7 @@ import { GitLogCommitLine, GitRevision, GitUser, -} from '../git'; +} from '../models'; const emptyEntry: LogEntry = {}; const emptyStr = ''; diff --git a/src/git/parsers/parsers.ts b/src/git/parsers/parsers.ts deleted file mode 100644 index 8a05da2..0000000 --- a/src/git/parsers/parsers.ts +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -export * from './blameParser'; -export * from './branchParser'; -export * from './diffParser'; -export * from './logParser'; -export * from './reflogParser'; -export * from './remoteParser'; -export * from './shortlogParser'; -export * from './stashParser'; -export * from './statusParser'; -export * from './tagParser'; -export * from './treeParser'; diff --git a/src/git/parsers/remoteParser.ts b/src/git/parsers/remoteParser.ts index 98c5bd7..cae0a43 100644 --- a/src/git/parsers/remoteParser.ts +++ b/src/git/parsers/remoteParser.ts @@ -1,8 +1,8 @@ 'use strict'; import { debug } from '../../system'; -import { GitRemote } from '../git'; +import { GitRemote } from '../models'; import { GitRemoteType } from '../models/remote'; -import { RemoteProvider } from '../remotes/factory'; +import { RemoteProvider } from '../remotes/provider'; const emptyStr = ''; diff --git a/src/git/parsers/shortlogParser.ts b/src/git/parsers/shortlogParser.ts index 87eec2e..1790dbe 100644 --- a/src/git/parsers/shortlogParser.ts +++ b/src/git/parsers/shortlogParser.ts @@ -1,6 +1,6 @@ 'use strict'; import { debug } from '../../system'; -import { GitContributor, GitShortLog, GitUser } from '../git'; +import { GitContributor, GitShortLog, GitUser } from '../models'; const shortlogRegex = /^(.*?)\t(.*?) <(.*?)>$/gm; const shortstatRegex = diff --git a/src/git/parsers/stashParser.ts b/src/git/parsers/stashParser.ts index 6363ede..d9aa639 100644 --- a/src/git/parsers/stashParser.ts +++ b/src/git/parsers/stashParser.ts @@ -1,6 +1,7 @@ 'use strict'; import { Arrays, debug, Strings } from '../../system'; -import { fileStatusRegex, GitCommitType, GitFile, GitFileIndexStatus, GitStash, GitStashCommit } from '../git'; +import { GitCommitType, GitFile, GitFileIndexStatus, GitStash, GitStashCommit } from '../models'; +import { fileStatusRegex } from './logParser'; // import { Logger } from './logger'; // Using %x00 codes because some shells seem to try to expand things if not diff --git a/src/git/parsers/statusParser.ts b/src/git/parsers/statusParser.ts index 54c26fa..cd37b73 100644 --- a/src/git/parsers/statusParser.ts +++ b/src/git/parsers/statusParser.ts @@ -1,6 +1,6 @@ 'use strict'; import { debug, Strings } from '../../system'; -import { GitStatus, GitStatusFile } from '../git'; +import { GitStatus, GitStatusFile } from '../models'; const emptyStr = ''; diff --git a/src/git/parsers/tagParser.ts b/src/git/parsers/tagParser.ts index 8e2b795..4053436 100644 --- a/src/git/parsers/tagParser.ts +++ b/src/git/parsers/tagParser.ts @@ -1,6 +1,6 @@ 'use strict'; import { debug } from '../../system'; -import { GitTag } from '../git'; +import { GitTag } from '../models'; const tagRegex = /^(.+)<\*r>(.*)(.*)(.*)(.*)(.*)$/gm; diff --git a/src/git/parsers/treeParser.ts b/src/git/parsers/treeParser.ts index e3cef4b..20492c7 100644 --- a/src/git/parsers/treeParser.ts +++ b/src/git/parsers/treeParser.ts @@ -1,6 +1,6 @@ 'use strict'; import { debug } from '../../system'; -import { GitTree } from '../git'; +import { GitTree } from '../models'; const emptyStr = ''; const treeRegex = /(?:.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+)/gm; diff --git a/src/git/providers/localGitProvider.ts b/src/git/providers/localGitProvider.ts deleted file mode 100644 index 7ee89d2..0000000 --- a/src/git/providers/localGitProvider.ts +++ /dev/null @@ -1,3790 +0,0 @@ -'use strict'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as paths from 'path'; -import { - Disposable, - env, - Event, - EventEmitter, - extensions, - Range, - TextEditor, - Uri, - window, - workspace, - WorkspaceFolder, -} from 'vscode'; -import { hrtime } from '@env/hrtime'; -import type { API as BuiltInGitApi, Repository as BuiltInGitRepository, GitExtension } from '../../@types/vscode.git'; -import { configuration } from '../../configuration'; -import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars } from '../../constants'; -import { Container } from '../../container'; -import { LogCorrelationContext, Logger } from '../../logger'; -import { Messages } from '../../messages'; -import { Arrays, debug, Functions, gate, Iterables, log, Paths, Promises, Strings, Versions } from '../../system'; -import { PromiseOrValue } from '../../system/promise'; -import { - CachedBlame, - CachedDiff, - CachedLog, - GitDocumentState, - TrackedDocument, -} from '../../trackers/gitDocumentTracker'; -import { - BranchSortOptions, - Git, - GitAuthor, - GitBlame, - GitBlameCommit, - GitBlameLine, - GitBlameLines, - GitBlameParser, - GitBranch, - GitBranchParser, - GitBranchReference, - GitCommitType, - GitContributor, - GitDiff, - GitDiffFilter, - GitDiffHunkLine, - GitDiffParser, - GitDiffShortStat, - GitErrors, - GitFile, - GitLog, - GitLogCommit, - GitLogParser, - GitMergeStatus, - GitRebaseStatus, - GitReference, - GitReflog, - GitRemote, - GitRemoteParser, - GitRevision, - GitStash, - GitStashParser, - GitStatus, - GitStatusFile, - GitStatusParser, - GitTag, - GitTagParser, - GitTree, - GitTreeParser, - GitUser, - isFolderGlob, - maxGitCliLength, - PullRequest, - PullRequestState, - Repository, - RepositoryChange, - RepositoryChangeComparisonMode, - RepositoryChangeEvent, - SearchPattern, - TagSortOptions, -} from '../git'; -import { GitProvider, GitProviderId, PagedResult, RepositoryInitWatcher, ScmRepository } from '../gitProvider'; -import { GitProviderService } from '../gitProviderService'; -import { GitUri } from '../gitUri'; -import { findGitPath, GitLocation, InvalidGitConfigError, UnableToFindGitError } from '../locator'; -import { GitReflogParser, GitShortLogParser } from '../parsers/parsers'; -import { RemoteProvider, RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory'; -import { fsExists, isWindows } from '../shell'; - -const emptyStr = ''; - -const RepoSearchWarnings = { - doesNotExist: /no such file or directory/i, -}; - -const doubleQuoteRegex = /"/g; -const driveLetterRegex = /(?<=^\/?)([a-zA-Z])(?=:\/)/; -const userConfigRegex = /^user\.(name|email) (.*)$/gm; -const mappedAuthorRegex = /(.+)\s<(.+)>/; - -const emptyPromise: Promise = Promise.resolve(undefined); -const reflogCommands = ['merge', 'pull']; - -export class LocalGitProvider implements GitProvider, Disposable { - descriptor = { id: GitProviderId.Git, name: 'Git' }; - - private _onDidChangeRepository = new EventEmitter(); - get onDidChangeRepository(): Event { - return this._onDidChangeRepository.event; - } - - private readonly _branchesCache = new Map>(); - private readonly _contributorsCache = new Map>(); - private readonly _mergeStatusCache = new Map(); - private readonly _rebaseStatusCache = new Map(); - private readonly _remotesWithApiProviderCache = new Map | null>(); - private readonly _stashesCache = new Map(); - private readonly _tagsCache = new Map>(); - private readonly _trackedCache = new Map>(); - private readonly _userMapCache = new Map(); - - constructor(private readonly container: Container) { - Git.setLocator(this.ensureGit.bind(this)); - } - - dispose() {} - - private get useCaching() { - return this.container.config.advanced.caching.enabled; - } - - private onRepositoryChanged(repo: Repository, e: RepositoryChangeEvent) { - if (e.changed(RepositoryChange.Config, RepositoryChangeComparisonMode.Any)) { - this._userMapCache.delete(repo.path); - } - - if (e.changed(RepositoryChange.Heads, RepositoryChange.Remotes, RepositoryChangeComparisonMode.Any)) { - this._branchesCache.delete(repo.path); - this._contributorsCache.delete(repo.path); - this._contributorsCache.delete(`stats|${repo.path}`); - } - - if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) { - this._remotesWithApiProviderCache.clear(); - } - - if (e.changed(RepositoryChange.Index, RepositoryChange.Unknown, RepositoryChangeComparisonMode.Any)) { - this._trackedCache.clear(); - } - - if (e.changed(RepositoryChange.Merge, RepositoryChangeComparisonMode.Any)) { - this._mergeStatusCache.delete(repo.path); - } - - if (e.changed(RepositoryChange.Rebase, RepositoryChangeComparisonMode.Any)) { - this._rebaseStatusCache.delete(repo.path); - } - - if (e.changed(RepositoryChange.Stash, RepositoryChangeComparisonMode.Any)) { - this._stashesCache.delete(repo.path); - } - - if (e.changed(RepositoryChange.Tags, RepositoryChangeComparisonMode.Any)) { - this._tagsCache.delete(repo.path); - } - - this._onDidChangeRepository.fire(e); - } - - private _gitLocator: Promise | undefined; - private async ensureGit(): Promise { - if (this._gitLocator == null) { - this._gitLocator = this.findGit(); - } - - return this._gitLocator; - } - - @log() - private async findGit(): Promise { - const cc = Logger.getCorrelationContext(); - - if (!configuration.getAny('git.enabled', null, true)) { - Logger.log(cc, 'Built-in Git is disabled ("git.enabled": false)'); - void Messages.showGitDisabledErrorMessage(); - - throw new UnableToFindGitError(); - } - - const scmPromise = this.getScmGitApi(); - - // Try to use the same git as the built-in vscode git extension, but only wait for a bit - const timeout = 100; - let gitPath; - try { - const gitApi = await Promises.cancellable(scmPromise, timeout); - gitPath = gitApi?.git.path; - } catch { - Logger.log(cc, `Stopped waiting for built-in Git, after ${timeout} ms...`); - } - - async function subscribeToScmOpenCloseRepository( - container: Container, - apiPromise: Promise, - ) { - const gitApi = await apiPromise; - if (gitApi == null) return; - - container.context.subscriptions.push( - gitApi.onDidCloseRepository(e => { - const repository = container.git.getCachedRepository(Strings.normalizePath(e.rootUri.fsPath)); - if (repository != null) { - repository.closed = true; - } - }), - gitApi.onDidOpenRepository(e => { - const repository = container.git.getCachedRepository(Strings.normalizePath(e.rootUri.fsPath)); - if (repository != null) { - repository.closed = false; - } - }), - ); - } - void subscribeToScmOpenCloseRepository(this.container, scmPromise); - - const start = hrtime(); - const location = await findGitPath(gitPath ?? configuration.getAny('git.path')); - - if (cc != null) { - cc.exitDetails = ` ${GlyphChars.Dot} Git found (${Strings.getDurationMilliseconds(start)} ms): ${ - location.version - } @ ${location.path === 'git' ? 'PATH' : location.path}`; - } else { - Logger.log( - cc, - `Git found: ${location.version} @ ${location.path === 'git' ? 'PATH' : location.path} ${ - GlyphChars.Dot - } ${Strings.getDurationMilliseconds(start)} ms`, - ); - } - - // Warn if git is less than v2.7.2 - if (Versions.compare(Versions.fromString(location.version), Versions.fromString('2.7.2')) === -1) { - Logger.log(cc, `Git version (${location.version}) is outdated`); - void Messages.showGitVersionUnsupportedErrorMessage(location.version, '2.7.2'); - } - - return location; - } - - async discoverRepositories(uri: Uri): Promise { - if (uri.scheme !== DocumentSchemes.File) return []; - - const autoRepositoryDetection = - configuration.getAny( - BuiltInGitConfiguration.AutoRepositoryDetection, - ) ?? true; - if (autoRepositoryDetection === false || autoRepositoryDetection === 'openEditors') return []; - - try { - void (await this.ensureGit()); - - const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!); - if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') { - for (const repository of repositories) { - void this.openScmRepository(repository.path); - } - } - - if (repositories.length > 0) { - this._trackedCache.clear(); - } - - return repositories; - } catch (ex) { - if (ex instanceof InvalidGitConfigError) { - void Messages.showGitInvalidConfigErrorMessage(); - } else if (ex instanceof UnableToFindGitError) { - void Messages.showGitMissingErrorMessage(); - } else { - const msg: string = ex?.message ?? ''; - if (msg) { - void window.showErrorMessage(`Unable to initialize Git; ${msg}`); - } - } - - throw ex; - } - } - - createRepository( - folder: WorkspaceFolder, - path: string, - root: boolean, - suspended?: boolean, - closed?: boolean, - ): Repository { - void this.openScmRepository(path); - return new Repository( - this.container, - this.onRepositoryChanged.bind(this), - this.descriptor, - folder, - path, - root, - suspended ?? !window.state.focused, - closed, - ); - } - - createRepositoryInitWatcher(): RepositoryInitWatcher { - const watcher = workspace.createFileSystemWatcher('**/.git', false, true, true); - return { - onDidCreate: watcher.onDidCreate, - dispose: () => watcher.dispose(), - }; - } - - @log({ - args: false, - singleLine: true, - prefix: (context, folder) => `${context.prefix}(${folder.uri.fsPath})`, - exit: result => - `returned ${result.length} repositories${ - result.length !== 0 ? ` (${result.map(r => r.path).join(', ')})` : emptyStr - }`, - }) - private async repositorySearch(folder: WorkspaceFolder): Promise { - const cc = Logger.getCorrelationContext(); - const { uri } = folder; - const depth = configuration.get('advanced.repositorySearchDepth', uri); - - Logger.log(cc, `searching (depth=${depth})...`); - - const repositories: Repository[] = []; - - const rootPath = await this.getRepoPath(uri.fsPath, true); - if (rootPath != null) { - Logger.log(cc, `found root repository in '${rootPath}'`); - repositories.push(this.createRepository(folder, rootPath, true)); - } - - if (depth <= 0) return repositories; - - // Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :) - let excludes = { - ...configuration.getAny>('files.exclude', uri, {}), - ...configuration.getAny>('search.exclude', uri, {}), - }; - - const excludedPaths = [ - ...Iterables.filterMap(Object.entries(excludes), ([key, value]) => { - if (!value) return undefined; - if (key.startsWith('**/')) return key.substring(3); - return key; - }), - ]; - - excludes = excludedPaths.reduce((accumulator, current) => { - accumulator[current] = true; - return accumulator; - }, Object.create(null) as Record); - - let repoPaths; - try { - repoPaths = await this.repositorySearchCore(uri.fsPath, depth, excludes); - } catch (ex) { - const msg: string = ex?.toString() ?? emptyStr; - if (RepoSearchWarnings.doesNotExist.test(msg)) { - Logger.log(cc, `FAILED${msg ? ` Error: ${msg}` : emptyStr}`); - } else { - Logger.error(ex, cc, 'FAILED'); - } - - return repositories; - } - - for (let p of repoPaths) { - p = paths.dirname(p); - // If we are the same as the root, skip it - if (Strings.normalizePath(p) === rootPath) continue; - - Logger.log(cc, `searching in '${p}'...`); - Logger.debug(cc, `normalizedRepoPath=${Strings.normalizePath(p)}, rootPath=${rootPath}`); - - const rp = await this.getRepoPath(p, true); - if (rp == null) continue; - - Logger.log(cc, `found repository in '${rp}'`); - repositories.push(this.createRepository(folder, rp, false)); - } - - return repositories; - } - - @debug({ args: { 2: false, 3: false } }) - private repositorySearchCore( - root: string, - depth: number, - excludes: Record, - repositories: string[] = [], - ): Promise { - const cc = Logger.getCorrelationContext(); - - return new Promise((resolve, reject) => { - fs.readdir(root, { withFileTypes: true }, async (err, files) => { - if (err != null) { - reject(err); - return; - } - - if (files.length === 0) { - resolve(repositories); - return; - } - - depth--; - - let f; - for (f of files) { - if (!f.isDirectory()) continue; - - if (f.name === '.git') { - repositories.push(paths.resolve(root, f.name)); - } else if (depth >= 0 && excludes[f.name] !== true) { - try { - await this.repositorySearchCore(paths.resolve(root, f.name), depth, excludes, repositories); - } catch (ex) { - Logger.error(ex, cc, 'FAILED'); - } - } - } - - resolve(repositories); - }); - }); - } - - @log() - async addRemote(repoPath: string, name: string, url: string): Promise { - await Git.remote__add(repoPath, name, url); - } - - @log() - async pruneRemote(repoPath: string, remoteName: string): Promise { - await Git.remote__prune(repoPath, remoteName); - } - - @log() - async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) { - const cc = Logger.getCorrelationContext(); - - ref1 = ref1 ?? uri.sha; - if (ref1 == null || uri.repoPath == null) return; - - if (ref2 == null) { - ref2 = ref1; - ref1 = `${ref1}^`; - } - - let patch; - try { - patch = await Git.diff(uri.repoPath, uri.fsPath, ref1, ref2); - void (await Git.apply(uri.repoPath, patch)); - } catch (ex) { - const msg: string = ex?.toString() ?? emptyStr; - if (patch && /patch does not apply/i.test(msg)) { - const result = await window.showWarningMessage( - 'Unable to apply changes cleanly. Retry and allow conflicts?', - { title: 'Yes' }, - { title: 'No', isCloseAffordance: true }, - ); - - if (result == null || result.title !== 'Yes') return; - - if (result.title === 'Yes') { - try { - void (await Git.apply(uri.repoPath, patch, { allowConflicts: true })); - - return; - } catch (e) { - // eslint-disable-next-line no-ex-assign - ex = e; - } - } - } - - Logger.error(ex, cc); - void Messages.showGenericErrorMessage('Unable to apply changes'); - } - } - - @log() - async branchContainsCommit(repoPath: string, name: string, ref: string): Promise { - let data = await Git.branch__containsOrPointsAt(repoPath, ref, { mode: 'contains', name: name }); - data = data?.trim(); - return Boolean(data); - } - - @log() - async checkout( - repoPath: string, - ref: string, - options?: { createBranch?: string } | { fileName?: string }, - ): Promise { - const cc = Logger.getCorrelationContext(); - - try { - await Git.checkout(repoPath, ref, options); - } catch (ex) { - const msg: string = ex?.toString() ?? emptyStr; - if (/overwritten by checkout/i.test(msg)) { - void Messages.showGenericErrorMessage( - `Unable to checkout '${ref}'. Please commit or stash your changes before switching branches`, - ); - return; - } - - Logger.error(ex, cc); - void void Messages.showGenericErrorMessage(`Unable to checkout '${ref}'`); - } - } - - @log() - resetCaches(...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]) { - if (cache.length === 0 || cache.includes('branches')) { - this._branchesCache.clear(); - } - - if (cache.length === 0 || cache.includes('contributors')) { - this._contributorsCache.clear(); - } - - if (cache.length === 0 || cache.includes('providers')) { - this._remotesWithApiProviderCache.clear(); - } - - if (cache.length === 0 || cache.includes('stashes')) { - this._stashesCache.clear(); - } - - if (cache.length === 0 || cache.includes('status')) { - this._mergeStatusCache.clear(); - this._rebaseStatusCache.clear(); - } - - if (cache.length === 0 || cache.includes('tags')) { - this._tagsCache.clear(); - } - - if (cache.length === 0) { - this._trackedCache.clear(); - this._userMapCache.clear(); - } - } - - @log({ args: { 1: uris => uris.length } }) - async excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise { - const paths = new Map(uris.map(u => [Strings.normalizePath(u.fsPath), u])); - - const data = await Git.check_ignore(repoPath, ...paths.keys()); - if (data == null) return uris; - - const ignored = data.split('\0').filter((i?: T): i is T => Boolean(i)); - if (ignored.length === 0) return uris; - - for (const file of ignored) { - paths.delete(file); - } - - return [...paths.values()]; - } - - @gate() - @log() - async fetch( - repoPath: string, - options: { all?: boolean; branch?: GitBranchReference; prune?: boolean; pull?: boolean; remote?: string } = {}, - ): Promise { - const { branch: branchRef, ...opts } = options; - if (GitReference.isBranch(branchRef)) { - const repo = await this.container.git.getRepository(repoPath); - const branch = await repo?.getBranch(branchRef?.name); - if (!branch?.remote && branch?.upstream == null) return undefined; - - return Git.fetch(repoPath, { - branch: branch.getNameWithoutRemote(), - remote: branch.getRemoteName()!, - upstream: branch.getTrackingWithoutRemote()!, - pull: options.pull, - }); - } - - return Git.fetch(repoPath, opts); - } - - @log() - async getBlameForFile(uri: GitUri): Promise { - const cc = Logger.getCorrelationContext(); - - let key = 'blame'; - if (uri.sha != null) { - key += `:${uri.sha}`; - } - - const doc = await this.container.tracker.getOrAdd(uri); - if (this.useCaching) { - if (doc.state != null) { - const cachedBlame = doc.state.get(key); - if (cachedBlame != null) { - Logger.debug(cc, `Cache hit: '${key}'`); - return cachedBlame.item; - } - } - - Logger.debug(cc, `Cache miss: '${key}'`); - - if (doc.state == null) { - doc.state = new GitDocumentState(doc.key); - } - } - - const promise = this.getBlameForFileCore(uri, doc, key, cc); - - if (doc.state != null) { - Logger.debug(cc, `Cache add: '${key}'`); - - const value: CachedBlame = { - item: promise as Promise, - }; - doc.state.set(key, value); - } - - return promise; - } - - private async getBlameForFileCore( - uri: GitUri, - document: TrackedDocument, - key: string, - cc: LogCorrelationContext | undefined, - ): Promise { - if (!(await this.isTracked(uri))) { - Logger.log(cc, `Skipping blame; '${uri.fsPath}' is not tracked`); - return emptyPromise as Promise; - } - - const [file, root] = Paths.splitPath(uri.fsPath, uri.repoPath, false); - - try { - const data = await Git.blame(root, file, uri.sha, { - args: this.container.config.advanced.blame.customArguments, - ignoreWhitespace: this.container.config.blame.ignoreWhitespace, - }); - const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); - return blame; - } catch (ex) { - // Trap and cache expected blame errors - if (document.state != null) { - const msg = ex?.toString() ?? ''; - Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); - - const value: CachedBlame = { - item: emptyPromise as Promise, - errorMessage: msg, - }; - document.state.set(key, value); - - document.setBlameFailure(); - - return emptyPromise as Promise; - } - - return undefined; - } - } - - @log({ args: { 1: '' } }) - async getBlameForFileContents(uri: GitUri, contents: string): Promise { - const cc = Logger.getCorrelationContext(); - - const key = `blame:${Strings.sha1(contents)}`; - - const doc = await this.container.tracker.getOrAdd(uri); - if (this.useCaching) { - if (doc.state != null) { - const cachedBlame = doc.state.get(key); - if (cachedBlame != null) { - Logger.debug(cc, `Cache hit: ${key}`); - return cachedBlame.item; - } - } - - Logger.debug(cc, `Cache miss: ${key}`); - - if (doc.state == null) { - doc.state = new GitDocumentState(doc.key); - } - } - - const promise = this.getBlameForFileContentsCore(uri, contents, doc, key, cc); - - if (doc.state != null) { - Logger.debug(cc, `Cache add: '${key}'`); - - const value: CachedBlame = { - item: promise as Promise, - }; - doc.state.set(key, value); - } - - return promise; - } - - async getBlameForFileContentsCore( - uri: GitUri, - contents: string, - document: TrackedDocument, - key: string, - cc: LogCorrelationContext | undefined, - ): Promise { - if (!(await this.isTracked(uri))) { - Logger.log(cc, `Skipping blame; '${uri.fsPath}' is not tracked`); - return emptyPromise as Promise; - } - - const [file, root] = Paths.splitPath(uri.fsPath, uri.repoPath, false); - - try { - const data = await Git.blame__contents(root, file, contents, { - args: this.container.config.advanced.blame.customArguments, - correlationKey: `:${key}`, - ignoreWhitespace: this.container.config.blame.ignoreWhitespace, - }); - const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); - return blame; - } catch (ex) { - // Trap and cache expected blame errors - if (document.state != null) { - const msg = ex?.toString() ?? ''; - Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); - - const value: CachedBlame = { - item: emptyPromise as Promise, - errorMessage: msg, - }; - document.state.set(key, value); - - document.setBlameFailure(); - return emptyPromise as Promise; - } - - return undefined; - } - } - - @log() - async getBlameForLine( - uri: GitUri, - editorLine: number, // editor lines are 0-based - options: { skipCache?: boolean } = {}, - ): Promise { - if (!options.skipCache && this.useCaching) { - const blame = await this.getBlameForFile(uri); - if (blame == null) return undefined; - - let blameLine = blame.lines[editorLine]; - if (blameLine == null) { - if (blame.lines.length !== editorLine) return undefined; - blameLine = blame.lines[editorLine - 1]; - } - - const commit = blame.commits.get(blameLine.sha); - if (commit == null) return undefined; - - const author = blame.authors.get(commit.author)!; - return { - author: { ...author, lineCount: commit.lines.length }, - commit: commit, - line: blameLine, - }; - } - - const lineToBlame = editorLine + 1; - const fileName = uri.fsPath; - - try { - const data = await Git.blame(uri.repoPath, fileName, uri.sha, { - args: this.container.config.advanced.blame.customArguments, - ignoreWhitespace: this.container.config.blame.ignoreWhitespace, - startLine: lineToBlame, - endLine: lineToBlame, - }); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); - if (blame == null) return undefined; - - return { - author: Iterables.first(blame.authors.values()), - commit: Iterables.first(blame.commits.values()), - line: blame.lines[editorLine], - }; - } catch { - return undefined; - } - } - - @log({ args: { 2: '' } }) - async getBlameForLineContents( - uri: GitUri, - editorLine: number, // editor lines are 0-based - contents: string, - options: { skipCache?: boolean } = {}, - ): Promise { - if (!options.skipCache && this.useCaching) { - const blame = await this.getBlameForFileContents(uri, contents); - if (blame == null) return undefined; - - let blameLine = blame.lines[editorLine]; - if (blameLine == null) { - if (blame.lines.length !== editorLine) return undefined; - blameLine = blame.lines[editorLine - 1]; - } - - const commit = blame.commits.get(blameLine.sha); - if (commit == null) return undefined; - - const author = blame.authors.get(commit.author)!; - return { - author: { ...author, lineCount: commit.lines.length }, - commit: commit, - line: blameLine, - }; - } - - const lineToBlame = editorLine + 1; - const fileName = uri.fsPath; - - try { - const data = await Git.blame__contents(uri.repoPath, fileName, contents, { - args: this.container.config.advanced.blame.customArguments, - ignoreWhitespace: this.container.config.blame.ignoreWhitespace, - startLine: lineToBlame, - endLine: lineToBlame, - }); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); - if (blame == null) return undefined; - - return { - author: Iterables.first(blame.authors.values()), - commit: Iterables.first(blame.commits.values()), - line: blame.lines[editorLine], - }; - } catch { - return undefined; - } - } - - @log() - async getBlameForRange(uri: GitUri, range: Range): Promise { - const blame = await this.getBlameForFile(uri); - if (blame == null) return undefined; - - return this.getBlameForRangeSync(blame, uri, range); - } - - @log({ args: { 2: '' } }) - async getBlameForRangeContents(uri: GitUri, range: Range, contents: string): Promise { - const blame = await this.getBlameForFileContents(uri, contents); - if (blame == null) return undefined; - - return this.getBlameForRangeSync(blame, uri, range); - } - - @log({ args: { 0: '' } }) - getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined { - if (blame.lines.length === 0) return { allLines: blame.lines, ...blame }; - - if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { - return { allLines: blame.lines, ...blame }; - } - - const lines = blame.lines.slice(range.start.line, range.end.line + 1); - const shas = new Set(lines.map(l => l.sha)); - - // ranges are 0-based - const startLine = range.start.line + 1; - const endLine = range.end.line + 1; - - const authors = new Map(); - const commits = new Map(); - for (const c of blame.commits.values()) { - if (!shas.has(c.sha)) continue; - - const commit = c.with({ - lines: c.lines.filter(l => l.line >= startLine && l.line <= endLine), - }); - commits.set(c.sha, commit); - - let author = authors.get(commit.author); - if (author == null) { - author = { - name: commit.author, - lineCount: 0, - }; - authors.set(author.name, author); - } - - author.lineCount += commit.lines.length; - } - - const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); - - return { - repoPath: uri.repoPath!, - authors: sortedAuthors, - commits: commits, - lines: lines, - allLines: blame.lines, - }; - } - - @log() - async getBranch(repoPath: string): Promise { - let { - values: [branch], - } = await this.getBranches(repoPath, { filter: b => b.current }); - if (branch != null) return branch; - - const data = await Git.rev_parse__currentBranch(repoPath, this.container.config.advanced.commitOrdering); - if (data == null) return undefined; - - const [name, upstream] = data[0].split('\n'); - if (GitBranch.isDetached(name)) { - const [rebaseStatus, committerDate] = await Promise.all([ - this.getRebaseStatus(repoPath), - Git.log__recent_committerdate(repoPath, this.container.config.advanced.commitOrdering), - ]); - - branch = new GitBranch( - repoPath, - rebaseStatus?.incoming.name ?? name, - false, - true, - committerDate != null ? new Date(Number(committerDate) * 1000) : undefined, - data[1], - upstream ? { name: upstream, missing: false } : undefined, - undefined, - undefined, - undefined, - rebaseStatus != null, - ); - } - - return branch; - } - - // @log({ - // args: { - // 0: b => b.name, - // }, - // }) - // async getBranchAheadRange(branch: GitBranch) { - // if (branch.state.ahead > 0) { - // return GitRevision.createRange(branch.upstream?.name, branch.ref); - // } - - // if (branch.upstream == null) { - // // If we have no upstream branch, try to find a best guess branch to use as the "base" - // const { values: branches } = await this.getBranches(branch.repoPath, { - // filter: b => weightedDefaultBranches.has(b.name), - // }); - // if (branches.length > 0) { - // let weightedBranch: { weight: number; branch: GitBranch } | undefined; - // for (const branch of branches) { - // const weight = weightedDefaultBranches.get(branch.name)!; - // if (weightedBranch == null || weightedBranch.weight < weight) { - // weightedBranch = { weight: weight, branch: branch }; - // } - - // if (weightedBranch.weight === maxDefaultBranchWeight) break; - // } - - // const possibleBranch = weightedBranch!.branch.upstream?.name ?? weightedBranch!.branch.ref; - // if (possibleBranch !== branch.ref) { - // return GitRevision.createRange(possibleBranch, branch.ref); - // } - // } - // } - - // return undefined; - // } - - @log({ args: { 1: false } }) - async getBranches( - repoPath: string | undefined, - options: { - filter?: (b: GitBranch) => boolean; - sort?: boolean | BranchSortOptions; - } = {}, - ): Promise> { - if (repoPath == null) return { values: [] }; - - let branchesPromise = this.useCaching ? this._branchesCache.get(repoPath) : undefined; - if (branchesPromise == null) { - async function load(this: LocalGitProvider) { - try { - const data = await Git.for_each_ref__branch(repoPath!, { all: true }); - // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch - if (data == null || data.length === 0) { - let current; - - const data = await Git.rev_parse__currentBranch( - repoPath!, - this.container.config.advanced.commitOrdering, - ); - if (data != null) { - const [name, upstream] = data[0].split('\n'); - const [rebaseStatus, committerDate] = await Promise.all([ - GitBranch.isDetached(name) ? this.getRebaseStatus(repoPath!) : undefined, - Git.log__recent_committerdate(repoPath!, this.container.config.advanced.commitOrdering), - ]); - - current = new GitBranch( - repoPath!, - rebaseStatus?.incoming.name ?? name, - false, - true, - committerDate != null ? new Date(Number(committerDate) * 1000) : undefined, - data[1], - { name: upstream, missing: false }, - undefined, - undefined, - undefined, - rebaseStatus != null, - ); - } - - return current != null ? [current] : []; - } - - return GitBranchParser.parse(data, repoPath!); - } catch (ex) { - this._branchesCache.delete(repoPath!); - - return []; - } - } - - branchesPromise = load.call(this); - - if (this.useCaching) { - this._branchesCache.set(repoPath, branchesPromise); - - if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { - this._branchesCache.delete(repoPath); - } - } - } - - let branches = await branchesPromise; - if (options.filter != null) { - branches = branches.filter(options.filter); - } - - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (options.sort) { - GitBranch.sort(branches, typeof options.sort === 'boolean' ? undefined : options.sort); - } - - return { values: branches }; - } - - // @log() - // async getBranchesAndTagsTipsFn(repoPath: string | undefined, currentName?: string) { - // const [{ values: branches }, { values: tags }] = await Promise.all([ - // this.getBranches(repoPath), - // this.getTags(repoPath), - // ]); - - // const branchesAndTagsBySha = Arrays.groupByFilterMap( - // (branches as (GitBranch | GitTag)[]).concat(tags as (GitBranch | GitTag)[]), - // bt => bt.sha, - // bt => { - // if (currentName) { - // if (bt.name === currentName) return undefined; - // if (bt.refType === 'branch' && bt.getNameWithoutRemote() === currentName) { - // return { name: bt.name, compactName: bt.getRemoteName(), type: bt.refType }; - // } - // } - - // return { name: bt.name, compactName: undefined, type: bt.refType }; - // }, - // ); - - // return (sha: string, options?: { compact?: boolean; icons?: boolean }): string | undefined => { - // const branchesAndTags = branchesAndTagsBySha.get(sha); - // if (branchesAndTags == null || branchesAndTags.length === 0) return undefined; - - // if (!options?.compact) { - // return branchesAndTags - // .map( - // bt => `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${bt.name}`, - // ) - // .join(', '); - // } - - // if (branchesAndTags.length > 1) { - // const [bt] = branchesAndTags; - // return `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${ - // bt.compactName ?? bt.name - // }, ${GlyphChars.Ellipsis}`; - // } - - // return branchesAndTags - // .map( - // bt => - // `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${ - // bt.compactName ?? bt.name - // }`, - // ) - // .join(', '); - // }; - // } - - @log() - async getChangedFilesCount(repoPath: string, ref?: string): Promise { - const data = await Git.diff__shortstat(repoPath, ref); - if (!data) return undefined; - - return GitDiffParser.parseShortStat(data); - } - - @log() - async getCommit(repoPath: string, ref: string): Promise { - const log = await this.getLog(repoPath, { limit: 2, ref: ref }); - if (log == null) return undefined; - - return log.commits.get(ref) ?? Iterables.first(log.commits.values()); - } - - @log() - async getCommitBranches( - repoPath: string, - ref: string, - options?: { mode?: 'contains' | 'pointsAt'; remotes?: boolean }, - ): Promise { - 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)); - } - - @log({ args: { 1: refs => refs.join(',') } }) - getAheadBehindCommitCount( - repoPath: string, - refs: string[], - ): Promise<{ ahead: number; behind: number } | undefined> { - return Git.rev_list__left_right(repoPath, refs); - } - - @log() - getCommitCount(repoPath: string, ref: string): Promise { - return Git.rev_list__count(repoPath, ref); - } - - @log() - async getCommitForFile( - repoPath: string | undefined, - fileName: string, - options: { ref?: string; firstIfNotFound?: boolean; range?: Range; reverse?: boolean } = {}, - ): Promise { - const cc = Logger.getCorrelationContext(); - - try { - const log = await this.getLogForFile(repoPath, fileName, { - limit: 2, - ref: options.ref, - range: options.range, - reverse: options.reverse, - }); - if (log == null) return undefined; - - const commit = options.ref ? log.commits.get(options.ref) : undefined; - if (commit == null && !options.firstIfNotFound && options.ref) { - // If the ref isn't a valid sha we will never find it, so let it fall through so we return the first - if (GitRevision.isSha(options.ref) || GitRevision.isUncommitted(options.ref)) return undefined; - } - - return commit ?? Iterables.first(log.commits.values()); - } catch (ex) { - Logger.error(ex, cc); - return undefined; - } - } - - @log() - async getOldestUnpushedRefForFile(repoPath: string, fileName: string): Promise { - const data = await Git.log__file(repoPath, fileName, '@{push}..', { - format: 'refs', - ordering: this.container.config.advanced.commitOrdering, - renames: true, - }); - if (data == null || data.length === 0) return undefined; - - return GitLogParser.parseLastRefOnly(data); - } - - @log() - getConfig(key: string, repoPath?: string): Promise { - return Git.config__get(key, repoPath); - } - - @log() - async getContributors( - repoPath: string, - options?: { all?: boolean; ref?: string; stats?: boolean }, - ): Promise { - if (repoPath == null) return []; - - const key = options?.stats ? `stats|${repoPath}` : repoPath; - - let contributors = this.useCaching ? this._contributorsCache.get(key) : undefined; - if (contributors == null) { - async function load(this: LocalGitProvider) { - try { - const currentUser = await this.getCurrentUser(repoPath); - - const data = await Git.log(repoPath, options?.ref, { - all: options?.all, - format: options?.stats ? 'shortlog+stats' : 'shortlog', - }); - const shortlog = GitShortLogParser.parseFromLog(data, repoPath, currentUser); - - return shortlog != null ? shortlog.contributors : []; - } catch (ex) { - this._contributorsCache.delete(key); - - return []; - } - } - - contributors = load.call(this); - - if (this.useCaching) { - this._contributorsCache.set(key, contributors); - - if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { - this._contributorsCache.delete(key); - } - } - } - - return contributors; - } - - @gate() - @log() - async getCurrentUser(repoPath: string): Promise { - let user = this._userMapCache.get(repoPath); - if (user != null) return user; - // If we found the repo, but no user data was found just return - if (user === null) return undefined; - - user = { name: undefined, email: undefined }; - - const data = await Git.config__get_regex('^user\\.', repoPath, { local: true }); - if (data) { - let key: string; - let value: string; - - let match; - do { - match = userConfigRegex.exec(data); - if (match == null) break; - - [, key, value] = match; - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - user[key as 'name' | 'email'] = ` ${value}`.substr(1); - } while (true); - } else { - user.name = - process.env.GIT_AUTHOR_NAME || process.env.GIT_COMMITTER_NAME || os.userInfo()?.username || undefined; - if (!user.name) { - // If we found no user data, mark it so we won't bother trying again - this._userMapCache.set(repoPath, null); - return undefined; - } - - user.email = - process.env.GIT_AUTHOR_EMAIL || - process.env.GIT_COMMITTER_EMAIL || - process.env.EMAIL || - `${user.name}@${os.hostname()}`; - } - - const author = `${user.name} <${user.email}>`; - // Check if there is a mailmap for the current user - const mappedAuthor = await Git.check_mailmap(repoPath, author); - if (mappedAuthor != null && mappedAuthor.length !== 0 && author !== mappedAuthor) { - const match = mappedAuthorRegex.exec(mappedAuthor); - if (match != null) { - [, user.name, user.email] = match; - } - } - - this._userMapCache.set(repoPath, user); - return user; - } - - @log() - async getDefaultBranchName(repoPath: string | undefined, remote?: string): Promise { - if (repoPath == null) return undefined; - - if (!remote) { - try { - const data = await Git.symbolic_ref(repoPath, 'HEAD'); - if (data != null) return data.trim(); - } catch {} - } - - remote = remote ?? 'origin'; - try { - const data = await Git.ls_remote__HEAD(repoPath, remote); - if (data == null) return undefined; - - const match = /ref:\s(\S+)\s+HEAD/m.exec(data); - if (match == null) return undefined; - - const [, branch] = match; - return branch.substr('refs/heads/'.length); - } catch { - return undefined; - } - } - - @log() - async getDiffForFile( - uri: GitUri, - ref1: string | undefined, - ref2?: string, - originalFileName?: string, - ): Promise { - const cc = Logger.getCorrelationContext(); - - let key = 'diff'; - if (ref1 != null) { - key += `:${ref1}`; - } - if (ref2 != null) { - key += `:${ref2}`; - } - - const doc = await this.container.tracker.getOrAdd(uri); - if (this.useCaching) { - if (doc.state != null) { - const cachedDiff = doc.state.get(key); - if (cachedDiff != null) { - Logger.debug(cc, `Cache hit: '${key}'`); - return cachedDiff.item; - } - } - - Logger.debug(cc, `Cache miss: '${key}'`); - - if (doc.state == null) { - doc.state = new GitDocumentState(doc.key); - } - } - - const promise = this.getDiffForFileCore( - uri.repoPath, - uri.fsPath, - ref1, - ref2, - originalFileName, - { encoding: GitProviderService.getEncoding(uri) }, - doc, - key, - cc, - ); - - if (doc.state != null) { - Logger.debug(cc, `Cache add: '${key}'`); - - const value: CachedDiff = { - item: promise as Promise, - }; - doc.state.set(key, value); - } - - return promise; - } - - private async getDiffForFileCore( - repoPath: string | undefined, - fileName: string, - ref1: string | undefined, - ref2: string | undefined, - originalFileName: string | undefined, - options: { encoding?: string }, - document: TrackedDocument, - key: string, - cc: LogCorrelationContext | undefined, - ): Promise { - const [file, root] = Paths.splitPath(fileName, repoPath, false); - - try { - // let data; - // if (ref2 == null && ref1 != null && !GitRevision.isUncommittedStaged(ref1)) { - // data = await Git.show__diff(root, file, ref1, originalFileName, { - // similarityThreshold: this.container.config.advanced.similarityThreshold, - // }); - // } else { - const data = await Git.diff(root, file, ref1, ref2, { - ...options, - filters: ['M'], - linesOfContext: 0, - renames: true, - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - // } - - const diff = GitDiffParser.parse(data); - return diff; - } catch (ex) { - // Trap and cache expected diff errors - if (document.state != null) { - const msg = ex?.toString() ?? ''; - Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); - - const value: CachedDiff = { - item: emptyPromise as Promise, - errorMessage: msg, - }; - document.state.set(key, value); - - return emptyPromise as Promise; - } - - return undefined; - } - } - - @log({ args: { 1: '' } }) - async getDiffForFileContents( - uri: GitUri, - ref: string, - contents: string, - originalFileName?: string, - ): Promise { - const cc = Logger.getCorrelationContext(); - - const key = `diff:${Strings.sha1(contents)}`; - - const doc = await this.container.tracker.getOrAdd(uri); - if (this.useCaching) { - if (doc.state != null) { - const cachedDiff = doc.state.get(key); - if (cachedDiff != null) { - Logger.debug(cc, `Cache hit: ${key}`); - return cachedDiff.item; - } - } - - Logger.debug(cc, `Cache miss: ${key}`); - - if (doc.state == null) { - doc.state = new GitDocumentState(doc.key); - } - } - - const promise = this.getDiffForFileContentsCore( - uri.repoPath, - uri.fsPath, - ref, - contents, - originalFileName, - { encoding: GitProviderService.getEncoding(uri) }, - doc, - key, - cc, - ); - - if (doc.state != null) { - Logger.debug(cc, `Cache add: '${key}'`); - - const value: CachedDiff = { - item: promise as Promise, - }; - doc.state.set(key, value); - } - - return promise; - } - - async getDiffForFileContentsCore( - repoPath: string | undefined, - fileName: string, - ref: string, - contents: string, - originalFileName: string | undefined, - options: { encoding?: string }, - document: TrackedDocument, - key: string, - cc: LogCorrelationContext | undefined, - ): Promise { - const [file, root] = Paths.splitPath(fileName, repoPath, false); - - try { - const data = await Git.diff__contents(root, file, ref, contents, { - ...options, - filters: ['M'], - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - - const diff = GitDiffParser.parse(data); - return diff; - } catch (ex) { - // Trap and cache expected diff errors - if (document.state != null) { - const msg = ex?.toString() ?? ''; - Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); - - const value: CachedDiff = { - item: emptyPromise as Promise, - errorMessage: msg, - }; - document.state.set(key, value); - - return emptyPromise as Promise; - } - - return undefined; - } - } - - @log() - async getDiffForLine( - uri: GitUri, - editorLine: number, // editor lines are 0-based - ref1: string | undefined, - ref2?: string, - originalFileName?: string, - ): Promise { - try { - const diff = await this.getDiffForFile(uri, ref1, ref2, originalFileName); - if (diff == null) return undefined; - - const line = editorLine + 1; - const hunk = diff.hunks.find(c => c.current.position.start <= line && c.current.position.end >= line); - if (hunk == null) return undefined; - - return hunk.lines[line - hunk.current.position.start]; - } catch (ex) { - return undefined; - } - } - - @log() - async getDiffStatus( - repoPath: string, - ref1?: string, - ref2?: string, - options: { filters?: GitDiffFilter[]; similarityThreshold?: number } = {}, - ): Promise { - try { - const data = await Git.diff__name_status(repoPath, ref1, ref2, { - similarityThreshold: this.container.config.advanced.similarityThreshold, - ...options, - }); - const files = GitDiffParser.parseNameStatus(data, repoPath); - return files == null || files.length === 0 ? undefined : files; - } catch (ex) { - return undefined; - } - } - - @log() - async getFileStatusForCommit(repoPath: string, fileName: string, ref: string): Promise { - if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return undefined; - - const data = await Git.show__name_status(repoPath, fileName, ref); - if (!data) return undefined; - - const files = GitDiffParser.parseNameStatus(data, repoPath); - if (files == null || files.length === 0) return undefined; - - return files[0]; - } - - @log() - async getLog( - repoPath: string, - { - ref, - ...options - }: { - all?: boolean; - authors?: string[]; - limit?: number; - merges?: boolean; - ordering?: string | null; - ref?: string; - reverse?: boolean; - since?: string; - } = {}, - ): Promise { - const limit = options.limit ?? this.container.config.advanced.maxListItems ?? 0; - - try { - const data = await Git.log(repoPath, ref, { - ...options, - limit: limit, - merges: options.merges == null ? true : options.merges, - ordering: options.ordering ?? this.container.config.advanced.commitOrdering, - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - const log = GitLogParser.parse( - data, - GitCommitType.Log, - repoPath, - undefined, - ref, - await this.getCurrentUser(repoPath), - limit, - options.reverse!, - undefined, - ); - - if (log != null) { - const opts = { ...options, ref: ref }; - log.query = (limit: number | undefined) => this.getLog(repoPath, { ...opts, limit: limit }); - if (log.hasMore) { - log.more = this.getLogMoreFn(log, opts); - } - } - - return log; - } catch (ex) { - return undefined; - } - } - - @log() - async getLogRefsOnly( - repoPath: string, - { - ref, - ...options - }: { - authors?: string[]; - limit?: number; - merges?: boolean; - ordering?: string | null; - ref?: string; - reverse?: boolean; - since?: string; - } = {}, - ): Promise | undefined> { - const limit = options.limit ?? this.container.config.advanced.maxListItems ?? 0; - - try { - const data = await Git.log(repoPath, ref, { - authors: options.authors, - format: 'refs', - limit: limit, - merges: options.merges == null ? true : options.merges, - reverse: options.reverse, - similarityThreshold: this.container.config.advanced.similarityThreshold, - since: options.since, - ordering: options.ordering ?? this.container.config.advanced.commitOrdering, - }); - const commits = GitLogParser.parseRefsOnly(data); - return new Set(commits); - } catch (ex) { - return undefined; - } - } - - private getLogMoreFn( - log: GitLog, - options: { - authors?: string[]; - limit?: number; - merges?: boolean; - ordering?: string | null; - ref?: string; - reverse?: boolean; - }, - ): (limit: number | { until: string } | undefined) => Promise { - return async (limit: number | { until: string } | undefined) => { - const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; - let moreLimit = typeof limit === 'number' ? limit : undefined; - - if (moreUntil && Iterables.some(log.commits.values(), c => c.ref === moreUntil)) { - return log; - } - - moreLimit = moreLimit ?? this.container.config.advanced.maxSearchItems ?? 0; - - // If the log is for a range, then just get everything prior + more - if (GitRevision.isRange(log.sha)) { - const moreLog = await this.getLog(log.repoPath, { - ...options, - limit: moreLimit === 0 ? 0 : (options.limit ?? 0) + moreLimit, - }); - // If we can't find any more, assume we have everything - if (moreLog == null) return { ...log, hasMore: false }; - - return moreLog; - } - - const ref = Iterables.last(log.commits.values())?.ref; - const moreLog = await this.getLog(log.repoPath, { - ...options, - limit: moreUntil == null ? moreLimit : 0, - ref: moreUntil == null ? `${ref}^` : `${moreUntil}^..${ref}^`, - }); - // If we can't find any more, assume we have everything - if (moreLog == null) return { ...log, hasMore: false }; - - // Merge authors - const authors = new Map([...log.authors]); - for (const [key, addAuthor] of moreLog.authors) { - const author = authors.get(key); - if (author == null) { - authors.set(key, addAuthor); - } else { - author.lineCount += addAuthor.lineCount; - } - } - - const commits = new Map([...log.commits, ...moreLog.commits]); - - const mergedLog: GitLog = { - repoPath: log.repoPath, - authors: authors, - commits: commits, - sha: log.sha, - range: undefined, - count: commits.size, - limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, - hasMore: moreUntil == null ? moreLog.hasMore : true, - query: (limit: number | undefined) => this.getLog(log.repoPath, { ...options, limit: limit }), - }; - mergedLog.more = this.getLogMoreFn(mergedLog, options); - - return mergedLog; - }; - } - - @log() - async getLogForSearch( - repoPath: string, - search: SearchPattern, - options: { limit?: number; ordering?: string | null; skip?: number } = {}, - ): Promise { - search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; - - try { - const limit = options.limit ?? this.container.config.advanced.maxSearchItems ?? 0; - const similarityThreshold = this.container.config.advanced.similarityThreshold; - - const operations = SearchPattern.parseSearchOperations(search.pattern); - - const searchArgs = new Set(); - const files: string[] = []; - - let useShow = false; - - let op; - let values = operations.get('commit:'); - if (values != null) { - useShow = true; - - searchArgs.add('-m'); - searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); - for (const value of values) { - searchArgs.add(value.replace(doubleQuoteRegex, '')); - } - } else { - searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); - searchArgs.add('--all'); - searchArgs.add('--full-history'); - searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings'); - if (search.matchRegex && !search.matchCase) { - searchArgs.add('--regexp-ignore-case'); - } - - for ([op, values] of operations.entries()) { - switch (op) { - case 'message:': - searchArgs.add('-m'); - if (search.matchAll) { - searchArgs.add('--all-match'); - } - for (const value of values) { - searchArgs.add( - `--grep=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, - ); - } - - break; - - case 'author:': - searchArgs.add('-m'); - for (const value of values) { - searchArgs.add( - `--author=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, - ); - } - - break; - - case 'change:': - for (const value of values) { - searchArgs.add( - search.matchRegex - ? `-G${value.replace(doubleQuoteRegex, '')}` - : `-S${value.replace(doubleQuoteRegex, '')}`, - ); - } - - break; - - case 'file:': - for (const value of values) { - files.push(value.replace(doubleQuoteRegex, '')); - } - - break; - } - } - } - - const args = [...searchArgs.values(), '--']; - if (files.length !== 0) { - args.push(...files); - } - - const data = await Git.log__search(repoPath, args, { - ordering: this.container.config.advanced.commitOrdering, - ...options, - limit: limit, - useShow: useShow, - }); - const log = GitLogParser.parse( - data, - GitCommitType.Log, - repoPath, - undefined, - undefined, - await this.getCurrentUser(repoPath), - limit, - false, - undefined, - ); - - if (log != null) { - log.query = (limit: number | undefined) => - this.getLogForSearch(repoPath, search, { ...options, limit: limit }); - if (log.hasMore) { - log.more = this.getLogForSearchMoreFn(log, search, options); - } - } - - return log; - } catch (ex) { - return undefined; - } - } - - private getLogForSearchMoreFn( - log: GitLog, - search: SearchPattern, - options: { limit?: number; ordering?: string | null }, - ): (limit: number | undefined) => Promise { - return async (limit: number | undefined) => { - limit = limit ?? this.container.config.advanced.maxSearchItems ?? 0; - - const moreLog = await this.getLogForSearch(log.repoPath, search, { - ...options, - limit: limit, - skip: log.count, - }); - if (moreLog == null) { - // If we can't find any more, assume we have everything - return { ...log, hasMore: false }; - } - - // Merge authors - const authors = new Map([...log.authors]); - for (const [key, addAuthor] of moreLog.authors) { - const author = authors.get(key); - if (author == null) { - authors.set(key, addAuthor); - } else { - author.lineCount += addAuthor.lineCount; - } - } - - const commits = new Map([...log.commits, ...moreLog.commits]); - - const mergedLog: GitLog = { - repoPath: log.repoPath, - authors: authors, - commits: commits, - sha: log.sha, - range: log.range, - count: commits.size, - limit: (log.limit ?? 0) + limit, - hasMore: moreLog.hasMore, - query: (limit: number | undefined) => - this.getLogForSearch(log.repoPath, search, { ...options, limit: limit }), - }; - mergedLog.more = this.getLogForSearchMoreFn(mergedLog, search, options); - - return mergedLog; - }; - } - @log() - async getLogForFile( - repoPath: string | undefined, - fileName: string, - options: { - all?: boolean; - limit?: number; - ordering?: string | null; - range?: Range; - ref?: string; - renames?: boolean; - reverse?: boolean; - since?: string; - skip?: number; - } = {}, - ): Promise { - if (repoPath != null && repoPath === Strings.normalizePath(fileName)) { - throw new Error(`File name cannot match the repository path; fileName=${fileName}`); - } - - const cc = Logger.getCorrelationContext(); - - options = { reverse: false, ...options }; - - if (options.renames == null) { - options.renames = this.container.config.advanced.fileHistoryFollowsRenames; - } - - let key = 'log'; - if (options.ref != null) { - key += `:${options.ref}`; - } - - if (options.all == null) { - options.all = this.container.config.advanced.fileHistoryShowAllBranches; - } - if (options.all) { - key += ':all'; - } - - options.limit = options.limit == null ? this.container.config.advanced.maxListItems || 0 : options.limit; - if (options.limit) { - key += `:n${options.limit}`; - } - - if (options.renames) { - key += ':follow'; - } - - if (options.reverse) { - key += ':reverse'; - } - - if (options.since) { - key += `:since=${options.since}`; - } - - if (options.skip) { - key += `:skip${options.skip}`; - } - - const doc = await this.container.tracker.getOrAdd(GitUri.fromFile(fileName, repoPath!, options.ref)); - if (this.useCaching && options.range == null) { - if (doc.state != null) { - const cachedLog = doc.state.get(key); - if (cachedLog != null) { - Logger.debug(cc, `Cache hit: '${key}'`); - return cachedLog.item; - } - - if (options.ref != null || options.limit != null) { - // Since we are looking for partial log, see if we have the log of the whole file - const cachedLog = doc.state.get( - `log${options.renames ? ':follow' : emptyStr}${options.reverse ? ':reverse' : emptyStr}`, - ); - if (cachedLog != null) { - if (options.ref == null) { - Logger.debug(cc, `Cache hit: ~'${key}'`); - return cachedLog.item; - } - - Logger.debug(cc, `Cache ?: '${key}'`); - let log = await cachedLog.item; - if (log != null && !log.hasMore && log.commits.has(options.ref)) { - Logger.debug(cc, `Cache hit: '${key}'`); - - // Create a copy of the log starting at the requested commit - let skip = true; - let i = 0; - const authors = new Map(); - const commits = new Map( - Iterables.filterMap<[string, GitLogCommit], [string, GitLogCommit]>( - log.commits.entries(), - ([ref, c]) => { - if (skip) { - if (ref !== options.ref) return undefined; - skip = false; - } - - i++; - if (options.limit != null && i > options.limit) { - return undefined; - } - - authors.set(c.author, log.authors.get(c.author)!); - return [ref, c]; - }, - ), - ); - - const opts = { ...options }; - log = { - ...log, - limit: options.limit, - count: commits.size, - commits: commits, - authors: authors, - query: (limit: number | undefined) => - this.getLogForFile(repoPath, fileName, { ...opts, limit: limit }), - }; - - return log; - } - } - } - } - - Logger.debug(cc, `Cache miss: '${key}'`); - - if (doc.state == null) { - doc.state = new GitDocumentState(doc.key); - } - } - - const promise = this.getLogForFileCore(repoPath, fileName, options, doc, key, cc); - - if (doc.state != null && options.range == null) { - Logger.debug(cc, `Cache add: '${key}'`); - - const value: CachedLog = { - item: promise as Promise, - }; - doc.state.set(key, value); - } - - return promise; - } - - private async getLogForFileCore( - repoPath: string | undefined, - fileName: string, - { - ref, - range, - ...options - }: { - all?: boolean; - limit?: number; - ordering?: string | null; - range?: Range; - ref?: string; - renames?: boolean; - reverse?: boolean; - since?: string; - skip?: number; - }, - document: TrackedDocument, - key: string, - cc: LogCorrelationContext | undefined, - ): Promise { - if (!(await this.isTracked(fileName, repoPath, ref))) { - Logger.log(cc, `Skipping log; '${fileName}' is not tracked`); - return emptyPromise as Promise; - } - - const [file, root] = Paths.splitPath(fileName, repoPath, false); - - try { - if (range != null && range.start.line > range.end.line) { - range = new Range(range.end, range.start); - } - - const data = await Git.log__file(root, file, ref, { - ordering: this.container.config.advanced.commitOrdering, - ...options, - firstParent: options.renames, - startLine: range == null ? undefined : range.start.line + 1, - endLine: range == null ? undefined : range.end.line + 1, - }); - const log = GitLogParser.parse( - data, - // 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, - await this.getCurrentUser(root), - options.limit, - options.reverse!, - range, - ); - - if (log != null) { - const opts = { ...options, ref: ref, range: range }; - log.query = (limit: number | undefined) => - this.getLogForFile(repoPath, fileName, { ...opts, limit: limit }); - if (log.hasMore) { - log.more = this.getLogForFileMoreFn(log, fileName, opts); - } - } - - return log; - } catch (ex) { - // Trap and cache expected log errors - if (document.state != null && range == null && !options.reverse) { - const msg: string = ex?.toString() ?? ''; - Logger.debug(cc, `Cache replace (with empty promise): '${key}'`); - - const value: CachedLog = { - item: emptyPromise as Promise, - errorMessage: msg, - }; - document.state.set(key, value); - - return emptyPromise as Promise; - } - - return undefined; - } - } - - private getLogForFileMoreFn( - log: GitLog, - fileName: string, - options: { - all?: boolean; - limit?: number; - ordering?: string | null; - range?: Range; - ref?: string; - renames?: boolean; - reverse?: boolean; - }, - ): (limit: number | { until: string } | undefined) => Promise { - return async (limit: number | { until: string } | undefined) => { - const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; - let moreLimit = typeof limit === 'number' ? limit : undefined; - - if (moreUntil && Iterables.some(log.commits.values(), c => c.ref === moreUntil)) { - return log; - } - - moreLimit = moreLimit ?? this.container.config.advanced.maxSearchItems ?? 0; - - const ref = Iterables.last(log.commits.values())?.ref; - const moreLog = await this.getLogForFile(log.repoPath, fileName, { - ...options, - limit: moreUntil == null ? moreLimit : 0, - ref: options.all ? undefined : moreUntil == null ? `${ref}^` : `${moreUntil}^..${ref}^`, - skip: options.all ? log.count : undefined, - }); - if (moreLog == null) { - // If we can't find any more, assume we have everything - return { ...log, hasMore: false }; - } - - // Merge authors - const authors = new Map([...log.authors]); - for (const [key, addAuthor] of moreLog.authors) { - const author = authors.get(key); - if (author == null) { - authors.set(key, addAuthor); - } else { - author.lineCount += addAuthor.lineCount; - } - } - - const commits = new Map([...log.commits, ...moreLog.commits]); - - const mergedLog: GitLog = { - repoPath: log.repoPath, - authors: authors, - commits: commits, - sha: log.sha, - range: log.range, - count: commits.size, - limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, - hasMore: moreUntil == null ? moreLog.hasMore : true, - query: (limit: number | undefined) => - this.getLogForFile(log.repoPath, fileName, { ...options, limit: limit }), - }; - - if (options.renames) { - const renamed = Iterables.find( - moreLog.commits.values(), - c => Boolean(c.originalFileName) && c.originalFileName !== fileName, - ); - if (renamed != null) { - fileName = renamed.originalFileName!; - } - } - - mergedLog.more = this.getLogForFileMoreFn(mergedLog, fileName, options); - - return mergedLog; - }; - } - - @log() - async getMergeBase(repoPath: string, ref1: string, ref2: string, options: { forkPoint?: boolean } = {}) { - const cc = Logger.getCorrelationContext(); - - try { - const data = await Git.merge_base(repoPath, ref1, ref2, options); - if (data == null) return undefined; - - return data.split('\n')[0].trim() || undefined; - } catch (ex) { - Logger.error(ex, cc); - return undefined; - } - } - - @gate() - @log() - async getMergeStatus(repoPath: string): Promise { - let status = this.useCaching ? this._mergeStatusCache.get(repoPath) : undefined; - if (status === undefined) { - const merge = await Git.rev_parse__verify(repoPath, 'MERGE_HEAD'); - if (merge != null) { - const [branch, mergeBase, possibleSourceBranches] = await Promise.all([ - this.getBranch(repoPath), - this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'), - this.getCommitBranches(repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }), - ]); - - status = { - type: 'merge', - repoPath: repoPath, - mergeBase: mergeBase, - HEAD: GitReference.create(merge, repoPath, { refType: 'revision' }), - current: GitReference.fromBranch(branch!), - incoming: - possibleSourceBranches?.length === 1 - ? GitReference.create(possibleSourceBranches[0], repoPath, { - refType: 'branch', - name: possibleSourceBranches[0], - remote: false, - }) - : undefined, - }; - } - - if (this.useCaching) { - this._mergeStatusCache.set(repoPath, status ?? null); - - if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { - this._mergeStatusCache.delete(repoPath); - } - } - } - - return status ?? undefined; - } - - @gate() - @log() - async getRebaseStatus(repoPath: string): Promise { - let status = this.useCaching ? this._rebaseStatusCache.get(repoPath) : undefined; - if (status === undefined) { - const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD'); - if (rebase != null) { - let [mergeBase, branch, onto, stepsNumber, stepsMessage, stepsTotal] = await Promise.all([ - this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'), - Git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']), - Git.readDotGitFile(repoPath, ['rebase-merge', 'onto']), - Git.readDotGitFile(repoPath, ['rebase-merge', 'msgnum'], { numeric: true }), - Git.readDotGitFile(repoPath, ['rebase-merge', 'message'], { throw: true }).catch(() => - Git.readDotGitFile(repoPath, ['rebase-merge', 'message-squashed']), - ), - Git.readDotGitFile(repoPath, ['rebase-merge', 'end'], { numeric: true }), - ]); - - if (branch == null || onto == null) return undefined; - - if (branch.startsWith('refs/heads/')) { - branch = branch.substr(11).trim(); - } - - const possibleSourceBranches = await this.getCommitBranches(repoPath, onto, { mode: 'pointsAt' }); - - let possibleSourceBranch: string | undefined; - for (const b of possibleSourceBranches) { - if (b.startsWith('(no branch, rebasing')) continue; - - possibleSourceBranch = b; - break; - } - - status = { - type: 'rebase', - repoPath: repoPath, - mergeBase: mergeBase, - HEAD: GitReference.create(rebase, repoPath, { refType: 'revision' }), - onto: GitReference.create(onto, repoPath, { refType: 'revision' }), - current: - possibleSourceBranch != null - ? GitReference.create(possibleSourceBranch, repoPath, { - refType: 'branch', - name: possibleSourceBranch, - remote: false, - }) - : undefined, - - incoming: GitReference.create(branch, repoPath, { - refType: 'branch', - name: branch, - remote: false, - }), - steps: { - current: { - number: stepsNumber ?? 0, - commit: GitReference.create(rebase, repoPath, { - refType: 'revision', - message: stepsMessage, - }), - }, - total: stepsTotal ?? 0, - }, - }; - } - - if (this.useCaching) { - this._rebaseStatusCache.set(repoPath, status ?? null); - - if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { - this._rebaseStatusCache.delete(repoPath); - } - } - } - - return status ?? undefined; - } - - @log() - async getNextDiffUris( - repoPath: string, - uri: Uri, - ref: string | undefined, - skip: number = 0, - ): Promise<{ current: GitUri; next: GitUri | undefined; deleted?: boolean } | undefined> { - // If we have no ref (or staged ref) there is no next commit - if (ref == null || ref.length === 0) return undefined; - - const fileName = GitUri.relativeTo(uri, repoPath); - - if (GitRevision.isUncommittedStaged(ref)) { - return { - current: GitUri.fromFile(fileName, repoPath, ref), - next: GitUri.fromFile(fileName, repoPath, undefined), - }; - } - - const next = await this.getNextUri(repoPath, uri, ref, skip); - if (next == null) { - const status = await this.getStatusForFile(repoPath, fileName); - if (status != null) { - // If the file is staged, diff with the staged version - if (status.indexStatus != null) { - return { - current: GitUri.fromFile(fileName, repoPath, ref), - next: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), - }; - } - } - - return { - current: GitUri.fromFile(fileName, repoPath, ref), - next: GitUri.fromFile(fileName, repoPath, undefined), - }; - } - - return { - current: - skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) - : (await this.getNextUri(repoPath, uri, ref, skip - 1))!, - next: next, - }; - } - - @log() - async getNextUri( - repoPath: string, - uri: Uri, - ref?: string, - skip: number = 0, - // editorLine?: number - ): Promise { - // If we have no ref (or staged ref) there is no next commit - if (ref == null || ref.length === 0 || GitRevision.isUncommittedStaged(ref)) return undefined; - - let filters: GitDiffFilter[] | undefined; - if (ref === GitRevision.deletedOrMissing) { - // If we are trying to move next from a deleted or missing ref then get the first commit - ref = undefined; - filters = ['A']; - } - - const fileName = GitUri.relativeTo(uri, repoPath); - let data = await Git.log__file(repoPath, fileName, ref, { - filters: filters, - format: 'simple', - limit: skip + 1, - ordering: this.container.config.advanced.commitOrdering, - reverse: true, - // startLine: editorLine != null ? editorLine + 1 : undefined, - }); - if (data == null || data.length === 0) return undefined; - - const [nextRef, file, status] = GitLogParser.parseSimple(data, skip); - // If the file was deleted, check for a possible rename - if (status === 'D') { - data = await Git.log__file(repoPath, '.', nextRef, { - filters: ['R', 'C'], - format: 'simple', - limit: 1, - ordering: this.container.config.advanced.commitOrdering, - // startLine: editorLine != null ? editorLine + 1 : undefined - }); - if (data == null || data.length === 0) { - return GitUri.fromFile(file ?? fileName, repoPath, nextRef); - } - - const [nextRenamedRef, renamedFile] = GitLogParser.parseSimpleRenamed(data, file ?? fileName); - return GitUri.fromFile( - renamedFile ?? file ?? fileName, - repoPath, - nextRenamedRef ?? nextRef ?? GitRevision.deletedOrMissing, - ); - } - - return GitUri.fromFile(file ?? fileName, repoPath, nextRef); - } - - @log() - async getPreviousDiffUris( - repoPath: string, - uri: Uri, - ref: string | undefined, - skip: number = 0, - firstParent: boolean = false, - ): Promise<{ current: GitUri; previous: GitUri | undefined } | undefined> { - if (ref === GitRevision.deletedOrMissing) return undefined; - - const fileName = GitUri.relativeTo(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) { - // First, check the file status to see if there is anything staged - const status = await this.getStatusForFile(repoPath, fileName); - 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 - if (status.indexStatus != null) { - // Backs up to get to HEAD - if (status.workingTreeStatus == null) { - skip++; - } - - if (skip === 0) { - // Diff working with staged - return { - current: GitUri.fromFile(fileName, repoPath, undefined), - previous: GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged), - }; - } - - return { - // Diff staged with HEAD (or prior if more skips) - current: GitUri.fromFile(fileName, 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), - previous: await this.getPreviousUri(repoPath, uri, undefined, skip, undefined, firstParent), - }; - } - } - } else if (skip === 0) { - skip++; - } - } - // If we are at the index (staged), diff staged with HEAD - else if (GitRevision.isUncommittedStaged(ref)) { - const current = - skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) - : (await this.getPreviousUri(repoPath, uri, undefined, skip - 1, undefined, firstParent))!; - if (current == null || current.sha === GitRevision.deletedOrMissing) return undefined; - - return { - current: current, - previous: await this.getPreviousUri(repoPath, uri, undefined, skip, undefined, firstParent), - }; - } - - // If we are at a commit, diff commit with previous - const current = - skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) - : (await this.getPreviousUri(repoPath, uri, ref, skip - 1, undefined, firstParent))!; - if (current == null || current.sha === GitRevision.deletedOrMissing) return undefined; - - return { - current: current, - previous: await this.getPreviousUri(repoPath, uri, ref, skip, undefined, firstParent), - }; - } - - @log() - async getPreviousLineDiffUris( - repoPath: string, - uri: Uri, - editorLine: number, - ref: string | undefined, - skip: number = 0, - ): Promise<{ current: GitUri; previous: GitUri | undefined; line: number } | undefined> { - if (ref === GitRevision.deletedOrMissing) return undefined; - - let fileName = GitUri.relativeTo(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) { - // First, check the blame on the current line to see if there are any working/staged changes - const gitUri = new GitUri(uri, repoPath); - - const document = await workspace.openTextDocument(uri); - const blameLine = document.isDirty - ? await this.getBlameForLineContents(gitUri, editorLine, document.getText()) - : await this.getBlameForLine(gitUri, editorLine); - if (blameLine == null) return undefined; - - // If line is uncommitted, we need to dig deeper to figure out where to go (because blame can't be trusted) - if (blameLine.commit.isUncommitted) { - // 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); - 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), - line: editorLine, - }; - } - } - - // Diff working with HEAD (or prior if more skips) - return { - current: GitUri.fromFile(fileName, repoPath, undefined), - previous: await this.getPreviousUri(repoPath, uri, undefined, skip, editorLine), - line: editorLine, - }; - } - - // First, check if we have a diff in the working tree - let hunkLine = await this.getDiffForLine(gitUri, editorLine, undefined); - if (hunkLine == null) { - // Next, check if we have a diff in the index (staged) - hunkLine = await this.getDiffForLine(gitUri, editorLine, undefined, GitRevision.uncommittedStaged); - - if (hunkLine != null) { - ref = GitRevision.uncommittedStaged; - } else { - skip++; - } - } - } - // 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.resolveToUri(fileName, repoPath); - editorLine = blameLine.line.originalLine - 1; - - if (skip === 0 && blameLine.commit.previousSha) { - previous = GitUri.fromFile(fileName, repoPath, blameLine.commit.previousSha); - } - } - } else { - if (GitRevision.isUncommittedStaged(ref)) { - const current = - skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) - : (await this.getPreviousUri(repoPath, uri, undefined, skip - 1, editorLine))!; - if (current.sha === GitRevision.deletedOrMissing) return undefined; - - return { - current: current, - previous: await this.getPreviousUri(repoPath, uri, undefined, skip, editorLine), - line: editorLine, - }; - } - - const gitUri = new GitUri(uri, { repoPath: repoPath, sha: ref }); - const blameLine = await this.getBlameForLine(gitUri, editorLine); - if (blameLine == null) return undefined; - - // Diff with line ref with previous - ref = blameLine.commit.sha; - fileName = blameLine.commit.fileName || (blameLine.commit.originalFileName ?? fileName); - uri = GitUri.resolveToUri(fileName, repoPath); - editorLine = blameLine.line.originalLine - 1; - - if (skip === 0 && blameLine.commit.previousSha) { - previous = GitUri.fromFile(fileName, repoPath, blameLine.commit.previousSha); - } - } - - const current = - skip === 0 - ? GitUri.fromFile(fileName, repoPath, ref) - : (await this.getPreviousUri(repoPath, uri, ref, skip - 1, editorLine))!; - if (current.sha === GitRevision.deletedOrMissing) return undefined; - - return { - current: current, - previous: previous ?? (await this.getPreviousUri(repoPath, uri, ref, skip, editorLine)), - line: editorLine, - }; - } - - @log() - async getPreviousUri( - repoPath: string, - uri: Uri, - ref?: string, - skip: number = 0, - editorLine?: number, - firstParent: boolean = false, - ): Promise { - if (ref === GitRevision.deletedOrMissing) return undefined; - - const cc = Logger.getCorrelationContext(); - - if (ref === GitRevision.uncommitted) { - ref = undefined; - } - - const fileName = GitUri.relativeTo(uri, repoPath); - // TODO: Add caching - let data; - try { - data = await Git.log__file(repoPath, fileName, ref, { - firstParent: firstParent, - format: 'simple', - limit: skip + 2, - ordering: this.container.config.advanced.commitOrdering, - startLine: editorLine != null ? editorLine + 1 : undefined, - }); - } catch (ex) { - const msg: string = ex?.toString() ?? emptyStr; - // 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); - if (status?.indexStatus != null) { - return GitUri.fromFile(fileName, repoPath, GitRevision.uncommittedStaged); - } - } - - ref = await Git.log__file_recent(repoPath, fileName, { - ordering: this.container.config.advanced.commitOrdering, - }); - return GitUri.fromFile(fileName, repoPath, ref ?? GitRevision.deletedOrMissing); - } - - Logger.error(ex, cc); - throw ex; - } - if (data == null || data.length === 0) return undefined; - - const [previousRef, file] = GitLogParser.parseSimple(data, skip, ref); - // 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); - } - - async getPullRequestForBranch( - branch: string, - remote: GitRemote, - options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, - ): Promise; - async getPullRequestForBranch( - branch: string, - provider: RichRemoteProvider, - options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, - ): Promise; - @gate((ref, remoteOrProvider, options) => { - const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; - return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}${ - options != null ? `|${options.limit ?? -1}:${options.include?.join(',')}` : '' - }`; - }) - @debug({ args: { 1: remoteOrProvider => remoteOrProvider.name } }) - async getPullRequestForBranch( - branch: string, - remoteOrProvider: GitRemote | RichRemoteProvider, - { - timeout, - ...options - }: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number } = {}, - ): Promise { - let provider; - if (GitRemote.is(remoteOrProvider)) { - ({ provider } = remoteOrProvider); - if (!provider?.hasApi()) return undefined; - } else { - provider = remoteOrProvider; - } - - let promiseOrPR = provider.getPullRequestForBranch(branch, options); - if (promiseOrPR == null || !Promises.is(promiseOrPR)) { - return promiseOrPR; - } - - if (timeout != null && timeout > 0) { - promiseOrPR = Promises.cancellable(promiseOrPR, timeout); - } - - try { - return await promiseOrPR; - } catch (ex) { - if (ex instanceof Promises.CancellationError) { - throw ex; - } - - return undefined; - } - } - - async getPullRequestForCommit( - ref: string, - remote: GitRemote, - options?: { timeout?: number }, - ): Promise; - async getPullRequestForCommit( - ref: string, - provider: RichRemoteProvider, - options?: { timeout?: number }, - ): Promise; - @gate((ref, remoteOrProvider, options) => { - const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; - return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}|${ - options?.timeout - }`; - }) - @debug({ args: { 1: remoteOrProvider => remoteOrProvider.name } }) - async getPullRequestForCommit( - ref: string, - remoteOrProvider: GitRemote | RichRemoteProvider, - { timeout }: { timeout?: number } = {}, - ): Promise { - if (GitRevision.isUncommitted(ref)) return undefined; - - let provider; - if (GitRemote.is(remoteOrProvider)) { - ({ provider } = remoteOrProvider); - if (!provider?.hasApi()) return undefined; - } else { - provider = remoteOrProvider; - } - - let promiseOrPR = provider.getPullRequestForCommit(ref); - if (promiseOrPR == null || !Promises.is(promiseOrPR)) { - return promiseOrPR; - } - - if (timeout != null && timeout > 0) { - promiseOrPR = Promises.cancellable(promiseOrPR, timeout); - } - - try { - return await promiseOrPR; - } catch (ex) { - if (ex instanceof Promises.CancellationError) { - throw ex; - } - - return undefined; - } - } - - @log() - async getIncomingActivity( - repoPath: string, - { - limit, - ...options - }: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number } = {}, - ): Promise { - const cc = Logger.getCorrelationContext(); - - limit = limit ?? this.container.config.advanced.maxListItems ?? 0; - try { - // Pass a much larger limit to reflog, because we aggregate the data and we won't know how many lines we'll need - const data = await Git.reflog(repoPath, { - ordering: this.container.config.advanced.commitOrdering, - ...options, - limit: limit * 100, - }); - if (data == null) return undefined; - - const reflog = GitReflogParser.parse(data, repoPath, reflogCommands, limit, limit * 100); - if (reflog?.hasMore) { - reflog.more = this.getReflogMoreFn(reflog, options); - } - - return reflog; - } catch (ex) { - Logger.error(ex, cc); - return undefined; - } - } - - private getReflogMoreFn( - reflog: GitReflog, - options: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number }, - ): (limit: number) => Promise { - return async (limit: number | undefined) => { - limit = limit ?? this.container.config.advanced.maxSearchItems ?? 0; - - const moreLog = await this.getIncomingActivity(reflog.repoPath, { - ...options, - limit: limit, - skip: reflog.total, - }); - if (moreLog == null) { - // If we can't find any more, assume we have everything - return { ...reflog, hasMore: false }; - } - - const mergedLog: GitReflog = { - repoPath: reflog.repoPath, - records: [...reflog.records, ...moreLog.records], - count: reflog.count + moreLog.count, - total: reflog.total + moreLog.total, - limit: (reflog.limit ?? 0) + limit, - hasMore: moreLog.hasMore, - }; - mergedLog.more = this.getReflogMoreFn(mergedLog, options); - - return mergedLog; - }; - } - - async getRichRemoteProvider( - repoPath: string | undefined, - options?: { includeDisconnected?: boolean }, - ): Promise | undefined>; - async getRichRemoteProvider( - remotes: GitRemote[], - options?: { includeDisconnected?: boolean }, - ): Promise | undefined>; - @gate( - (remotesOrRepoPath, options) => - `${typeof remotesOrRepoPath === 'string' ? remotesOrRepoPath : remotesOrRepoPath[0]?.repoPath}:${ - options?.includeDisconnected ?? false - }`, - ) - @log({ - args: { - 0: remotesOrRepoPath => - Array.isArray(remotesOrRepoPath) ? remotesOrRepoPath.map(r => r.name).join(',') : remotesOrRepoPath, - }, - }) - async getRichRemoteProvider( - remotesOrRepoPath: GitRemote[] | string | undefined, - { includeDisconnected }: { includeDisconnected?: boolean } = {}, - ): Promise | undefined> { - if (remotesOrRepoPath == null) return undefined; - - const cacheKey = `${includeDisconnected ? 'disconnected|' : ''}${ - typeof remotesOrRepoPath === 'string' ? remotesOrRepoPath : remotesOrRepoPath[0]?.repoPath - }`; - if (cacheKey != null) { - const remote = this._remotesWithApiProviderCache.get(cacheKey); - if (remote !== undefined) return remote ?? undefined; - } - - const remotes = ( - typeof remotesOrRepoPath === 'string' ? await this.getRemotes(remotesOrRepoPath) : remotesOrRepoPath - ).filter(r => r.provider != null); - - if (remotes.length === 0) return undefined; - - let remote; - if (remotes.length === 1) { - remote = remotes[0]; - } else { - const weightedRemotes = new Map([ - ['upstream', 15], - ['origin', 10], - ]); - - const branch = await this.getBranch(remotes[0].repoPath); - const branchRemote = branch?.getRemoteName(); - - if (branchRemote != null) { - weightedRemotes.set(branchRemote, 100); - } - - let bestRemote; - let weight = 0; - for (const r of remotes) { - if (r.default) { - bestRemote = r; - break; - } - - // Don't choose a remote unless its weighted above - const matchedWeight = weightedRemotes.get(r.name) ?? -1; - if (matchedWeight > weight) { - bestRemote = r; - weight = matchedWeight; - } - } - - remote = bestRemote ?? null; - } - - if (!remote?.provider?.hasApi()) { - if (cacheKey != null) { - this._remotesWithApiProviderCache.set(cacheKey, null); - } - return undefined; - } - - const { provider } = remote; - if (!includeDisconnected) { - const connected = provider.maybeConnected ?? (await provider.isConnected()); - if (!connected) { - if (cacheKey != null) { - this._remotesWithApiProviderCache.set(cacheKey, null); - } - return undefined; - } - } - - if (cacheKey != null) { - this._remotesWithApiProviderCache.set(cacheKey, remote as GitRemote); - } - return remote as GitRemote; - } - - @log() - async getRemotes( - repoPath: string | undefined, - options: { sort?: boolean } = {}, - ): Promise[]> { - if (repoPath == null) return []; - - const repository = await this.container.git.getRepository(repoPath); - const remotes = await (repository != null - ? repository.getRemotes({ sort: options.sort }) - : this.getRemotesCore(repoPath, undefined, { sort: options.sort })); - - return remotes.filter(r => r.provider != null) as GitRemote[]; - } - - async getRemotesCore( - repoPath: string | undefined, - providers?: RemoteProviders, - options: { sort?: boolean } = {}, - ): Promise { - if (repoPath == null) return []; - - providers = providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null)); - - try { - const data = await Git.remote(repoPath); - const remotes = GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providers)); - if (remotes == null) return []; - - if (options.sort) { - GitRemote.sort(remotes); - } - - return remotes; - } catch (ex) { - Logger.error(ex); - return []; - } - } - - @gate() - @debug() - async getRepoPath(filePath: string, isDirectory?: boolean): Promise { - const cc = Logger.getCorrelationContext(); - - let repoPath: string | undefined; - try { - let path: string; - if (isDirectory) { - path = filePath; - } else { - const stat = await new Promise(resolve => - fs.stat(filePath, (err, stat) => resolve(err == null ? stat : undefined)), - ); - path = stat?.isDirectory() ? filePath : paths.dirname(filePath); - } - - repoPath = await Git.rev_parse__show_toplevel(path); - if (repoPath == null) return repoPath; - - if (isWindows) { - // On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped drive path back, you get the UNC path for the mapped drive. - // So try to normalize it back to the mapped drive path, if possible - - const repoUri = Uri.file(repoPath); - const pathUri = Uri.file(path); - if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) { - const match = driveLetterRegex.exec(pathUri.path); - if (match != null) { - const [, letter] = match; - - try { - const networkPath = await new Promise(resolve => - fs.realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) => - resolve(err != null ? undefined : resolvedPath), - ), - ); - if (networkPath != null) { - repoPath = Strings.normalizePath( - repoUri.fsPath.replace( - networkPath, - `${letter.toLowerCase()}:${networkPath.endsWith('\\') ? '\\' : ''}`, - ), - ); - return repoPath; - } - } catch {} - } - - repoPath = Strings.normalizePath(pathUri.fsPath); - } - - return repoPath; - } - - // If we are not on Windows (symlinks don't seem to have the same issue on Windows), check if we are a symlink and if so, use the symlink path (not its resolved path) - // This is because VS Code will provide document Uris using the symlinked path - repoPath = await new Promise(resolve => { - fs.realpath(path, { encoding: 'utf8' }, (err, resolvedPath) => { - if (err != null) { - Logger.debug(cc, `fs.realpath failed; repoPath=${repoPath}`); - resolve(repoPath); - return; - } - - if (Strings.equalsIgnoreCase(path, resolvedPath)) { - Logger.debug(cc, `No symlink detected; repoPath=${repoPath}`); - resolve(repoPath); - return; - } - - const linkPath = Strings.normalizePath(resolvedPath, { stripTrailingSlash: true }); - repoPath = repoPath!.replace(linkPath, path); - Logger.debug( - cc, - `Symlink detected; repoPath=${repoPath}, path=${path}, resolvedPath=${resolvedPath}`, - ); - resolve(repoPath); - }); - }); - - return repoPath; - } catch (ex) { - Logger.error(ex, cc); - repoPath = undefined; - return repoPath; - } finally { - if (repoPath) { - void this.ensureProperWorkspaceCasing(repoPath, filePath); - } - } - } - - @gate(() => '') - private async ensureProperWorkspaceCasing(repoPath: string, filePath: string) { - if (this.container.config.advanced.messages.suppressImproperWorkspaceCasingWarning) return; - - filePath = filePath.replace(/\\/g, '/'); - - let regexPath; - let testPath; - if (filePath > repoPath) { - regexPath = filePath; - testPath = repoPath; - } else { - testPath = filePath; - regexPath = repoPath; - } - - let pathRegex = new RegExp(`^${regexPath}`); - if (!pathRegex.test(testPath)) { - pathRegex = new RegExp(pathRegex, 'i'); - if (pathRegex.test(testPath)) { - await Messages.showIncorrectWorkspaceCasingWarningMessage(); - } - } - } - - @gate() - @log() - async getStash(repoPath: string | undefined): Promise { - if (repoPath == null) return undefined; - - let stash = this.useCaching ? this._stashesCache.get(repoPath) : undefined; - if (stash === undefined) { - const data = await Git.stash__list(repoPath, { - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - stash = GitStashParser.parse(data, repoPath); - - if (this.useCaching) { - this._stashesCache.set(repoPath, stash ?? null); - - if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { - this._stashesCache.delete(repoPath); - } - } - } - - return stash ?? undefined; - } - - @log() - async getStatusForFile(repoPath: string, fileName: string): Promise { - const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; - - const data = await Git.status__file(repoPath, fileName, porcelainVersion, { - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - const status = GitStatusParser.parse(data, repoPath, porcelainVersion); - if (status == null || !status.files.length) return undefined; - - return status.files[0]; - } - - @log() - async getStatusForFiles(repoPath: string, path: string): Promise { - const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; - - const data = await Git.status__file(repoPath, path, porcelainVersion, { - similarityThreshold: this.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 { - if (repoPath == null) return undefined; - - const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; - - const data = await Git.status(repoPath, porcelainVersion, { - similarityThreshold: this.container.config.advanced.similarityThreshold, - }); - const status = GitStatusParser.parse(data, repoPath, porcelainVersion); - - if (status?.detached) { - const rebaseStatus = await this.getRebaseStatus(repoPath); - if (rebaseStatus != null) { - return new GitStatus( - repoPath, - rebaseStatus.incoming.name, - status.sha, - status.files, - status.state, - status.upstream, - true, - ); - } - } - return status; - } - - @log({ args: { 1: false } }) - async getTags( - repoPath: string | undefined, - options: { filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions } = {}, - ): Promise> { - if (repoPath == null) return { values: [] }; - - let tagsPromise = this.useCaching ? this._tagsCache.get(repoPath) : undefined; - if (tagsPromise == null) { - async function load(this: LocalGitProvider) { - try { - const data = await Git.tag(repoPath!); - return GitTagParser.parse(data, repoPath!) ?? []; - } catch (ex) { - this._tagsCache.delete(repoPath!); - - return []; - } - } - - tagsPromise = load.call(this); - - if (this.useCaching) { - this._tagsCache.set(repoPath, tagsPromise); - - if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { - this._tagsCache.delete(repoPath); - } - } - } - - let tags = await tagsPromise; - if (options.filter != null) { - tags = tags.filter(options.filter); - } - - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (options.sort) { - GitTag.sort(tags, typeof options.sort === 'boolean' ? undefined : options.sort); - } - - return { values: tags }; - } - - @log() - async getTreeFileForRevision(repoPath: string, fileName: string, ref: string): Promise { - if (repoPath == null || fileName == null || fileName.length === 0) return undefined; - - const data = await Git.ls_tree(repoPath, ref, { fileName: fileName }); - const trees = GitTreeParser.parse(data); - return trees?.length ? trees[0] : undefined; - } - - @log() - async getTreeForRevision(repoPath: string, ref: string): Promise { - if (repoPath == null) return []; - - const data = await Git.ls_tree(repoPath, ref); - return GitTreeParser.parse(data) ?? []; - } - - @log() - getVersionedFileBuffer(repoPath: string, fileName: string, ref: string): Promise { - return Git.show(repoPath, fileName, ref, { encoding: 'buffer' }); - } - - @log() - async getVersionedUri( - repoPath: string | undefined, - 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.resolveToUri(fileName, repoPath); - return (await fsExists(uri.fsPath)) ? uri : undefined; - } - - @log({ args: { 1: false } }) - async hasBranchOrTag( - repoPath: string | undefined, - { - filter, - }: { - filter?: { branches?: (b: GitBranch) => boolean; tags?: (t: GitTag) => boolean }; - } = {}, - ) { - const [{ values: branches }, { values: tags }] = await Promise.all([ - this.getBranches(repoPath, { - filter: filter?.branches, - sort: false, - }), - this.getTags(repoPath, { - filter: filter?.tags, - sort: false, - }), - ]); - - return branches.length !== 0 || tags.length !== 0; - } - - @log() - async hasRemotes(repoPath: string | undefined): Promise { - if (repoPath == null) return false; - - const repository = await this.container.git.getRepository(repoPath); - if (repository == null) return false; - - return repository.hasRemotes(); - } - - @log() - async hasTrackingBranch(repoPath: string | undefined): Promise { - if (repoPath == null) return false; - - const repository = await this.container.git.getRepository(repoPath); - if (repository == null) return false; - - return repository.hasUpstreamBranch(); - } - - @log({ - args: { 1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) }, - }) - async isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise { - if (repoPath == null) return false; - - editor = editor ?? window.activeTextEditor; - if (editor == null) return false; - - const doc = await this.container.tracker.getOrAdd(editor.document.uri); - return repoPath === doc?.uri.repoPath; - } - - isTrackable(uri: Uri): boolean { - const { scheme } = uri; - return ( - scheme === DocumentSchemes.File || - scheme === DocumentSchemes.Git || - scheme === DocumentSchemes.GitLens || - scheme === DocumentSchemes.PRs || - scheme === DocumentSchemes.Vsls - ); - } - - private async isTracked(filePath: string, repoPath?: string, ref?: string): Promise; - private async isTracked(uri: GitUri): Promise; - @log({ exit: tracked => `returned ${tracked}` /*, singleLine: true }*/ }) - private async isTracked(filePathOrUri: string | GitUri, repoPath?: string, ref?: string): Promise { - let cacheKey: string; - let relativeFilePath: string; - - if (typeof filePathOrUri === 'string') { - if (ref === GitRevision.deletedOrMissing) return false; - - cacheKey = ref ? `${ref}:${Strings.normalizePath(filePathOrUri)}` : Strings.normalizePath(filePathOrUri); - [relativeFilePath, repoPath] = Paths.splitPath(filePathOrUri, repoPath); - } else { - if (!this.isTrackable(filePathOrUri)) return false; - - // Always use the ref of the GitUri - ref = filePathOrUri.sha; - cacheKey = ref - ? `${ref}:${Strings.normalizePath(filePathOrUri.fsPath)}` - : Strings.normalizePath(filePathOrUri.fsPath); - relativeFilePath = filePathOrUri.fsPath; - repoPath = filePathOrUri.repoPath; - } - - if (ref != null) { - cacheKey = `${ref}:${cacheKey}`; - } - - let tracked = this._trackedCache.get(cacheKey); - if (tracked != null) return tracked; - - tracked = this.isTrackedCore(relativeFilePath, repoPath ?? emptyStr, ref); - this._trackedCache.set(cacheKey, tracked); - - tracked = await tracked; - this._trackedCache.set(cacheKey, tracked); - return tracked; - } - - @debug() - private async isTrackedCore(fileName: string, repoPath: string, ref?: string) { - if (ref === GitRevision.deletedOrMissing) return false; - - try { - // Even if we have a ref, check first to see if the file exists (that way the cache will be better reused) - let tracked = Boolean(await Git.ls_files(repoPath, fileName)); - if (!tracked && ref != null && !GitRevision.isUncommitted(ref)) { - tracked = Boolean(await Git.ls_files(repoPath, fileName, { ref: ref })); - // If we still haven't found this file, make sure it wasn't deleted in that ref (i.e. check the previous) - if (!tracked) { - tracked = Boolean(await Git.ls_files(repoPath, fileName, { ref: `${ref}^` })); - } - } - return tracked; - } catch (ex) { - Logger.error(ex); - return false; - } - } - - @log() - async getDiffTool(repoPath?: string): Promise { - return ( - (await Git.config__get('diff.guitool', repoPath, { local: true })) ?? - Git.config__get('diff.tool', repoPath, { local: true }) - ); - } - - @log() - async openDiffTool( - repoPath: string, - uri: Uri, - options: { ref1?: string; ref2?: string; staged?: boolean; tool?: string } = {}, - ): Promise { - try { - if (!options.tool) { - const cc = Logger.getCorrelationContext(); - - options.tool = this.container.config.advanced.externalDiffTool || (await this.getDiffTool(repoPath)); - if (options.tool == null) throw new Error('No diff tool found'); - - Logger.log(cc, `Using tool=${options.tool}`); - } - - const { tool, ...opts } = options; - await Git.difftool(repoPath, uri.fsPath, tool, opts); - } catch (ex) { - const msg: string = ex?.toString() ?? ''; - if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) { - const viewDocs = 'View Git Docs'; - const result = await window.showWarningMessage( - 'Unable to open changes because the specified diff tool cannot be found or no Git diff tool is configured', - viewDocs, - ); - if (result === viewDocs) { - void env.openExternal( - Uri.parse('https://git-scm.com/docs/git-config#Documentation/git-config.txt-difftool'), - ); - } - - return; - } - - Logger.error(ex, 'openDiffTool'); - void Messages.showGenericErrorMessage('Unable to open compare'); - } - } - - @log() - async openDirectoryCompare(repoPath: string, ref1: string, ref2?: string, tool?: string): Promise { - try { - if (!tool) { - const cc = Logger.getCorrelationContext(); - - tool = this.container.config.advanced.externalDirectoryDiffTool || (await this.getDiffTool(repoPath)); - if (tool == null) throw new Error('No diff tool found'); - - Logger.log(cc, `Using tool=${tool}`); - } - - await Git.difftool__dir_diff(repoPath, tool, ref1, ref2); - } catch (ex) { - const msg: string = ex?.toString() ?? ''; - if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) { - const viewDocs = 'View Git Docs'; - const result = await window.showWarningMessage( - 'Unable to open directory compare because the specified diff tool cannot be found or no Git diff tool is configured', - viewDocs, - ); - if (result === viewDocs) { - void env.openExternal( - Uri.parse('https://git-scm.com/docs/git-config#Documentation/git-config.txt-difftool'), - ); - } - - return; - } - - Logger.error(ex, 'openDirectoryCompare'); - void Messages.showGenericErrorMessage('Unable to open directory compare'); - } - } - - async resolveReference( - repoPath: string, - ref: string, - fileName?: string, - options?: { timeout?: number }, - ): Promise; - async resolveReference(repoPath: string, ref: string, uri?: Uri, options?: { timeout?: number }): Promise; - @log() - async resolveReference( - repoPath: string, - ref: string, - fileNameOrUri?: string | Uri, - options?: { timeout?: number }, - ) { - if (ref == null || ref.length === 0 || ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) { - return ref; - } - - if (fileNameOrUri == null) { - if (GitRevision.isSha(ref) || !GitRevision.isShaLike(ref) || ref.endsWith('^3')) return ref; - - return (await Git.rev_parse__verify(repoPath, ref)) ?? ref; - } - - const fileName = - typeof fileNameOrUri === 'string' - ? fileNameOrUri - : Strings.normalizePath(paths.relative(repoPath, fileNameOrUri.fsPath)); - - const blob = await Git.rev_parse__verify(repoPath, ref, fileName); - if (blob == null) return GitRevision.deletedOrMissing; - - let promise: Promise = Git.log__find_object( - repoPath, - blob, - ref, - this.container.config.advanced.commitOrdering, - fileName, - ); - if (options?.timeout != null) { - promise = Promise.race([promise, Functions.wait(options.timeout)]); - } - - return (await promise) ?? ref; - } - - @log() - validateBranchOrTagName(repoPath: string, ref: string): Promise { - return Git.check_ref_format(ref, repoPath); - } - - @log() - async validateReference(repoPath: string, ref: string): Promise { - if (ref == null || ref.length === 0) return false; - if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return true; - - return (await Git.rev_parse__verify(repoPath, ref)) != null; - } - - stageFile(repoPath: string, fileName: string): Promise; - stageFile(repoPath: string, uri: Uri): Promise; - @log() - async stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { - await Git.add( - repoPath, - typeof fileNameOrUri === 'string' ? fileNameOrUri : Paths.splitPath(fileNameOrUri.fsPath, repoPath)[0], - ); - } - - stageDirectory(repoPath: string, directory: string): Promise; - stageDirectory(repoPath: string, uri: Uri): Promise; - @log() - async stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { - await Git.add( - repoPath, - typeof directoryOrUri === 'string' ? directoryOrUri : Paths.splitPath(directoryOrUri.fsPath, repoPath)[0], - ); - } - - unStageFile(repoPath: string, fileName: string): Promise; - unStageFile(repoPath: string, uri: Uri): Promise; - @log() - async unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { - await Git.reset( - repoPath, - typeof fileNameOrUri === 'string' ? fileNameOrUri : Paths.splitPath(fileNameOrUri.fsPath, repoPath)[0], - ); - } - - unStageDirectory(repoPath: string, directory: string): Promise; - unStageDirectory(repoPath: string, uri: Uri): Promise; - @log() - async unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { - await Git.reset( - repoPath, - typeof directoryOrUri === 'string' ? directoryOrUri : Paths.splitPath(directoryOrUri.fsPath, repoPath)[0], - ); - } - - @log() - async stashApply( - repoPath: string, - stashName: string, - { deleteAfter }: { deleteAfter?: boolean } = {}, - ): Promise { - await Git.stash__apply(repoPath, stashName, Boolean(deleteAfter)); - } - - @log() - async stashDelete(repoPath: string, stashName: string, ref?: string): Promise { - await Git.stash__delete(repoPath, stashName, ref); - } - - @log({ args: { 2: uris => uris?.length } }) - async stashSave( - repoPath: string, - message?: string, - uris?: Uri[], - options: { includeUntracked?: boolean; keepIndex?: boolean } = {}, - ): Promise { - if (uris == null) return Git.stash__push(repoPath, message, options); - - await this.ensureGitVersion( - '2.13.2', - 'Stashing individual files', - ' Please retry by stashing everything or install a more recent version of Git and try again.', - ); - - const pathspecs = uris.map(u => `./${Paths.splitPath(u.fsPath, repoPath)[0]}`); - - const stdinVersion = '2.30.0'; - const stdin = await Git.isAtLeastVersion(stdinVersion); - // If we don't support stdin, then error out if we are over the maximum allowed git cli length - if (!stdin && Arrays.countStringLength(pathspecs) > maxGitCliLength) { - await this.ensureGitVersion( - stdinVersion, - `Stashing so many files (${pathspecs.length}) at once`, - ' Please retry by stashing fewer files or install a more recent version of Git and try again.', - ); - } - - return Git.stash__push(repoPath, message, { - ...options, - pathspecs: pathspecs, - stdin: stdin, - }); - } - - private _scmGitApi: Promise | undefined; - private async getScmGitApi(): Promise { - return this._scmGitApi ?? (this._scmGitApi = this.getScmGitApiCore()); - } - - @log() - private async getScmGitApiCore(): Promise { - try { - const extension = extensions.getExtension('vscode.git'); - if (extension == null) return undefined; - - const gitExtension = extension.isActive ? extension.exports : await extension.activate(); - return gitExtension?.getAPI(1); - } catch { - return undefined; - } - } - - @log() - async getOpenScmRepositories(): Promise { - const cc = Logger.getCorrelationContext(); - try { - const gitApi = await this.getScmGitApi(); - return gitApi?.repositories ?? []; - } catch (ex) { - Logger.error(ex, cc); - return []; - } - } - - @log() - async getOrOpenScmRepository(repoPath: string): Promise { - const cc = Logger.getCorrelationContext(); - try { - const gitApi = await this.getScmGitApi(); - if (gitApi?.openRepository != null) { - return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined; - } - - return gitApi?.getRepository(Uri.file(repoPath)) ?? undefined; - } catch (ex) { - Logger.error(ex, cc); - return undefined; - } - } - - @log() - private async openScmRepository(repoPath: string): Promise { - const cc = Logger.getCorrelationContext(); - try { - const gitApi = await this.getScmGitApi(); - return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined; - } catch (ex) { - Logger.error(ex, cc); - return undefined; - } - } - - private async ensureGitVersion(version: string, prefix: string, suffix: string): Promise { - if (await Git.isAtLeastVersion(version)) return; - - throw new Error( - `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${await Git.version()}).${suffix}`, - ); - } -} diff --git a/src/git/remotes/bitbucket-server.ts b/src/git/remotes/bitbucket-server.ts index 5f9516d..aadc5ee 100644 --- a/src/git/remotes/bitbucket-server.ts +++ b/src/git/remotes/bitbucket-server.ts @@ -2,7 +2,7 @@ import { Range, Uri } from 'vscode'; import { DynamicAutolinkReference } from '../../annotations/autolinks'; import { AutolinkReference } from '../../config'; -import { GitRevision } from '../models/models'; +import { GitRevision } from '../models'; import { Repository } from '../models/repository'; import { RemoteProvider } from './provider'; diff --git a/src/git/remotes/bitbucket.ts b/src/git/remotes/bitbucket.ts index e87ece1..898750a 100644 --- a/src/git/remotes/bitbucket.ts +++ b/src/git/remotes/bitbucket.ts @@ -2,7 +2,7 @@ import { Range, Uri } from 'vscode'; import { DynamicAutolinkReference } from '../../annotations/autolinks'; import { AutolinkReference } from '../../config'; -import { GitRevision } from '../models/models'; +import { GitRevision } from '../models'; import { Repository } from '../models/repository'; import { RemoteProvider } from './provider'; diff --git a/src/git/remotes/factory.ts b/src/git/remotes/factory.ts index d499f4f..3391146 100644 --- a/src/git/remotes/factory.ts +++ b/src/git/remotes/factory.ts @@ -9,9 +9,9 @@ import { GerritRemote } from './gerrit'; import { GiteaRemote } from './gitea'; import { GitHubRemote } from './github'; import { GitLabRemote } from './gitlab'; -import { RemoteProvider, RichRemoteProvider } from './provider'; +import { RemoteProvider } from './provider'; -export { RemoteProvider, RichRemoteProvider }; +// export { RemoteProvider, RichRemoteProvider }; export type RemoteProviders = { custom: boolean; matcher: string | RegExp; diff --git a/src/git/remotes/gerrit.ts b/src/git/remotes/gerrit.ts index d495c94..36c48ca 100644 --- a/src/git/remotes/gerrit.ts +++ b/src/git/remotes/gerrit.ts @@ -2,7 +2,7 @@ import { Range, Uri } from 'vscode'; import { DynamicAutolinkReference } from '../../annotations/autolinks'; import { AutolinkReference } from '../../config'; -import { GitRevision } from '../models/models'; +import { GitRevision } from '../models'; import { Repository } from '../models/repository'; import { RemoteProvider } from './provider'; diff --git a/src/git/remotes/gitea.ts b/src/git/remotes/gitea.ts index 010c641..4b9a0cd 100644 --- a/src/git/remotes/gitea.ts +++ b/src/git/remotes/gitea.ts @@ -2,7 +2,7 @@ import { Range, Uri } from 'vscode'; import { DynamicAutolinkReference } from '../../annotations/autolinks'; import { AutolinkReference } from '../../config'; -import { GitRevision } from '../models/models'; +import { GitRevision } from '../models'; import { Repository } from '../models/repository'; import { RemoteProvider } from './provider'; diff --git a/src/git/remotes/github.ts b/src/git/remotes/github.ts index 643243c..f032923 100644 --- a/src/git/remotes/github.ts +++ b/src/git/remotes/github.ts @@ -12,7 +12,7 @@ import { PullRequest, PullRequestState, Repository, -} from '../models/models'; +} from '../models'; import { RichRemoteProvider } from './provider'; const issueEnricher3rdPartyRegex = /\b(?[^/\s]+\/[^/\s]+)\\#(?[0-9]+)\b(?!]\()/g; diff --git a/src/git/remotes/gitlab.ts b/src/git/remotes/gitlab.ts index 3ca3621..8575296 100644 --- a/src/git/remotes/gitlab.ts +++ b/src/git/remotes/gitlab.ts @@ -2,7 +2,7 @@ import { Range, Uri } from 'vscode'; import { DynamicAutolinkReference } from '../../annotations/autolinks'; import { AutolinkReference } from '../../config'; -import { GitRevision } from '../models/models'; +import { GitRevision } from '../models'; import { Repository } from '../models/repository'; import { RemoteProvider } from './provider'; diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index 0b7d612..f84a447 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -24,7 +24,7 @@ import { PullRequestState, RemoteProviderReference, Repository, -} from '../models/models'; +} from '../models'; export const enum RemoteResourceType { Branch = 'branch', diff --git a/src/git/search.ts b/src/git/search.ts index 271d0e6..c658f92 100644 --- a/src/git/search.ts +++ b/src/git/search.ts @@ -1,6 +1,5 @@ 'use strict'; -import { GitRevision } from './git'; -import { GitRevisionReference } from './models/models'; +import { GitRevision, GitRevisionReference } from './models'; export type SearchOperators = | '' diff --git a/src/git/shell.ts b/src/git/shell.ts deleted file mode 100644 index ad59cfc..0000000 --- a/src/git/shell.ts +++ /dev/null @@ -1,223 +0,0 @@ -'use strict'; -import { ExecException, execFile } from 'child_process'; -import * as fs from 'fs'; -import * as paths from 'path'; -import * as iconv from 'iconv-lite'; -import { Logger } from '../logger'; - -export const isWindows = process.platform === 'win32'; - -const slashesRegex = /[\\/]/; -const ps1Regex = /\.ps1$/i; -const batOrCmdRegex = /\.(bat|cmd)$/i; -const jsRegex = /\.(js)$/i; - -/** - * Search PATH to see if a file exists in any of the path folders. - * - * @param {string} exe The file to search for - * @return {string} A fully qualified path, or the original path if nothing - * is found - * - * @private - */ -function runDownPath(exe: string): string { - // NB: Windows won't search PATH looking for executables in spawn like - // Posix does - - // Files with any directory path don't get this applied - if (slashesRegex.test(exe)) return exe; - - const target = paths.join('.', exe); - try { - const stats = fs.statSync(target); - if (stats?.isFile() && isExecutable(stats)) return target; - } catch {} - - const path = process.env.PATH; - if (path != null && path.length !== 0) { - const haystack = path.split(isWindows ? ';' : ':'); - let stats; - for (const p of haystack) { - const needle = paths.join(p, exe); - try { - stats = fs.statSync(needle); - if (stats?.isFile() && isExecutable(stats)) return needle; - } catch {} - } - } - - return exe; -} - -function isExecutable(stats: fs.Stats) { - if (isWindows) return true; - - const isGroup = stats.gid ? process.getgid != null && stats.gid === process.getgid() : true; - const isUser = stats.uid ? process.getuid != null && stats.uid === process.getuid() : true; - - return Boolean(stats.mode & 0o0001 || (stats.mode & 0o0010 && isGroup) || (stats.mode & 0o0100 && isUser)); -} - -/** - * Finds the executable and parameters to run on Windows. This method - * mimics the POSIX behavior of being able to run scripts as executables by - * replacing the passed-in executable with the script runner, for PowerShell, - * CMD, and node scripts. - * - * This method also does the work of running down PATH, which spawn on Windows - * also doesn't do, unlike on POSIX. - */ -export function findExecutable(exe: string, args: string[]): { cmd: string; args: string[] } { - // POSIX can just execute scripts directly, no need for silly goosery - if (!isWindows) return { cmd: runDownPath(exe), args: args }; - - if (!fs.existsSync(exe)) { - // NB: When you write something like `surf-client ... -- surf-build` on Windows, - // a shell would normally convert that to surf-build.cmd, but since it's passed - // in as an argument, it doesn't happen - const possibleExts = ['.exe', '.bat', '.cmd', '.ps1']; - for (const ext of possibleExts) { - const possibleFullPath = runDownPath(`${exe}${ext}`); - - if (fs.existsSync(possibleFullPath)) return findExecutable(possibleFullPath, args); - } - } - - if (ps1Regex.test(exe)) { - const cmd = paths.join( - process.env.SYSTEMROOT ?? 'C:\\WINDOWS', - 'System32', - 'WindowsPowerShell', - 'v1.0', - 'PowerShell.exe', - ); - const psargs = ['-ExecutionPolicy', 'Unrestricted', '-NoLogo', '-NonInteractive', '-File', exe]; - - return { cmd: cmd, args: psargs.concat(args) }; - } - - if (batOrCmdRegex.test(exe)) { - const cmd = paths.join(process.env.SYSTEMROOT ?? 'C:\\WINDOWS', 'System32', 'cmd.exe'); - const cmdArgs = ['/C', exe, ...args]; - - return { cmd: cmd, args: cmdArgs }; - } - - if (jsRegex.test(exe)) { - const cmd = process.execPath; - const nodeArgs = [exe]; - - return { cmd: cmd, args: nodeArgs.concat(args) }; - } - - return { cmd: exe, args: args }; -} - -export interface RunOptions { - cwd?: string; - readonly env?: Record; - readonly encoding?: TEncoding; - /** - * The size the output buffer to allocate to the spawned process. Set this - * if you are anticipating a large amount of output. - * - * If not specified, this will be 10MB (10485760 bytes) which should be - * enough for most Git operations. - */ - readonly maxBuffer?: number; - /** - * An optional string or buffer which will be written to - * the child process stdin stream immediately immediately - * after spawning the process. - */ - readonly stdin?: string | Buffer; - /** - * The encoding to use when writing to stdin, if the stdin - * parameter is a string. - */ - readonly stdinEncoding?: string; -} - -const bufferExceededRegex = /stdout maxBuffer( length)? exceeded/; - -export class RunError extends Error { - constructor( - private readonly original: ExecException, - public readonly stdout: string, - public readonly stderr: string, - ) { - super(original.message); - - stdout = stdout.trim(); - stderr = stderr.trim(); - Error.captureStackTrace?.(this, RunError); - } - - get cmd(): string | undefined { - return this.original.cmd; - } - - get killed(): boolean | undefined { - return this.original.killed; - } - - get code(): number | undefined { - return this.original.code; - } - - get signal(): NodeJS.Signals | undefined { - return this.original.signal; - } -} - -export function run( - command: string, - args: any[], - encoding: BufferEncoding | 'buffer' | string, - options: RunOptions = {}, -): Promise { - const { stdin, stdinEncoding, ...opts }: RunOptions = { maxBuffer: 100 * 1024 * 1024, ...options }; - - return new Promise((resolve, reject) => { - const proc = execFile(command, args, opts, (error: ExecException | null, stdout, stderr) => { - if (error != null) { - if (bufferExceededRegex.test(error.message)) { - error.message = `Command output exceeded the allocated stdout buffer. Set 'options.maxBuffer' to a larger value than ${opts.maxBuffer} bytes`; - } - - reject( - new RunError( - error, - encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer' - ? stdout - : iconv.decode(Buffer.from(stdout, 'binary'), encoding), - encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer' - ? stderr - : iconv.decode(Buffer.from(stderr, 'binary'), encoding), - ), - ); - - return; - } - - if (stderr) { - Logger.warn(`Warning(${command} ${args.join(' ')}): ${stderr}`); - } - - resolve( - encoding === 'utf8' || encoding === 'binary' || encoding === 'buffer' - ? (stdout as TOut) - : (iconv.decode(Buffer.from(stdout, 'binary'), encoding) as TOut), - ); - }); - - if (stdin != null) { - proc.stdin?.end(stdin, (stdinEncoding ?? 'utf8') as BufferEncoding); - } - }); -} - -export function fsExists(path: string) { - return new Promise(resolve => fs.exists(path, exists => resolve(exists))); -} diff --git a/src/github/github.ts b/src/github/github.ts index 0902133..485bd8b 100644 --- a/src/github/github.ts +++ b/src/github/github.ts @@ -1,16 +1,14 @@ 'use strict'; import { graphql } from '@octokit/graphql'; import { - AuthenticationError, - ClientError, DefaultBranch, IssueOrPullRequest, IssueOrPullRequestType, PullRequest, PullRequestState, - RichRemoteProvider, -} from '../git/git'; +} from '../git/models'; import { Account } from '../git/models/author'; +import { AuthenticationError, ClientError, RichRemoteProvider } from '../git/remotes/provider'; import { Logger } from '../logger'; import { debug } from '../system'; diff --git a/src/hovers/hovers.ts b/src/hovers/hovers.ts index 6fc0902..b8b8510 100644 --- a/src/hovers/hovers.ts +++ b/src/hovers/hovers.ts @@ -1,11 +1,12 @@ 'use strict'; import { CancellationToken, MarkdownString } from 'vscode'; +import { hrtime } from '@env/hrtime'; import { DiffWithCommand, ShowQuickCommitCommand } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { hrtime } from '../env/node/hrtime'; +import { CommitFormatter } from '../git/formatters'; +import { GitUri } from '../git/gitUri'; import { - CommitFormatter, GitBlameCommit, GitCommit, GitDiffHunk, @@ -14,8 +15,7 @@ import { GitRemote, GitRevision, PullRequest, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { Logger, LogLevel } from '../logger'; import { Iterables, Promises, Strings } from '../system'; diff --git a/src/messages.ts b/src/messages.ts index 2f98d08..e48ca2d 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,7 +1,7 @@ 'use strict'; import { ConfigurationTarget, env, MessageItem, Uri, window } from 'vscode'; import { configuration } from './configuration'; -import { GitCommit } from './git/git'; +import { GitCommit } from './git/models'; import { Logger } from './logger'; export const enum SuppressedMessages { diff --git a/src/quickpicks/commitPicker.ts b/src/quickpicks/commitPicker.ts index c658ac4..3af98d1 100644 --- a/src/quickpicks/commitPicker.ts +++ b/src/quickpicks/commitPicker.ts @@ -2,7 +2,7 @@ import { Disposable, window } from 'vscode'; import { configuration } from '../configuration'; import { Container } from '../container'; -import { GitLog, GitLogCommit, GitStash, GitStashCommit } from '../git/git'; +import { GitLog, GitLogCommit, GitStash, GitStashCommit } from '../git/models'; import { KeyboardScope, Keys } from '../keyboard'; import { CommandQuickPickItem, diff --git a/src/quickpicks/commitQuickPickItems.ts b/src/quickpicks/commitQuickPickItems.ts index 7fa78e6..89a455a 100644 --- a/src/quickpicks/commitQuickPickItems.ts +++ b/src/quickpicks/commitQuickPickItems.ts @@ -4,7 +4,8 @@ import { QuickPickItem, window } from 'vscode'; import { Commands, GitActions, OpenChangedFilesCommandArgs } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { CommitFormatter, GitFile, GitLogCommit, GitStatusFile } from '../git/git'; +import { CommitFormatter } from '../git/formatters'; +import { GitFile, GitLogCommit, GitStatusFile } from '../git/models'; import { Keys } from '../keyboard'; import { Strings } from '../system'; import { CommandQuickPickItem } from './quickPicksItems'; diff --git a/src/quickpicks/gitQuickPickItems.ts b/src/quickpicks/gitQuickPickItems.ts index d479c86..9ef03a4 100644 --- a/src/quickpicks/gitQuickPickItems.ts +++ b/src/quickpicks/gitQuickPickItems.ts @@ -13,7 +13,7 @@ import { GitStashCommit, GitTag, Repository, -} from '../git/git'; +} from '../git/models'; import { Dates, Strings } from '../system'; import { CommandQuickPickItem, QuickPickItemOfT } from './quickPicksItems'; diff --git a/src/quickpicks/quickPicksItems.ts b/src/quickpicks/quickPicksItems.ts index eace950..aad6494 100644 --- a/src/quickpicks/quickPicksItems.ts +++ b/src/quickpicks/quickPicksItems.ts @@ -2,7 +2,8 @@ import { commands, QuickPickItem } from 'vscode'; import { Commands, GitActions } from '../commands'; import { Container } from '../container'; -import { GitReference, GitRevisionReference, GitStashCommit, SearchPattern } from '../git/git'; +import { GitReference, GitRevisionReference, GitStashCommit } from '../git/models'; +import { SearchPattern } from '../git/search'; import { Keys } from '../keyboard'; declare module 'vscode' { diff --git a/src/quickpicks/referencePicker.ts b/src/quickpicks/referencePicker.ts index 24f07b4..ea268be 100644 --- a/src/quickpicks/referencePicker.ts +++ b/src/quickpicks/referencePicker.ts @@ -4,7 +4,7 @@ import { GitActions } from '../commands'; import { getBranchesAndOrTags, getValidateGitReferenceFn, QuickCommandButtons } from '../commands/quickCommand'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { BranchSortOptions, GitBranch, GitReference, GitTag, TagSortOptions } from '../git/git'; +import { BranchSortOptions, GitBranch, GitReference, GitTag, TagSortOptions } from '../git/models'; import { KeyboardScope, Keys } from '../keyboard'; import { BranchQuickPickItem, getQuickPickIgnoreFocusOut, RefQuickPickItem, TagQuickPickItem } from '../quickpicks'; diff --git a/src/quickpicks/remoteProviderPicker.ts b/src/quickpicks/remoteProviderPicker.ts index 21d184c..a997488 100644 --- a/src/quickpicks/remoteProviderPicker.ts +++ b/src/quickpicks/remoteProviderPicker.ts @@ -3,14 +3,8 @@ import { Disposable, env, QuickInputButton, ThemeIcon, Uri, window } from 'vscod import { Commands, OpenOnRemoteCommandArgs } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { - getNameFromRemoteResource, - GitBranch, - GitRemote, - RemoteProvider, - RemoteResource, - RemoteResourceType, -} from '../git/git'; +import { GitBranch, GitRemote } from '../git/models'; +import { getNameFromRemoteResource, RemoteProvider, RemoteResource, RemoteResourceType } from '../git/remotes/provider'; import { Keys } from '../keyboard'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickpicks'; diff --git a/src/quickpicks/repositoryPicker.ts b/src/quickpicks/repositoryPicker.ts index 65c6708..903c681 100644 --- a/src/quickpicks/repositoryPicker.ts +++ b/src/quickpicks/repositoryPicker.ts @@ -1,7 +1,7 @@ 'use strict'; import { Disposable, window } from 'vscode'; import { Container } from '../container'; -import { Repository } from '../git/git'; +import { Repository } from '../git/models'; import { getQuickPickIgnoreFocusOut, RepositoryQuickPickItem } from '../quickpicks'; import { Iterables } from '../system'; diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index 47e9c44..9cd202a 100644 --- a/src/statusbar/statusBarController.ts +++ b/src/statusbar/statusBarController.ts @@ -15,7 +15,8 @@ import { command, Commands, ToggleFileChangesAnnotationCommandArgs } from '../co import { configuration, FileAnnotationType, StatusBarCommand } from '../configuration'; import { GlyphChars, isTextEditor } from '../constants'; import { Container } from '../container'; -import { CommitFormatter, GitBlameCommit, PullRequest } from '../git/git'; +import { CommitFormatter } from '../git/formatters'; +import { GitBlameCommit, PullRequest } from '../git/models'; import { Hovers } from '../hovers/hovers'; import { LogCorrelationContext, Logger } from '../logger'; import { debug, Promises } from '../system'; diff --git a/src/system/date.ts b/src/system/date.ts index 9cccfe0..3a71f0e 100644 --- a/src/system/date.ts +++ b/src/system/date.ts @@ -4,8 +4,8 @@ import * as advancedFormat from 'dayjs/plugin/advancedFormat'; import * as relativeTime from 'dayjs/plugin/relativeTime'; import * as updateLocale from 'dayjs/plugin/updateLocale'; -dayjs.extend(advancedFormat.default); -dayjs.extend(relativeTime.default, { +dayjs.default.extend(advancedFormat.default); +dayjs.default.extend(relativeTime.default, { thresholds: [ { l: 's', r: 44, d: 'second' }, { l: 'm', r: 89 }, @@ -22,9 +22,9 @@ dayjs.extend(relativeTime.default, { { l: 'yy', d: 'year' }, ], }); -dayjs.extend(updateLocale.default); +dayjs.default.extend(updateLocale.default); -dayjs.updateLocale('en', { +dayjs.default.updateLocale('en', { relativeTime: { future: 'in %s', past: '%s ago', @@ -101,7 +101,7 @@ const shortLocale = { }, }; -dayjs.locale('en-short', shortLocale, true); +dayjs.default.locale('en-short', shortLocale, true); export const MillisecondsPerMinute = 60000; // 60 * 1000 export const MillisecondsPerHour = 3600000; // 60 * 60 * 1000 diff --git a/src/system/string.ts b/src/system/string.ts index 669f081..98b7e23 100644 --- a/src/system/string.ts +++ b/src/system/string.ts @@ -1,7 +1,7 @@ 'use strict'; -import { BinaryToTextEncoding, createHash } from 'crypto'; import ansiRegex from 'ansi-regex'; -import { isWindows } from '../git/shell'; +import { md5 as _md5 } from '@env/crypto'; +import { isWindows } from '@env/platform'; export { fromBase64, base64 } from '@env/base64'; @@ -281,8 +281,8 @@ export function* lines(s: string, char: string = '\n'): IterableIterator } } -export function md5(s: string, encoding: BinaryToTextEncoding = 'base64'): string { - return createHash('md5').update(s).digest(encoding); +export function md5(s: string, encoding: 'base64' | 'hex' = 'base64'): string { + return _md5(s, encoding); } export function normalizePath(fileName: string, options?: { addLeadingSlash?: boolean; stripTrailingSlash?: boolean }) { @@ -379,10 +379,6 @@ export function sanitizeForFileSystem(s: string, replacement: string = '_') { return s.replace(illegalCharsForFSRegex, replacement); } -export function sha1(s: string, encoding: BinaryToTextEncoding = 'base64'): string { - return createHash('sha1').update(s).digest(encoding); -} - export function splitLast(s: string, splitter: string) { const index = s.lastIndexOf(splitter); if (index === -1) return [s]; diff --git a/src/terminal/linkProvider.ts b/src/terminal/linkProvider.ts index 799481d..2c2ef2e 100644 --- a/src/terminal/linkProvider.ts +++ b/src/terminal/linkProvider.ts @@ -7,8 +7,8 @@ import { ShowQuickCommitCommandArgs, } from '../commands'; import { Container } from '../container'; -import { GitBranch, GitReference, GitTag } from '../git/git'; import { PagedResult } from '../git/gitProvider'; +import { GitBranch, GitReference, GitTag } from '../git/models'; const commandsRegexShared = /\b(g(?:it)?\b\s*)\b(branch|checkout|cherry-pick|fetch|grep|log|merge|pull|push|rebase|reset|revert|show|stash|status|tag)\b/gi; diff --git a/src/trackers/documentTracker.ts b/src/trackers/documentTracker.ts index df1b82a..c0c5b89 100644 --- a/src/trackers/documentTracker.ts +++ b/src/trackers/documentTracker.ts @@ -19,9 +19,9 @@ import { import { configuration } from '../configuration'; import { ContextKeys, DocumentSchemes, isActiveDocument, isTextEditor, setContext } from '../constants'; import { Container } from '../container'; -import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../git/git'; import { RepositoriesChangeEvent } from '../git/gitProviderService'; import { GitUri } from '../git/gitUri'; +import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../git/models'; import { Functions, Iterables } from '../system'; import { DocumentBlameStateChangeEvent, TrackedDocument } from './trackedDocument'; diff --git a/src/trackers/gitDocumentTracker.ts b/src/trackers/gitDocumentTracker.ts index beeb20c..249a61d 100644 --- a/src/trackers/gitDocumentTracker.ts +++ b/src/trackers/gitDocumentTracker.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitBlame, GitDiff, GitLog } from '../git/git'; +import { GitBlame, GitDiff, GitLog } from '../git/models'; import { DocumentTracker } from './documentTracker'; export * from './documentTracker'; diff --git a/src/trackers/gitLineTracker.ts b/src/trackers/gitLineTracker.ts index 3ce0474..2209319 100644 --- a/src/trackers/gitLineTracker.ts +++ b/src/trackers/gitLineTracker.ts @@ -2,7 +2,7 @@ import { Disposable, TextEditor } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBlameCommit, GitLogCommit } from '../git/git'; +import { GitBlameCommit, GitLogCommit } from '../git/models'; import { Logger } from '../logger'; import { debug } from '../system'; import { diff --git a/src/trackers/trackedDocument.ts b/src/trackers/trackedDocument.ts index fccdde0..f741c9b 100644 --- a/src/trackers/trackedDocument.ts +++ b/src/trackers/trackedDocument.ts @@ -2,8 +2,8 @@ import { Disposable, Event, EventEmitter, TextDocument, TextEditor } from 'vscode'; import { ContextKeys, getEditorIfActive, isActiveDocument, setContext } from '../constants'; import { Container } from '../container'; -import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitRevision } from '../git/models'; import { Logger } from '../logger'; export interface DocumentBlameStateChangeEvent { diff --git a/src/views/branchesView.ts b/src/views/branchesView.ts index f6acbc4..e8341d9 100644 --- a/src/views/branchesView.ts +++ b/src/views/branchesView.ts @@ -18,6 +18,7 @@ import { } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; +import { GitUri } from '../git/gitUri'; import { GitBranchReference, GitLogCommit, @@ -26,8 +27,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { gate, Strings } from '../system'; import { BranchesNode, diff --git a/src/views/commitsView.ts b/src/views/commitsView.ts index 5f53077..30e26b9 100644 --- a/src/views/commitsView.ts +++ b/src/views/commitsView.ts @@ -12,6 +12,7 @@ import { import { CommitsViewConfig, configuration, ViewFilesLayout, ViewShowBranchComparison } from '../configuration'; import { ContextKeys, GlyphChars, setContext } from '../constants'; import { Container } from '../container'; +import { GitUri } from '../git/gitUri'; import { GitLogCommit, GitReference, @@ -20,8 +21,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { debug, Functions, gate, Strings } from '../system'; import { BranchNode, diff --git a/src/views/contributorsView.ts b/src/views/contributorsView.ts index bd5df7c..e0e82b5 100644 --- a/src/views/contributorsView.ts +++ b/src/views/contributorsView.ts @@ -13,8 +13,8 @@ import { Avatars } from '../avatars'; import { configuration, ContributorsViewConfig, ViewFilesLayout } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitContributor, RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitContributor, RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../git/models'; import { debug, gate, Strings } from '../system'; import { ContributorNode, diff --git a/src/views/nodes/autolinkedItemNode.ts b/src/views/nodes/autolinkedItemNode.ts index 7243cee..d6e747e 100644 --- a/src/views/nodes/autolinkedItemNode.ts +++ b/src/views/nodes/autolinkedItemNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { MarkdownString, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitFile, IssueOrPullRequest, IssueOrPullRequestType } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitFile, IssueOrPullRequest, IssueOrPullRequestType } from '../../git/models'; import { Dates } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { ContextValues, ViewNode } from './viewNode'; diff --git a/src/views/nodes/autolinkedItemsNode.ts b/src/views/nodes/autolinkedItemsNode.ts index 4d278d4..8ff750d 100644 --- a/src/views/nodes/autolinkedItemsNode.ts +++ b/src/views/nodes/autolinkedItemsNode.ts @@ -1,7 +1,8 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitFile, GitLog, GitRemote, IssueOrPullRequest, PullRequest, RichRemoteProvider } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitFile, GitLog, GitRemote, IssueOrPullRequest, PullRequest } from '../../git/models'; +import { RichRemoteProvider } from '../../git/remotes/provider'; import { debug, gate, Promises } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { AutolinkedItemNode } from './autolinkedItemNode'; diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index cf3a522..21436ce 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -2,6 +2,7 @@ import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, window } from 'vscode'; import { ViewBranchesLayout, ViewShowBranchComparison } from '../../configuration'; import { Colors, GlyphChars } from '../../constants'; +import { GitUri } from '../../git/gitUri'; import { BranchDateFormatting, GitBranch, @@ -10,8 +11,7 @@ import { GitRemote, GitRemoteType, PullRequestState, -} from '../../git/git'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { debug, gate, Iterables, log, Strings } from '../../system'; import { BranchesView } from '../branchesView'; import { CommitsView } from '../commitsView'; diff --git a/src/views/nodes/branchTrackingStatusFilesNode.ts b/src/views/nodes/branchTrackingStatusFilesNode.ts index 46d79bf..30a0744 100644 --- a/src/views/nodes/branchTrackingStatusFilesNode.ts +++ b/src/views/nodes/branchTrackingStatusFilesNode.ts @@ -2,8 +2,8 @@ import * as paths from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../configuration'; -import { GitBranch, GitFileWithCommit, GitRevision } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitFileWithCommit, GitRevision } from '../../git/models'; import { Arrays, Iterables, Strings } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { BranchNode } from './branchNode'; diff --git a/src/views/nodes/branchTrackingStatusNode.ts b/src/views/nodes/branchTrackingStatusNode.ts index 125f574..c8f96bf 100644 --- a/src/views/nodes/branchTrackingStatusNode.ts +++ b/src/views/nodes/branchTrackingStatusNode.ts @@ -1,8 +1,8 @@ 'use strict'; import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { Colors } from '../../constants'; -import { GitBranch, GitLog, GitRemote, GitRevision, GitTrackingState } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitLog, GitRemote, GitRevision, GitTrackingState } from '../../git/models'; import { Dates, debug, gate, Iterables, Strings } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { BranchNode } from './branchNode'; diff --git a/src/views/nodes/branchesNode.ts b/src/views/nodes/branchesNode.ts index a577db6..7a4eb20 100644 --- a/src/views/nodes/branchesNode.ts +++ b/src/views/nodes/branchesNode.ts @@ -1,8 +1,8 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; -import { Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { Repository } from '../../git/models'; import { Arrays, debug, gate } from '../../system'; import { BranchesView } from '../branchesView'; import { RepositoriesView } from '../repositoriesView'; diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index 37738d9..7f16b9a 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -2,8 +2,9 @@ import * as paths from 'path'; import { Command, Selection, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; -import { GitBranch, GitFile, GitLogCommit, GitRevisionReference, StatusFileFormatter } from '../../git/git'; +import { StatusFileFormatter } from '../../git/formatters'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitFile, GitLogCommit, GitRevisionReference } from '../../git/models'; import { FileHistoryView } from '../fileHistoryView'; import { View, ViewsWithCommits } from '../viewBase'; import { ContextValues, ViewNode, ViewRefFileNode } from './viewNode'; diff --git a/src/views/nodes/commitNode.ts b/src/views/nodes/commitNode.ts index e16b7a9..565caf1 100644 --- a/src/views/nodes/commitNode.ts +++ b/src/views/nodes/commitNode.ts @@ -4,7 +4,8 @@ import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemColla import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; import { ViewFilesLayout } from '../../configuration'; import { Colors, GlyphChars } from '../../constants'; -import { CommitFormatter, GitBranch, GitLogCommit, GitRevisionReference } from '../../git/git'; +import { CommitFormatter } from '../../git/formatters'; +import { GitBranch, GitLogCommit, GitRevisionReference } from '../../git/models'; import { Arrays, Strings } from '../../system'; import { FileHistoryView } from '../fileHistoryView'; import { TagsView } from '../tagsView'; diff --git a/src/views/nodes/compareBranchNode.ts b/src/views/nodes/compareBranchNode.ts index 74e55a2..fa0b128 100644 --- a/src/views/nodes/compareBranchNode.ts +++ b/src/views/nodes/compareBranchNode.ts @@ -2,8 +2,8 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewShowBranchComparison } from '../../configuration'; import { BranchComparison, BranchComparisons, GlyphChars, WorkspaceState } from '../../constants'; -import { GitBranch, GitRevision } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitRevision } from '../../git/models'; import { CommandQuickPickItem, ReferencePicker } from '../../quickpicks'; import { debug, gate, log, Strings } from '../../system'; import { BranchesView } from '../branchesView'; diff --git a/src/views/nodes/compareResultsNode.ts b/src/views/nodes/compareResultsNode.ts index b479102..e16ef95 100644 --- a/src/views/nodes/compareResultsNode.ts +++ b/src/views/nodes/compareResultsNode.ts @@ -1,8 +1,8 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { NamedRef } from '../../constants'; -import { GitRevision } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitRevision } from '../../git/models'; import { debug, gate, log, Strings } from '../../system'; import { SearchAndCompareView } from '../searchAndCompareView'; import { RepositoryNode } from './repositoryNode'; @@ -19,7 +19,7 @@ export class CompareResultsNode extends ViewNode { } static getPinnableId(repoPath: string, ref1: string, ref2: string) { - return Strings.sha1(`${repoPath}|${ref1}|${ref2}`); + return Strings.md5(`${repoPath}|${ref1}|${ref2}`); } private _children: ViewNode[] | undefined; diff --git a/src/views/nodes/contributorNode.ts b/src/views/nodes/contributorNode.ts index 0b2e8c3..3879542 100644 --- a/src/views/nodes/contributorNode.ts +++ b/src/views/nodes/contributorNode.ts @@ -2,8 +2,8 @@ import { MarkdownString, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { getPresenceDataUri } from '../../avatars'; import { GlyphChars } from '../../constants'; -import { GitContributor, GitLog } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitContributor, GitLog } from '../../git/models'; import { debug, gate, Iterables, Strings } from '../../system'; import { ContactPresence } from '../../vsls/vsls'; import { ContributorsView } from '../contributorsView'; diff --git a/src/views/nodes/contributorsNode.ts b/src/views/nodes/contributorsNode.ts index 6cc49ec..7710725 100644 --- a/src/views/nodes/contributorsNode.ts +++ b/src/views/nodes/contributorsNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitContributor, Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitContributor, Repository } from '../../git/models'; import { debug, gate, timeout } from '../../system'; import { ContributorsView } from '../contributorsView'; import { RepositoriesView } from '../repositoriesView'; diff --git a/src/views/nodes/fileHistoryNode.ts b/src/views/nodes/fileHistoryNode.ts index dd98bdd..6d9d261 100644 --- a/src/views/nodes/fileHistoryNode.ts +++ b/src/views/nodes/fileHistoryNode.ts @@ -2,6 +2,7 @@ import * as paths from 'path'; import { Disposable, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { configuration } from '../../configuration'; +import { GitUri } from '../../git/gitUri'; import { GitBranch, GitLog, @@ -10,9 +11,7 @@ import { RepositoryChangeComparisonMode, RepositoryChangeEvent, RepositoryFileSystemChangeEvent, - toFolderGlob, -} from '../../git/git'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { Logger } from '../../logger'; import { Arrays, debug, gate, Iterables, memoize } from '../../system'; import { FileHistoryView } from '../fileHistoryView'; @@ -243,7 +242,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl @memoize() private getPathOrGlob() { - return this.folder ? toFolderGlob(this.uri.fsPath) : this.uri.fsPath; + return this.folder ? paths.join(this.uri.fsPath, '*') : this.uri.fsPath; } get hasMore() { diff --git a/src/views/nodes/fileHistoryTrackerNode.ts b/src/views/nodes/fileHistoryTrackerNode.ts index 309ca7b..040ad4e 100644 --- a/src/views/nodes/fileHistoryTrackerNode.ts +++ b/src/views/nodes/fileHistoryTrackerNode.ts @@ -2,8 +2,8 @@ import { Disposable, FileType, TextEditor, TreeItem, TreeItemCollapsibleState, window, workspace } from 'vscode'; import { UriComparer } from '../../comparers'; import { ContextKeys, setContext } from '../../constants'; -import { GitReference, GitRevision } from '../../git/git'; 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'; diff --git a/src/views/nodes/fileRevisionAsCommitNode.ts b/src/views/nodes/fileRevisionAsCommitNode.ts index ff2601a..afd49c9 100644 --- a/src/views/nodes/fileRevisionAsCommitNode.ts +++ b/src/views/nodes/fileRevisionAsCommitNode.ts @@ -12,15 +12,9 @@ import { } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; import { Colors, GlyphChars } from '../../constants'; -import { - CommitFormatter, - GitBranch, - GitFile, - GitLogCommit, - GitRevisionReference, - StatusFileFormatter, -} from '../../git/git'; +import { CommitFormatter, StatusFileFormatter } from '../../git/formatters'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitFile, GitLogCommit, GitRevisionReference } from '../../git/models'; import { FileHistoryView } from '../fileHistoryView'; import { LineHistoryView } from '../lineHistoryView'; import { ViewsWithCommits } from '../viewBase'; diff --git a/src/views/nodes/helpers.ts b/src/views/nodes/helpers.ts index 35d9e98..46bc907 100644 --- a/src/views/nodes/helpers.ts +++ b/src/views/nodes/helpers.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitLogCommit } from '../../git/git'; +import { GitLogCommit } from '../../git/models'; import { MessageNode } from './common'; import { ContextValues, ViewNode } from './viewNode'; diff --git a/src/views/nodes/lineHistoryNode.ts b/src/views/nodes/lineHistoryNode.ts index 64c5c37..0f8d123 100644 --- a/src/views/nodes/lineHistoryNode.ts +++ b/src/views/nodes/lineHistoryNode.ts @@ -1,5 +1,6 @@ 'use strict'; import { Disposable, Selection, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; +import { GitUri } from '../../git/gitUri'; import { GitBranch, GitCommitType, @@ -12,8 +13,7 @@ import { RepositoryChangeComparisonMode, RepositoryChangeEvent, RepositoryFileSystemChangeEvent, -} from '../../git/git'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { Logger } from '../../logger'; import { debug, gate, Iterables, memoize } from '../../system'; import { FileHistoryView } from '../fileHistoryView'; diff --git a/src/views/nodes/lineHistoryTrackerNode.ts b/src/views/nodes/lineHistoryTrackerNode.ts index 66a0a61..976b960 100644 --- a/src/views/nodes/lineHistoryTrackerNode.ts +++ b/src/views/nodes/lineHistoryTrackerNode.ts @@ -2,8 +2,8 @@ import { Selection, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { UriComparer } from '../../comparers'; import { ContextKeys, setContext } from '../../constants'; -import { GitReference, GitRevision } from '../../git/git'; 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'; diff --git a/src/views/nodes/mergeConflictCurrentChangesNode.ts b/src/views/nodes/mergeConflictCurrentChangesNode.ts index 834378a..a9beed1 100644 --- a/src/views/nodes/mergeConflictCurrentChangesNode.ts +++ b/src/views/nodes/mergeConflictCurrentChangesNode.ts @@ -2,8 +2,9 @@ import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs } from '../../commands'; import { BuiltInCommands, GlyphChars } from '../../constants'; -import { CommitFormatter, GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/git'; +import { CommitFormatter } from '../../git/formatters'; import { GitUri } from '../../git/gitUri'; +import { GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/models'; import { FileHistoryView } from '../fileHistoryView'; import { LineHistoryView } from '../lineHistoryView'; import { ViewsWithCommits } from '../viewBase'; diff --git a/src/views/nodes/mergeConflictFileNode.ts b/src/views/nodes/mergeConflictFileNode.ts index 39ad520..fabda2f 100644 --- a/src/views/nodes/mergeConflictFileNode.ts +++ b/src/views/nodes/mergeConflictFileNode.ts @@ -2,8 +2,9 @@ import * as paths from 'path'; import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { BuiltInCommands } from '../../constants'; -import { GitFile, GitMergeStatus, GitRebaseStatus, StatusFileFormatter } from '../../git/git'; +import { StatusFileFormatter } from '../../git/formatters'; import { GitUri } from '../../git/gitUri'; +import { GitFile, GitMergeStatus, GitRebaseStatus } from '../../git/models'; import { ViewsWithCommits } from '../viewBase'; import { FileNode } from './folderNode'; import { MergeConflictCurrentChangesNode } from './mergeConflictCurrentChangesNode'; diff --git a/src/views/nodes/mergeConflictIncomingChangesNode.ts b/src/views/nodes/mergeConflictIncomingChangesNode.ts index 7c386a2..fbd2cfc 100644 --- a/src/views/nodes/mergeConflictIncomingChangesNode.ts +++ b/src/views/nodes/mergeConflictIncomingChangesNode.ts @@ -2,8 +2,9 @@ import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs } from '../../commands'; import { BuiltInCommands, GlyphChars } from '../../constants'; -import { CommitFormatter, GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/git'; +import { CommitFormatter } from '../../git/formatters'; import { GitUri } from '../../git/gitUri'; +import { GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/models'; import { FileHistoryView } from '../fileHistoryView'; import { LineHistoryView } from '../lineHistoryView'; import { ViewsWithCommits } from '../viewBase'; diff --git a/src/views/nodes/mergeStatusNode.ts b/src/views/nodes/mergeStatusNode.ts index 614b76a..f67dbf4 100644 --- a/src/views/nodes/mergeStatusNode.ts +++ b/src/views/nodes/mergeStatusNode.ts @@ -2,8 +2,8 @@ import * as paths from 'path'; import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../configuration'; -import { GitBranch, GitMergeStatus, GitReference, GitStatus } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitMergeStatus, GitReference, GitStatus } from '../../git/models'; import { Arrays, Strings } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { BranchNode } from './branchNode'; diff --git a/src/views/nodes/pullRequestNode.ts b/src/views/nodes/pullRequestNode.ts index 79fc63a..a99776f 100644 --- a/src/views/nodes/pullRequestNode.ts +++ b/src/views/nodes/pullRequestNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { MarkdownString, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitBranch, GitCommit, PullRequest, PullRequestState } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitBranch, GitCommit, PullRequest, PullRequestState } from '../../git/models'; import { ViewsWithCommits } from '../viewBase'; import { RepositoryNode } from './repositoryNode'; import { ContextValues, ViewNode } from './viewNode'; diff --git a/src/views/nodes/rebaseStatusNode.ts b/src/views/nodes/rebaseStatusNode.ts index 23dacf0..b550109 100644 --- a/src/views/nodes/rebaseStatusNode.ts +++ b/src/views/nodes/rebaseStatusNode.ts @@ -13,16 +13,16 @@ import { import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; import { ViewFilesLayout } from '../../configuration'; import { BuiltInCommands, GlyphChars } from '../../constants'; +import { CommitFormatter } from '../../git/formatters'; +import { GitUri } from '../../git/gitUri'; import { - CommitFormatter, GitBranch, GitLogCommit, GitRebaseStatus, GitReference, GitRevisionReference, GitStatus, -} from '../../git/git'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { Arrays, Strings } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { BranchNode } from './branchNode'; diff --git a/src/views/nodes/reflogNode.ts b/src/views/nodes/reflogNode.ts index 7e2c3ff..f071230 100644 --- a/src/views/nodes/reflogNode.ts +++ b/src/views/nodes/reflogNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitReflog, Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitReflog, Repository } from '../../git/models'; import { debug, gate } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { LoadMoreNode, MessageNode } from './common'; diff --git a/src/views/nodes/reflogRecordNode.ts b/src/views/nodes/reflogRecordNode.ts index e5f2028..7038ab2 100644 --- a/src/views/nodes/reflogRecordNode.ts +++ b/src/views/nodes/reflogRecordNode.ts @@ -1,8 +1,8 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { GlyphChars } from '../../constants'; -import { GitLog, GitReflogRecord } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitLog, GitReflogRecord } from '../../git/models'; import { debug, gate, Iterables } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { CommitNode } from './commitNode'; diff --git a/src/views/nodes/remoteNode.ts b/src/views/nodes/remoteNode.ts index 981aba8..192fe34 100644 --- a/src/views/nodes/remoteNode.ts +++ b/src/views/nodes/remoteNode.ts @@ -2,8 +2,8 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; -import { GitRemote, GitRemoteType, Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitRemote, GitRemoteType, Repository } from '../../git/models'; import { Arrays, log } from '../../system'; import { RemotesView } from '../remotesView'; import { RepositoriesView } from '../repositoriesView'; diff --git a/src/views/nodes/remotesNode.ts b/src/views/nodes/remotesNode.ts index 4fb700b..bcc5146 100644 --- a/src/views/nodes/remotesNode.ts +++ b/src/views/nodes/remotesNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { Repository } from '../../git/models'; import { debug, gate } from '../../system'; import { RemotesView } from '../remotesView'; import { RepositoriesView } from '../repositoriesView'; diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index f902684..26a158e 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -1,6 +1,7 @@ 'use strict'; import { Disposable, MarkdownString, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GlyphChars } from '../../constants'; +import { GitUri } from '../../git/gitUri'; import { GitBranch, GitRemote, @@ -10,8 +11,7 @@ import { RepositoryChangeComparisonMode, RepositoryChangeEvent, RepositoryFileSystemChangeEvent, -} from '../../git/git'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { Arrays, debug, Functions, gate, log, Strings } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { BranchesNode } from './branchesNode'; diff --git a/src/views/nodes/resultsCommitsNode.ts b/src/views/nodes/resultsCommitsNode.ts index 289c45f..69c64a0 100644 --- a/src/views/nodes/resultsCommitsNode.ts +++ b/src/views/nodes/resultsCommitsNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitLog } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitLog } from '../../git/models'; import { debug, gate, Iterables, Promises } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { AutolinkedItemsNode } from './autolinkedItemsNode'; diff --git a/src/views/nodes/resultsFileNode.ts b/src/views/nodes/resultsFileNode.ts index ca4b246..afc486f 100644 --- a/src/views/nodes/resultsFileNode.ts +++ b/src/views/nodes/resultsFileNode.ts @@ -2,8 +2,9 @@ import * as paths from 'path'; import { Command, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs } from '../../commands'; -import { GitFile, GitReference, GitRevisionReference, StatusFileFormatter } from '../../git/git'; +import { StatusFileFormatter } from '../../git/formatters'; import { GitUri } from '../../git/gitUri'; +import { GitFile, GitReference, GitRevisionReference } from '../../git/models'; import { View } from '../viewBase'; import { FileNode } from './folderNode'; import { ContextValues, ViewNode, ViewRefFileNode } from './viewNode'; diff --git a/src/views/nodes/resultsFilesNode.ts b/src/views/nodes/resultsFilesNode.ts index d5bc8cd..b5dc38b 100644 --- a/src/views/nodes/resultsFilesNode.ts +++ b/src/views/nodes/resultsFilesNode.ts @@ -2,8 +2,8 @@ import * as paths from 'path'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../configuration'; -import { GitFile } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitFile } from '../../git/models'; import { Arrays, debug, gate, Iterables, Promises, Strings } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { FileNode, FolderNode } from './folderNode'; diff --git a/src/views/nodes/searchResultsNode.ts b/src/views/nodes/searchResultsNode.ts index d72c0ce..67d6604 100644 --- a/src/views/nodes/searchResultsNode.ts +++ b/src/views/nodes/searchResultsNode.ts @@ -1,8 +1,9 @@ 'use strict'; import { ThemeIcon, TreeItem } from 'vscode'; import { executeGitCommand } from '../../commands'; -import { GitLog, SearchPattern } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitLog } from '../../git/models'; +import { SearchPattern } from '../../git/search'; import { debug, gate, log, Strings } from '../../system'; import { SearchAndCompareView } from '../searchAndCompareView'; import { RepositoryNode } from './repositoryNode'; @@ -27,7 +28,7 @@ export class SearchResultsNode extends ViewNode implements } static getPinnableId(repoPath: string, search: SearchPattern) { - return Strings.sha1(`${repoPath}|${SearchPattern.toKey(search)}`); + return Strings.md5(`${repoPath}|${SearchPattern.toKey(search)}`); } static override is(node: any): node is SearchResultsNode { diff --git a/src/views/nodes/stashFileNode.ts b/src/views/nodes/stashFileNode.ts index 8f9a58d..4d0e54f 100644 --- a/src/views/nodes/stashFileNode.ts +++ b/src/views/nodes/stashFileNode.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitFile, GitLogCommit } from '../../git/git'; +import { GitFile, GitLogCommit } from '../../git/models'; import { RepositoriesView } from '../repositoriesView'; import { StashesView } from '../stashesView'; import { CommitFileNode } from './commitFileNode'; diff --git a/src/views/nodes/stashNode.ts b/src/views/nodes/stashNode.ts index 33db651..fcf761a 100644 --- a/src/views/nodes/stashNode.ts +++ b/src/views/nodes/stashNode.ts @@ -2,7 +2,8 @@ import * as paths from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../config'; -import { CommitFormatter, GitStashCommit, GitStashReference } from '../../git/git'; +import { CommitFormatter } from '../../git/formatters'; +import { GitStashCommit, GitStashReference } from '../../git/models'; import { Arrays, Strings } from '../../system'; import { ContextValues, FileNode, FolderNode, RepositoryNode, StashFileNode, ViewNode, ViewRefNode } from '../nodes'; import { RepositoriesView } from '../repositoriesView'; diff --git a/src/views/nodes/stashesNode.ts b/src/views/nodes/stashesNode.ts index 667a463..7b4db74 100644 --- a/src/views/nodes/stashesNode.ts +++ b/src/views/nodes/stashesNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { Repository } from '../../git/models'; import { debug, gate, Iterables } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { StashesView } from '../stashesView'; diff --git a/src/views/nodes/statusFileNode.ts b/src/views/nodes/statusFileNode.ts index 16a8c83..9646ecd 100644 --- a/src/views/nodes/statusFileNode.ts +++ b/src/views/nodes/statusFileNode.ts @@ -2,8 +2,9 @@ import * as paths from 'path'; import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs, DiffWithPreviousCommandArgs } from '../../commands'; -import { GitFile, GitLogCommit, StatusFileFormatter } from '../../git/git'; +import { StatusFileFormatter } from '../../git/formatters/statusFormatter'; import { GitUri } from '../../git/gitUri'; +import { GitFile, GitLogCommit } from '../../git/models'; import { Strings } from '../../system'; import { ViewsWithCommits } from '../viewBase'; import { FileRevisionAsCommitNode } from './fileRevisionAsCommitNode'; diff --git a/src/views/nodes/statusFilesNode.ts b/src/views/nodes/statusFilesNode.ts index 01feee8..e1f98c5 100644 --- a/src/views/nodes/statusFilesNode.ts +++ b/src/views/nodes/statusFilesNode.ts @@ -2,6 +2,7 @@ import * as paths from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../configuration'; +import { GitUri } from '../../git/gitUri'; import { GitCommitType, GitFileWithCommit, @@ -11,8 +12,7 @@ import { GitStatus, GitStatusFile, GitTrackingState, -} from '../../git/git'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { Arrays, Iterables, Strings } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { FileNode, FolderNode } from './folderNode'; diff --git a/src/views/nodes/tagNode.ts b/src/views/nodes/tagNode.ts index 0ca7d7a..aa95fe1 100644 --- a/src/views/nodes/tagNode.ts +++ b/src/views/nodes/tagNode.ts @@ -3,8 +3,8 @@ import { TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; import { emojify } from '../../emojis'; -import { GitLog, GitRevision, GitTag, GitTagReference, TagDateFormatting } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { GitLog, GitRevision, GitTag, GitTagReference, TagDateFormatting } from '../../git/models'; import { debug, gate, Iterables, Strings } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { TagsView } from '../tagsView'; diff --git a/src/views/nodes/tagsNode.ts b/src/views/nodes/tagsNode.ts index 3c9473c..95b4f8f 100644 --- a/src/views/nodes/tagsNode.ts +++ b/src/views/nodes/tagsNode.ts @@ -1,8 +1,8 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; -import { Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { Repository } from '../../git/models'; import { Arrays, debug, gate } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { TagsView } from '../tagsView'; diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index 86b6f1a..b9d3088 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -9,6 +9,8 @@ import { TreeViewVisibilityChangeEvent, } from 'vscode'; import { GlyphChars } from '../../constants'; +import { RepositoriesChangeEvent } from '../../git/gitProviderService'; +import { GitUri } from '../../git/gitUri'; import { GitFile, GitReference, @@ -18,9 +20,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, -} from '../../git/git'; -import { RepositoriesChangeEvent } from '../../git/gitProviderService'; -import { GitUri } from '../../git/gitUri'; +} from '../../git/models'; import { Logger } from '../../logger'; import { debug, Functions, gate, log, logName, Strings } from '../../system'; import { TreeViewNodeCollapsibleStateChangeEvent, View } from '../viewBase'; diff --git a/src/views/remotesView.ts b/src/views/remotesView.ts index 6b714cd..6b905c6 100644 --- a/src/views/remotesView.ts +++ b/src/views/remotesView.ts @@ -12,6 +12,7 @@ import { import { configuration, RemotesViewConfig, ViewBranchesLayout, ViewFilesLayout } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; +import { GitUri } from '../git/gitUri'; import { GitBranch, GitBranchReference, @@ -22,8 +23,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { gate, Strings } from '../system'; import { BranchNode, diff --git a/src/views/repositoriesView.ts b/src/views/repositoriesView.ts index 811b7cb..8d5948f 100644 --- a/src/views/repositoriesView.ts +++ b/src/views/repositoriesView.ts @@ -28,7 +28,7 @@ import { GitRevisionReference, GitStashReference, GitTagReference, -} from '../git/git'; +} from '../git/models'; import { gate } from '../system'; import { BranchesNode, @@ -647,7 +647,10 @@ export class RepositoriesView extends ViewBase { @@ -705,7 +708,10 @@ export class RepositoriesView extends ViewBase { diff --git a/src/views/searchAndCompareView.ts b/src/views/searchAndCompareView.ts index 3637c64..a533042 100644 --- a/src/views/searchAndCompareView.ts +++ b/src/views/searchAndCompareView.ts @@ -4,7 +4,8 @@ import { getRepoPathOrPrompt } from '../commands'; import { configuration, SearchAndCompareViewConfig, ViewFilesLayout } from '../configuration'; import { ContextKeys, NamedRef, PinnedItem, PinnedItems, setContext, WorkspaceState } from '../constants'; import { Container } from '../container'; -import { GitLog, GitRevision, SearchPattern } from '../git/git'; +import { GitLog, GitRevision } from '../git/models'; +import { SearchPattern } from '../git/search'; import { ReferencePicker, ReferencesQuickPickIncludes } from '../quickpicks'; import { debug, gate, Iterables, log, Promises } from '../system'; import { diff --git a/src/views/stashesView.ts b/src/views/stashesView.ts index 9613f52..c97bb13 100644 --- a/src/views/stashesView.ts +++ b/src/views/stashesView.ts @@ -12,14 +12,14 @@ import { import { configuration, StashesViewConfig, ViewFilesLayout } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; +import { GitUri } from '../git/gitUri'; import { GitReference, GitStashReference, RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { gate, Strings } from '../system'; import { RepositoriesSubscribeableNode, diff --git a/src/views/tagsView.ts b/src/views/tagsView.ts index b98b356..050756b 100644 --- a/src/views/tagsView.ts +++ b/src/views/tagsView.ts @@ -12,14 +12,14 @@ import { import { configuration, TagsViewConfig, ViewBranchesLayout, ViewFilesLayout } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; +import { GitUri } from '../git/gitUri'; import { GitReference, GitTagReference, RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, -} from '../git/git'; -import { GitUri } from '../git/gitUri'; +} from '../git/models'; import { gate, Strings } from '../system'; import { BranchOrTagFolderNode, diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index eda280a..317f48f 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -15,8 +15,8 @@ import { import { configuration, FileAnnotationType, ViewShowBranchComparison } from '../configuration'; import { BuiltInCommands, BuiltInGitCommands, ContextKeys, setContext } from '../constants'; import { Container } from '../container'; -import { GitReference, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; +import { GitReference, GitRevision } from '../git/models'; import { debug } from '../system'; import { runGitCommandInTerminal } from '../terminal'; import { diff --git a/src/views/viewDecorationProvider.ts b/src/views/viewDecorationProvider.ts index fa451e3..40fbedd 100644 --- a/src/views/viewDecorationProvider.ts +++ b/src/views/viewDecorationProvider.ts @@ -11,7 +11,7 @@ import { window, } from 'vscode'; import { GlyphChars } from '../constants'; -import { GitBranchStatus } from '../git/git'; +import { GitBranchStatus } from '../git/models'; export class ViewFileDecorationProvider implements FileDecorationProvider, Disposable { private readonly _onDidChange = new EventEmitter(); diff --git a/src/vsls/guest.ts b/src/vsls/guest.ts index fe0a25e..23a4af7 100644 --- a/src/vsls/guest.ts +++ b/src/vsls/guest.ts @@ -2,7 +2,8 @@ import { CancellationToken, Disposable, window, WorkspaceFolder } from 'vscode'; import type { LiveShare, SharedServiceProxy } from '../@types/vsls'; import { Container } from '../container'; -import { GitCommandOptions, Repository, RepositoryChangeEvent } from '../git/git'; +import { GitCommandOptions } from '../git/commandOptions'; +import { Repository, RepositoryChangeEvent } from '../git/models'; import { Logger } from '../logger'; import { debug, log } from '../system'; import { VslsHostService } from './host'; diff --git a/src/vsls/host.ts b/src/vsls/host.ts index 222df07..2c36067 100644 --- a/src/vsls/host.ts +++ b/src/vsls/host.ts @@ -1,8 +1,8 @@ 'use strict'; import { CancellationToken, Disposable, Uri, workspace, WorkspaceFoldersChangeEvent } from 'vscode'; +import { git } from '@env/git'; import type { LiveShare, SharedService } from '../@types/vsls'; import { Container } from '../container'; -import { git } from '../git/git'; import { GitUri } from '../git/gitUri'; import { Logger } from '../logger'; import { debug, Iterables, log, Strings } from '../system'; diff --git a/src/vsls/protocol.ts b/src/vsls/protocol.ts index 0e3e764..be97893 100644 --- a/src/vsls/protocol.ts +++ b/src/vsls/protocol.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { GitCommandOptions } from '../git/git'; +import { GitCommandOptions } from '../git/commandOptions'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export class RequestType { diff --git a/src/webviews/rebaseEditor.ts b/src/webviews/rebaseEditor.ts index b09b333..00e7f84 100644 --- a/src/webviews/rebaseEditor.ts +++ b/src/webviews/rebaseEditor.ts @@ -1,5 +1,4 @@ 'use strict'; -import { randomBytes } from 'crypto'; import { CancellationToken, commands, @@ -15,11 +14,12 @@ import { workspace, WorkspaceEdit, } from 'vscode'; +import { getNonce } from '@env/crypto'; import { ShowQuickCommitCommand } from '../commands'; import { configuration } from '../configuration'; import { BuiltInCommands } from '../constants'; import { Container } from '../container'; -import { RepositoryChange, RepositoryChangeComparisonMode } from '../git/git'; +import { RepositoryChange, RepositoryChangeComparisonMode } from '../git/models'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { debug, gate, Iterables, Strings } from '../system'; @@ -478,7 +478,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl const bootstrap = await this.parseState(context); const cspSource = context.panel.webview.cspSource; - const cspNonce = randomBytes(16).toString('base64'); + const cspNonce = getNonce(); const root = context.panel.webview.asWebviewUri(this.container.context.extensionUri).toString(); const html = content diff --git a/src/webviews/webviewBase.ts b/src/webviews/webviewBase.ts index f1cf7aa..17d087c 100644 --- a/src/webviews/webviewBase.ts +++ b/src/webviews/webviewBase.ts @@ -1,5 +1,4 @@ 'use strict'; -import { randomBytes } from 'crypto'; import { commands, ConfigurationChangeEvent, @@ -13,10 +12,12 @@ import { window, workspace, } from 'vscode'; +import { getNonce } from '@env/crypto'; import { Commands } from '../commands'; import { configuration } from '../configuration'; import { Container } from '../container'; -import { CommitFormatter, GitBlameCommit, PullRequest, PullRequestState } from '../git/git'; +import { CommitFormatter } from '../git/formatters'; +import { GitBlameCommit, PullRequest, PullRequestState } from '../git/models'; import { Logger } from '../logger'; import { DidChangeConfigurationNotificationType, @@ -332,7 +333,7 @@ export abstract class WebviewBase implements Disposable { ]); const cspSource = webview.cspSource; - const cspNonce = randomBytes(16).toString('base64'); + const cspNonce = getNonce(); const root = webview.asWebviewUri(this.container.context.extensionUri).toString(); const html = content diff --git a/tsconfig.browser.json b/tsconfig.browser.json index 7435b64..f041ac7 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -3,7 +3,8 @@ "compilerOptions": { "lib": ["dom", "dom.iterable", "es2020"], "paths": { - "@env/*": ["src/env/browser/*"] + "@env/*": ["src/env/browser/*"], + "path": ["node_modules/path-browserify"] }, "tsBuildInfoFile": "tsconfig.browser.tsbuildinfo" }, diff --git a/webpack.config.js b/webpack.config.js index 7e427c0..a646588 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -142,11 +142,6 @@ function getExtensionConfig(target, mode, env) { }, }), ], - splitChunks: { - cacheGroups: { - defaultVendors: false, - }, - }, }, externals: { vscode: 'commonjs vscode', @@ -187,34 +182,8 @@ function getExtensionConfig(target, mode, env) { ], }, resolve: { - alias: - target === 'webworker' - ? { - '@env': path.resolve(__dirname, 'src', 'env', 'browser'), - } - : { - '@env': path.resolve(__dirname, 'src', 'env', target), - // 'universal-user-agent': path.join( - // __dirname, - // 'node_modules', - // 'universal-user-agent', - // 'dist-node', - // 'index.js', - // ), - }, - fallback: - target === 'webworker' - ? { - child_process: false, - crypto: require.resolve('crypto-browserify'), - fs: false, - os: false, - path: require.resolve('path-browserify'), - process: false, - stream: false, - url: false, - } - : undefined, + alias: { '@env': path.resolve(__dirname, 'src', 'env', target === 'webworker' ? 'browser' : target) }, + fallback: target === 'webworker' ? { path: require.resolve('path-browserify') } : undefined, mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'], extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], }, diff --git a/yarn.lock b/yarn.lock index e62e43d..09fa33d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -216,9 +216,9 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*": - version "17.0.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.3.tgz#4b086e316ed4504f49bd78135d48642bf50aa135" - integrity sha512-bAKB1GcA28FR/D8HHQ5U4FYk7nvoZdp7TZSy9oIyQ8gpYCzpeESa3LCK2TbeocXic7GwIXCkCItJg0DttO3ZaQ== + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0" + integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw== "@types/node@14.17.4": version "14.17.4" @@ -484,9 +484,9 @@ acorn-walk@^8.0.0: integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== acorn@^8.0.4, acorn@^8.4.1, acorn@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" - integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== ajv-formats@^2.1.1: version "2.1.1" @@ -661,16 +661,6 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -769,16 +759,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== - boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -799,65 +779,6 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - browserslist@^4.14.5: version "4.19.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" @@ -897,11 +818,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -1047,14 +963,6 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - circular-dependency-plugin@5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz#39e836079db1d3cf2f988dc48c5188a44058b600" @@ -1197,37 +1105,6 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - cross-env@7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -1271,23 +1148,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - csp-html-webpack-plugin@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/csp-html-webpack-plugin/-/csp-html-webpack-plugin-5.1.0.tgz#b3bfa5a50d207fe5b6bb4839dc33aa59621a35a0" @@ -1311,9 +1171,9 @@ css-loader@6.5.1: semver "^7.3.5" css-select@^4.1.3: - version "4.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.0.tgz#ab28276d3afb00cc05e818bd33eb030f14f57895" - integrity sha512-6YVG6hsH9yIb/si3Th/is8Pex7qnVHO6t7q7U6TIUnkQASGbS8tnUDBftnPynLNnuUl/r2+PTd0ekiiq7R0zJw== + version "4.2.1" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" + integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== dependencies: boolbase "^1.0.0" css-what "^5.1.0" @@ -1505,28 +1365,11 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1639,22 +1482,9 @@ duplexer@^0.1.2: integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== electron-to-chromium@^1.4.17: - version "1.4.27" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.27.tgz#bfc6e798d8a56a17d658312f4b7ae1a7ca87724f" - integrity sha512-uZ95szi3zUbzRDx1zx/xnsCG+2xgZyy57pDOeaeO4r8zx5Dqe8Jv1ti8cunvBwJHVI5LzPuw8umKwZb3WKYxSQ== - -elliptic@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" + version "1.4.28" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.28.tgz#fef0e92e281df6d568f482d8d53c34ca5374de48" + integrity sha512-Gzbf0wUtKfyPaqf0Plz+Ctinf9eQIzxEqBHwSvbGfeOm9GMNdLxyu1dNiCUfM+x6r4BE0xUJNh3Nmg9gfAtTmg== emoji-regex@^8.0.0: version "8.0.0" @@ -1750,60 +1580,65 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild-android-arm64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.7.tgz#8c78cbb617f9f216abfb5a84cca453b51421a1b6" - integrity sha512-9/Q1NC4JErvsXzJKti0NHt+vzKjZOgPIjX/e6kkuCzgfT/GcO3FVBcGIv4HeJG7oMznE6KyKhvLrFgt7CdU2/w== - -esbuild-darwin-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.7.tgz#7424bdb64c104556d36b7429af79ab51415ab8f4" - integrity sha512-Z9X+3TT/Xj+JiZTVlwHj2P+8GoiSmUnGVz0YZTSt8WTbW3UKw5Pw2ucuJ8VzbD2FPy0jbIKJkko/6CMTQchShQ== - -esbuild-darwin-arm64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.7.tgz#6a243dc0132aeb11c1991f968a6a9e393f43c6bc" - integrity sha512-68e7COhmwIiLXBEyxUxZSSU0akgv8t3e50e2QOtKdBUE0F6KIRISzFntLe2rYlNqSsjGWsIO6CCc9tQxijjSkw== - -esbuild-freebsd-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.7.tgz#e7281e50522e724c4da502504dcd75be0db46c94" - integrity sha512-76zy5jAjPiXX/S3UvRgG85Bb0wy0zv/J2lel3KtHi4V7GUTBfhNUPt0E5bpSXJ6yMT7iThhnA5rOn+IJiUcslQ== - -esbuild-freebsd-arm64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.7.tgz#31e513098efd181d76a3ba3ea285836d37f018a3" - integrity sha512-lSlYNLiqyzd7qCN5CEOmLxn7MhnGHPcu5KuUYOG1i+t5A6q7LgBmfYC9ZHJBoYyow3u4CNu79AWHbvVLpE/VQQ== - -esbuild-linux-32@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.7.tgz#82cf96accbf55d3007c3338dc3b3144efa9ae108" - integrity sha512-Vk28u409wVOXqTaT6ek0TnfQG4Ty1aWWfiysIaIRERkNLhzLhUf4i+qJBN8mMuGTYOkE40F0Wkbp6m+IidOp2A== - -esbuild-linux-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.7.tgz#67bdfe23a6ca918a0bb8e9558a3ee0fdf98c0bc0" - integrity sha512-+Lvz6x+8OkRk3K2RtZwO+0a92jy9si9cUea5Zoru4yJ/6EQm9ENX5seZE0X9DTwk1dxJbjmLsJsd3IoowyzgVg== - -esbuild-linux-arm64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.7.tgz#f79c69ff0c176559c418de8e59aa3cf388fff992" - integrity sha512-kJd5beWSqteSAW086qzCEsH6uwpi7QRIpzYWHzEYwKKu9DiG1TwIBegQJmLpPsLp4v5RAFjea0JAmAtpGtRpqg== - -esbuild-linux-arm@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.7.tgz#3d665b35e1c27dbe1c9deb8bf956d7d1f191a21b" - integrity sha512-OzpXEBogbYdcBqE4uKynuSn5YSetCvK03Qv1HcOY1VN6HmReuatjJ21dCH+YPHSpMEF0afVCnNfffvsGEkxGJQ== - -esbuild-linux-mips64le@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.7.tgz#226114a0cc6649ba0ffd3428118a8f622872f16d" - integrity sha512-mFWpnDhZJmj/h7pxqn1GGDsKwRfqtV7fx6kTF5pr4PfXe8pIaTERpwcKkoCwZUkWAOmUEjMIUAvFM72A6hMZnA== - -esbuild-linux-ppc64le@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.7.tgz#5c67ae56517f2644d567b2ca5ecb97f9520cfc49" - integrity sha512-wM7f4M0bsQXfDL4JbbYD0wsr8cC8KaQ3RPWc/fV27KdErPW7YsqshZZSjDV0kbhzwpNNdhLItfbaRT8OE8OaKA== +esbuild-android-arm64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.8.tgz#69324e08ba68c7d9a541e7b825d7235b83e17bd6" + integrity sha512-tAEoSHnPBSH0cCAFa/aYs3LPsoTY4SwsP6wDKi4PaelbQYNJjqNpAeweyJ8l98g1D6ZkLyqsHbkYj+209sezkA== + +esbuild-darwin-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.8.tgz#7176b692b9de746ba2f9dd4dd81dc4f1b7670786" + integrity sha512-t7p7WzTb+ybiD/irkMt5j/NzB+jY+8yPTsrXk5zCOH1O7DdthRnAUJ7pJPwImdL7jAGRbLtYRxUPgCHs/0qUPw== + +esbuild-darwin-arm64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.8.tgz#59167584e58428877e48e05c4cca58755f843327" + integrity sha512-5FeaT2zMUajKnBwUMSsjZev5iA38YHrDmXhkOCwZQIFUvhqojinqCrvv/X7dyxb1987bcY9KGwJ+EwDwd922HQ== + +esbuild-freebsd-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.8.tgz#00b7d6e00abba9c2eccc9acd576c796333671e9c" + integrity sha512-pGHBLSf7ynfyDZXUtbq/GsA2VIwQlWXrUj1AMcE0id47mRdEUM8/1ZuqMGZx63hRnNgtK9zNJ8OIu2c7qq76Qw== + +esbuild-freebsd-arm64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.8.tgz#57f0cd5a1cb37fa2c0e84e780677fe62f1e8c894" + integrity sha512-g4GgAnrx6Gh1BjKJjJWgPnOR4tW2FcAx9wFvyUjRsIjB35gT+aAFR+P/zStu5OG9LnbS8Pvjd4wS68QIXk+2dA== + +esbuild-linux-32@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.8.tgz#bbf3e5d3fb30f949030d0c2241ac93a172917d56" + integrity sha512-wPfQJadF5vTzriw/B8Ide74PeAJlZW7czNx3NIUHkHlXb+En1SeIqNzl6jG9DuJUl57xD9Ucl9YJFEkFeX8eLg== + +esbuild-linux-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.8.tgz#08631e9e0da613603bcec782f29fecbc6f4596de" + integrity sha512-+RNuLk9RhRDL2kG+KTEYl5cIgF6AGLkRnKKWEu9DpCZaickONEqrKyQSVn410Hj105DLdW6qvIXQQHPycJhExg== + +esbuild-linux-arm64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.8.tgz#206d39c8dfbb7c72aa2f5fc52f7402b5b8a77366" + integrity sha512-BtWoKNYul9UoxUvQUSdSrvSmJyFL1sGnNPTSqWCg1wMe4kmc8UY2yVsXSSkKO8N2jtHxlgFyz/XhvNBzEwGVcw== + +esbuild-linux-arm@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.8.tgz#e28e70420d187f5e403bfa4a72df676d53d707fd" + integrity sha512-HIct38SvUAIJbiTwV/PVQroimQo96TGtzRDAEZxTorB4vsAj1r8bd0keXExPU4RH7G0zIqC4loQQpWYL+nH4Vg== + +esbuild-linux-mips64le@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.8.tgz#04997ac1a0df794a4d5e04d78015863d48490590" + integrity sha512-0DxnCl9XTvaQtsX6Qa+Phr5i9b04INwwSv2RbQ2UWRLoQ/037iaFzbmuhgrcmaGOcRwPkCa+4Qo5EgI01MUgsQ== + +esbuild-linux-ppc64le@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.8.tgz#1827378feff9702c156047ba118c1f3bd74da67e" + integrity sha512-Uzr/OMj97Q0qoWLXCvXCKUY/z1SNI4iSZEuYylM5Nd71HGStL32XWq/MReJ0PYMvUMKKJicKSKw2jWM1uBQ84Q== + +esbuild-linux-s390x@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.8.tgz#200ac44cda59b81135b325c3a29d016969650876" + integrity sha512-vURka7aCA5DrRoOqOn6pXYwFlDSoQ4qnqam8AC0Ikn6tibutuhgar6M3Ek2DCuz9yqd396mngdYr5A8x2TPkww== esbuild-loader@2.18.0: version "2.18.0" @@ -1817,58 +1652,59 @@ esbuild-loader@2.18.0: tapable "^2.2.0" webpack-sources "^2.2.0" -esbuild-netbsd-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.7.tgz#69dc0469ea089013956d8c6aa71c9e7fc25fc567" - integrity sha512-J/afS7woKyzGgAL5FlgvMyqgt5wQ597lgsT+xc2yJ9/7BIyezeXutXqfh05vszy2k3kSvhLesugsxIA71WsqBw== - -esbuild-openbsd-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.7.tgz#3a9d04ecf820708e2e5b7d26fa7332e3f19f6b6c" - integrity sha512-7CcxgdlCD+zAPyveKoznbgr3i0Wnh0L8BDGRCjE/5UGkm5P/NQko51tuIDaYof8zbmXjjl0OIt9lSo4W7I8mrw== - -esbuild-sunos-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.7.tgz#7c33a682f0fd9565cae7df165d0e8736b7b62623" - integrity sha512-GKCafP2j/KUljVC3nesw1wLFSZktb2FGCmoT1+730zIF5O6hNroo0bSEofm6ZK5mNPnLiSaiLyRB9YFgtkd5Xg== - -esbuild-windows-32@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.7.tgz#24ec706a5f25b4499048f56146bcff0ed3839dce" - integrity sha512-5I1GeL/gZoUUdTPA0ws54bpYdtyeA2t6MNISalsHpY269zK8Jia/AXB3ta/KcDHv2SvNwabpImeIPXC/k0YW6A== - -esbuild-windows-64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.7.tgz#dd6d5b5bace93cd7a9174d31fbd727ba21885abf" - integrity sha512-CIGKCFpQOSlYsLMbxt8JjxxvVw9MlF1Rz2ABLVfFyHUF5OeqHD5fPhGrCVNaVrhO8Xrm+yFmtjcZudUGr5/WYQ== - -esbuild-windows-arm64@0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.7.tgz#ecfd9ac289606f26760c4f737caaeeadfff3cfe3" - integrity sha512-eOs1eSivOqN7cFiRIukEruWhaCf75V0N8P0zP7dh44LIhLl8y6/z++vv9qQVbkBm5/D7M7LfCfCTmt1f1wHOCw== - -esbuild@0.14.7, esbuild@^0.14.6: - version "0.14.7" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.7.tgz#e85cead55b0e1001abf1b2ce4a11c1d4d709d13c" - integrity sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw== +esbuild-netbsd-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.8.tgz#8159d8eae111f80ea6e4cbfa5d4cf658388a72d4" + integrity sha512-tjyDak2/pp0VUAhBW6/ueuReMd5qLHNlisXl5pq0Xn0z+kH9urA/t1igm0JassWbdMz123td5ZEQWoD9KbtOAw== + +esbuild-openbsd-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.8.tgz#2a9498d881a3ab94927c724f34dd1160eef1f3b8" + integrity sha512-zAKKV15fIyAuDDga5rQv0lW2ufBWj/OCjqjDBb3dJf5SfoAi/DMIHuzmkKQeDQ+oxt9Rp1D7ZOlOBVflutFTqQ== + +esbuild-sunos-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.8.tgz#2447de7d79848ad528c7d44caab4938eb8f5a0cc" + integrity sha512-xV41Wa8imziM/2dbWZjLKQbIETRgo5dE0oc/uPsgaecJhsrdA0VkGa/V432LJSUYv967xHDQdoRRl5tr80+NnQ== + +esbuild-windows-32@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.8.tgz#3287281552d7e4c851b3106940ff5826f518043e" + integrity sha512-AxpdeLKQSyCZo7MzdOyV4OgEbEJcjnrS/2niAjbHESbjuS5P1DN/5vZoJ/JSWDVa/40OkBuHBhAXMx1HK3UDsg== + +esbuild-windows-64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.8.tgz#b4052868438b4f17b5c2a908cf344ed2bd267c38" + integrity sha512-/3pllNoy8mrz/E1rYalwiwwhzJBrYQhEapwAteHZbFVhGzYuB8F80e8x5eA8dhFHxDiZh1VzK+hREwwSt8UTQA== + +esbuild-windows-arm64@0.14.8: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.8.tgz#512d06097cb4b848526a37c48a47223f1c6cc667" + integrity sha512-lTm5naoNgaUvzIiax3XYIEebqwr3bIIEEtqUhzQ2UQ+JMBmvhr02w3sJIJqF3axTX6TgWrC1OtM7DYNvFG+aXA== + +esbuild@0.14.8, esbuild@^0.14.6: + version "0.14.8" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.8.tgz#f60a07ca9400d61d09a98f96d666c50613972550" + integrity sha512-stMsCBmxwaMpeK8GC/49L/cRGIwsHwoEN7Twk5zDTHlm/63c0KXFKzDC8iM2Mi3fyCKwS002TAH6IlAvqR6t3g== optionalDependencies: - esbuild-android-arm64 "0.14.7" - esbuild-darwin-64 "0.14.7" - esbuild-darwin-arm64 "0.14.7" - esbuild-freebsd-64 "0.14.7" - esbuild-freebsd-arm64 "0.14.7" - esbuild-linux-32 "0.14.7" - esbuild-linux-64 "0.14.7" - esbuild-linux-arm "0.14.7" - esbuild-linux-arm64 "0.14.7" - esbuild-linux-mips64le "0.14.7" - esbuild-linux-ppc64le "0.14.7" - esbuild-netbsd-64 "0.14.7" - esbuild-openbsd-64 "0.14.7" - esbuild-sunos-64 "0.14.7" - esbuild-windows-32 "0.14.7" - esbuild-windows-64 "0.14.7" - esbuild-windows-arm64 "0.14.7" + esbuild-android-arm64 "0.14.8" + esbuild-darwin-64 "0.14.8" + esbuild-darwin-arm64 "0.14.8" + esbuild-freebsd-64 "0.14.8" + esbuild-freebsd-arm64 "0.14.8" + esbuild-linux-32 "0.14.8" + esbuild-linux-64 "0.14.8" + esbuild-linux-arm "0.14.8" + esbuild-linux-arm64 "0.14.8" + esbuild-linux-mips64le "0.14.8" + esbuild-linux-ppc64le "0.14.8" + esbuild-linux-s390x "0.14.8" + esbuild-netbsd-64 "0.14.8" + esbuild-openbsd-64 "0.14.8" + esbuild-sunos-64 "0.14.8" + esbuild-windows-32 "0.14.8" + esbuild-windows-64 "0.14.8" + esbuild-windows-arm64 "0.14.8" escalade@^3.1.1: version "3.1.1" @@ -2073,14 +1909,6 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - exec-buffer@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/exec-buffer/-/exec-buffer-3.2.0.tgz#b1686dbd904c7cf982e652c1f5a79b1e5573082b" @@ -2650,28 +2478,11 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hosted-git-info@^4.0.1, hosted-git-info@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" @@ -3321,7 +3132,7 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -md5.js@^1.3.4: +md5.js@1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== @@ -3378,14 +3189,6 @@ micromatch@^4.0.0, micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.51.0, mime-db@^1.28.0: version "1.51.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" @@ -3430,16 +3233,6 @@ mini-css-extract-plugin@2.4.5: dependencies: schema-utils "^4.0.0" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -3831,17 +3624,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -3924,17 +3706,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - peek-readable@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.0.2.tgz#a5cb847e347d3eccdc37642c82d2b4155c1ab8af" @@ -4113,18 +3884,6 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -4164,21 +3923,13 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -4348,14 +4099,6 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -4373,7 +4116,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -4471,14 +4214,6 @@ set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"