ソースを参照

Add basic virtual repository support (wip)

main
Eric Amodio 2年前
コミット
e175082192
27個のファイルの変更2724行の追加1144行の削除
  1. +1
    -0
      .vscode/queries.github-graphql-nb
  2. +14
    -0
      package.json
  3. +17
    -23
      src/codelens/codeLensProvider.ts
  4. +1
    -1
      src/commands/showQuickCommitFile.ts
  5. +5
    -0
      src/config.ts
  6. +2
    -2
      src/container.ts
  7. +8
    -2
      src/env/browser/git.ts
  8. +4
    -4
      src/env/node/git.ts
  9. +97
    -286
      src/env/node/git/localGitProvider.ts
  10. +64
    -0
      src/errors.ts
  11. +20
    -26
      src/git/gitProvider.ts
  12. +161
    -67
      src/git/gitProviderService.ts
  13. +45
    -4
      src/git/gitUri.ts
  14. +34
    -34
      src/git/models/repository.ts
  15. +1
    -1
      src/git/remotes/github.ts
  16. +0
    -648
      src/github/github.ts
  17. +2083
    -0
      src/premium/github/githubGitProvider.ts
  18. +88
    -0
      src/premium/remotehub.ts
  19. +5
    -2
      src/repositories.ts
  20. +8
    -2
      src/system/path.ts
  21. +12
    -6
      src/views/nodes/fileHistoryNode.ts
  22. +21
    -6
      src/views/nodes/fileHistoryTrackerNode.ts
  23. +13
    -10
      src/views/nodes/lineHistoryNode.ts
  24. +14
    -14
      src/views/nodes/repositoriesNode.ts
  25. +2
    -2
      src/vsls/guest.ts
  26. +3
    -3
      src/vsls/host.ts
  27. +1
    -1
      src/vsls/protocol.ts

+ 1
- 0
.vscode/queries.github-graphql-nb
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 14
- 0
package.json ファイルの表示

@ -3117,6 +3117,20 @@
"markdownDeprecationMessage": "Deprecated. Use the [Insiders edition](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens-insiders) of GitLens instead"
}
}
},
{
"id": "experimental",
"title": "Experimental",
"order": 9999,
"properties": {
"gitlens.experimental.virtualRepositories.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to enable the experimental virtual repositories support",
"scope": "window",
"order": 0
}
}
}
],
"configurationDefaults": {

+ 17
- 23
src/codelens/codeLensProvider.ts ファイルの表示

@ -37,11 +37,12 @@ import {
} from '../configuration';
import { BuiltInCommands, DocumentSchemes } from '../constants';
import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import type { 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';
import { is, once } from '../system/function';
import { filterMap, find, first, join, map } from '../system/iterable';
export class GitRecentChangeCodeLens extends CodeLens {
constructor(
@ -90,6 +91,8 @@ export class GitCodeLensProvider implements CodeLensProvider {
{ scheme: DocumentSchemes.GitLens },
{ scheme: DocumentSchemes.PRs },
{ scheme: DocumentSchemes.Vsls },
{ scheme: DocumentSchemes.Virtual },
{ scheme: DocumentSchemes.GitHub },
];
private _onDidChangeCodeLenses = new EventEmitter<void>();
@ -175,7 +178,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (token.isCancellationRequested) return lenses;
const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 0, 1000000, 1000000)));
const documentRangeFn = once(() => document.validateRange(new Range(0, 0, 1000000, 1000000)));
// Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately
const dirtyCommand: Command | undefined = dirty
@ -211,9 +214,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined;
if (dirty || cfg.recentChange.enabled) {
if (!dirty) {
blameForRangeFn = Functions.once(() =>
this.container.git.getBlameForRangeSync(blame!, gitUri, blameRange),
);
blameForRangeFn = once(() => this.container.git.getBlameRange(blame!, gitUri, blameRange));
}
const fileSymbol = new SymbolInformation(
@ -239,9 +240,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
}
if (!dirty && cfg.authors.enabled) {
if (blameForRangeFn === undefined) {
blameForRangeFn = Functions.once(() =>
this.container.git.getBlameForRangeSync(blame!, gitUri, blameRange),
);
blameForRangeFn = once(() => this.container.git.getBlameRange(blame!, gitUri, blameRange));
}
const fileSymbol = new SymbolInformation(
@ -400,9 +399,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
let blameForRangeFn: (() => GitBlameLines | undefined) | undefined;
if (dirty || cfg.recentChange.enabled) {
if (!dirty) {
blameForRangeFn = Functions.once(() =>
this.container.git.getBlameForRangeSync(blame!, gitUri!, blameRange),
);
blameForRangeFn = once(() => this.container.git.getBlameRange(blame!, gitUri!, blameRange));
}
lenses.push(
new GitRecentChangeCodeLens(
@ -444,9 +441,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (multiline && !dirty) {
if (blameForRangeFn === undefined) {
blameForRangeFn = Functions.once(() =>
this.container.git.getBlameForRangeSync(blame!, gitUri!, blameRange),
);
blameForRangeFn = once(() => this.container.git.getBlameRange(blame!, gitUri!, blameRange));
}
lenses.push(
new GitAuthorsCodeLens(
@ -492,7 +487,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
const blame = lens.getBlame();
if (blame === undefined) return lens;
const recentCommit: GitCommit = Iterables.first(blame.commits.values());
const recentCommit: GitCommit = first(blame.commits.values());
// TODO@eamodio This is FAR too expensive, but this accounts for commits that delete lines -- is there another way?
// if (lens.uri != null) {
// const commit = await this.container.git.getCommitForFile(lens.uri.repoPath, lens.uri.fsPath, {
@ -578,7 +573,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
const count = blame.authors.size;
const author = Iterables.first(blame.authors.values()).name;
const author = first(blame.authors.values()).name;
let title = `${count} ${count > 1 ? 'authors' : 'author'} (${author}${count > 1 ? ' and others' : ''})`;
if (this.container.config.debug) {
@ -588,8 +583,8 @@ export class GitCodeLensProvider implements CodeLensProvider {
(lens.symbol as SymbolInformation).containerName
? `|${(lens.symbol as SymbolInformation).containerName}`
: ''
}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join(
Iterables.map(blame.authors.values(), a => a.name),
}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${join(
map(blame.authors.values(), a => a.name),
', ',
)})]`;
}
@ -598,8 +593,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
return this.applyCommandWithNoClickAction(title, lens);
}
const commit =
Iterables.find(blame.commits.values(), c => c.author === author) ?? Iterables.first(blame.commits.values());
const commit = find(blame.commits.values(), c => c.author === author) ?? first(blame.commits.values());
switch (lens.desiredCommand) {
case CodeLensCommand.CopyRemoteCommitUrl:
@ -730,7 +724,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
): T {
let refs;
if (commit === undefined) {
refs = [...Iterables.filterMap(blame.commits.values(), c => (c.isUncommitted ? undefined : c.ref))];
refs = [...filterMap(blame.commits.values(), c => (c.isUncommitted ? undefined : c.ref))];
} else {
refs = [commit.ref];
}
@ -884,5 +878,5 @@ function getRangeFromSymbol(symbol: DocumentSymbol | SymbolInformation) {
}
function isDocumentSymbol(symbol: DocumentSymbol | SymbolInformation): symbol is DocumentSymbol {
return Functions.is<DocumentSymbol>(symbol, 'children');
return is<DocumentSymbol>(symbol, 'children');
}

+ 1
- 1
src/commands/showQuickCommitFile.ts ファイルの表示

@ -64,7 +64,7 @@ export class ShowQuickCommitFileCommand extends ActiveEditorCachedCommand {
let gitUri;
if (args.revisionUri !== undefined) {
gitUri = GitUri.fromRevisionUri(Uri.parse(args.revisionUri));
gitUri = GitUri.fromRevisionUri(Uri.parse(args.revisionUri, true));
args.sha = gitUri.sha;
} else {
gitUri = await GitUri.fromUri(uri);

+ 5
- 0
src/config.ts ファイルの表示

@ -48,6 +48,11 @@ export interface Config {
defaultGravatarsStyle: GravatarDefaultStyle;
defaultTimeFormat: DateTimeFormat | string | null;
detectNestedRepositories: boolean;
experimental: {
virtualRepositories: {
enabled: boolean;
};
};
fileAnnotations: {
command: string | null;
};

+ 2
- 2
src/container.ts ファイルの表示

@ -253,7 +253,7 @@ export class Container {
return this._git;
}
private _github: Promise<import('./github/github').GitHubApi | undefined> | undefined;
private _github: Promise<import('./premium/github/github').GitHubApi | undefined> | undefined;
get github() {
if (this._github == null) {
this._github = this._loadGitHubApi();
@ -264,7 +264,7 @@ export class Container {
private async _loadGitHubApi() {
try {
return new (await import(/* webpackChunkName: "github" */ './github/github')).GitHubApi();
return new (await import(/* webpackChunkName: "github" */ './premium/github/github')).GitHubApi();
} catch (ex) {
Logger.error(ex);
return undefined;

+ 8
- 2
src/env/browser/git.ts ファイルの表示

@ -1,11 +1,17 @@
import { Container } from '../../container';
import { GitCommandOptions } from '../../git/commandOptions';
import { GitHubGitProvider } from '../../premium/github/githubGitProvider';
import { GitProvider } from '../../git/gitProvider';
// Force import of GitHub since dynamic imports are not supported in the WebWorker ExtensionHost
import * as GitHub from '../../premium/github/github';
export function git(_options: GitCommandOptions, ..._args: any[]): Promise<string | Buffer> {
return Promise.resolve('');
}
export function getSupportedGitProviders(_container: Container): GitProvider[] {
return [];
export function getSupportedGitProviders(container: Container): GitProvider[] {
if (!container.config.experimental.virtualRepositories.enabled) return [];
GitHub.GitHubApi;
return [new GitHubGitProvider(container)];
}

+ 4
- 4
src/env/node/git.ts ファイルの表示

@ -1,12 +1,12 @@
import { Container } from '../../container';
import { GitProvider } from '../../git/gitProvider';
import { GitHubGitProvider } from '../../premium/github/githubGitProvider';
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)];
return container.config.experimental.virtualRepositories.enabled
? [new LocalGitProvider(container), new GitHubGitProvider(container)]
: [new LocalGitProvider(container)];
}

+ 97
- 286
src/env/node/git/localGitProvider.ts ファイルの表示

@ -9,7 +9,6 @@ import {
extensions,
FileType,
Range,
TextEditor,
Uri,
window,
workspace,
@ -158,7 +157,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
private readonly _contributorsCache = new Map<string, Promise<GitContributor[]>>();
private readonly _mergeStatusCache = new Map<string, GitMergeStatus | null>();
private readonly _rebaseStatusCache = new Map<string, GitRebaseStatus | null>();
private readonly _remotesWithApiProviderCache = new Map<string, GitRemote<RichRemoteProvider> | null>();
private readonly _repoInfoCache = new Map<string, RepositoryInfo>();
private readonly _stashesCache = new Map<string, GitStash | null>();
private readonly _tagsCache = new Map<string, Promise<PagedResult<GitTag>>>();
@ -189,10 +187,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
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._trackedPaths.clear();
}
@ -345,7 +339,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
openRepository(
folder: WorkspaceFolder,
folder: WorkspaceFolder | undefined,
uri: Uri,
root: boolean,
suspended?: boolean,
@ -357,7 +351,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
this.onRepositoryChanged.bind(this),
this.descriptor,
folder,
uri.fsPath,
uri,
root,
suspended ?? !window.state.focused,
closed,
@ -496,12 +490,27 @@ export class LocalGitProvider implements GitProvider, Disposable {
});
}
canHandlePathOrUri(pathOrUri: string | Uri): string | undefined {
let scheme;
if (typeof pathOrUri === 'string') {
const match = isUriRegex.exec(pathOrUri);
if (match == null) return pathOrUri;
[, scheme] = match;
} else {
({ scheme } = pathOrUri);
}
if (!this.supportedSchemes.includes(scheme)) return undefined;
return typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.fsPath;
}
getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri {
// Convert the base to a Uri if it isn't one
if (typeof base === 'string') {
// If it looks like a Uri parse it
if (isUriRegex.test(base)) {
base = Uri.parse(base);
base = Uri.parse(base, true);
} else {
if (!isAbsolute(base)) {
debugger;
@ -554,7 +563,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (typeof base === 'string') {
// If it looks like a Uri parse it
if (isUriRegex.test(base)) {
base = Uri.parse(base);
base = Uri.parse(base, true);
} else {
if (!isAbsolute(base)) {
debugger;
@ -568,7 +577,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
// Convert the path to a Uri if it isn't one
if (typeof pathOrUri === 'string') {
if (isUriRegex.test(pathOrUri)) {
pathOrUri = Uri.parse(pathOrUri);
pathOrUri = Uri.parse(pathOrUri, true);
} else {
if (!isAbsolute(pathOrUri)) return normalizePath(pathOrUri);
@ -733,33 +742,29 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
@log()
resetCaches(...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]) {
if (cache.length === 0 || cache.includes('branches')) {
resetCaches(...affects: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]) {
if (affects.length === 0 || affects.includes('branches')) {
this._branchesCache.clear();
}
if (cache.length === 0 || cache.includes('contributors')) {
if (affects.length === 0 || affects.includes('contributors')) {
this._contributorsCache.clear();
}
if (cache.length === 0 || cache.includes('providers')) {
this._remotesWithApiProviderCache.clear();
}
if (cache.length === 0 || cache.includes('stashes')) {
if (affects.length === 0 || affects.includes('stashes')) {
this._stashesCache.clear();
}
if (cache.length === 0 || cache.includes('status')) {
if (affects.length === 0 || affects.includes('status')) {
this._mergeStatusCache.clear();
this._rebaseStatusCache.clear();
}
if (cache.length === 0 || cache.includes('tags')) {
if (affects.length === 0 || affects.includes('tags')) {
this._tagsCache.clear();
}
if (cache.length === 0) {
if (affects.length === 0) {
this._trackedPaths.clear();
this._repoInfoCache.clear();
}
@ -1166,7 +1171,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
const blame = await this.getBlameForFile(uri);
if (blame == null) return undefined;
return this.getBlameForRangeSync(blame, uri, range);
return this.getBlameRange(blame, uri, range);
}
@log<LocalGitProvider['getBlameForRangeContents']>({ args: { 2: '<contents>' } })
@ -1174,11 +1179,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
const blame = await this.getBlameForFileContents(uri, contents);
if (blame == null) return undefined;
return this.getBlameForRangeSync(blame, uri, range);
return this.getBlameRange(blame, uri, range);
}
@log<LocalGitProvider['getBlameForRangeContents']>({ args: { 0: '<blame>' } })
getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
@log<LocalGitProvider['getBlameRange']>({ args: { 0: '<blame>' } })
getBlameRange(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) {
@ -1260,42 +1265,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
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,
@ -1378,58 +1347,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
return result;
}
// @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<GitDiffShortStat | undefined> {
const data = await Git.diff__shortstat(repoPath, ref);
@ -1550,6 +1467,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
@gate()
@log()
async getCurrentUser(repoPath: string): Promise<GitUser | undefined> {
if (!repoPath) return undefined;
const cc = Logger.getCorrelationContext();
const repo = this._repoInfoCache.get(repoPath);
let user = repo?.user;
@ -1559,48 +1480,57 @@ export class LocalGitProvider implements GitProvider, Disposable {
user = { name: undefined, email: undefined };
const data = await Git.config__get_regex('^user\\.', repoPath, { local: true });
if (data) {
let key: string;
let value: string;
try {
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 || userInfo()?.username || undefined;
if (!user.name) {
// If we found no user data, mark it so we won't bother trying again
this._repoInfoCache.set(repoPath, { ...repo, user: null });
return undefined;
}
let match;
do {
match = userConfigRegex.exec(data);
if (match == null) break;
user.email =
process.env.GIT_AUTHOR_EMAIL ||
process.env.GIT_COMMITTER_EMAIL ||
process.env.EMAIL ||
`${user.name}@${hostname()}`;
}
[, 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 || userInfo()?.username || undefined;
if (!user.name) {
// If we found no user data, mark it so we won't bother trying again
this._repoInfoCache.set(repoPath, { ...repo, user: null });
return undefined;
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;
}
}
user.email =
process.env.GIT_AUTHOR_EMAIL ||
process.env.GIT_COMMITTER_EMAIL ||
process.env.EMAIL ||
`${user.name}@${hostname()}`;
}
this._repoInfoCache.set(repoPath, { ...repo, user: user });
return user;
} catch (ex) {
Logger.error(ex, cc);
debugger;
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;
}
// Mark it so we won't bother trying again
this._repoInfoCache.set(repoPath, { ...repo, user: null });
return undefined;
}
this._repoInfoCache.set(repoPath, { ...repo, user: user });
return user;
}
@log()
@ -1900,6 +1830,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
since?: string;
},
): Promise<GitLog | undefined> {
const cc = Logger.getCorrelationContext();
const limit = options?.limit ?? this.container.config.advanced.maxListItems ?? 0;
try {
@ -1931,6 +1863,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
return log;
} catch (ex) {
Logger.error(ex, cc);
debugger;
return undefined;
}
}
@ -1949,6 +1883,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
since?: string;
},
): Promise<Set<string> | undefined> {
const cc = Logger.getCorrelationContext();
const limit = options?.limit ?? this.container.config.advanced.maxListItems ?? 0;
try {
@ -1965,6 +1901,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
const commits = GitLogParser.parseRefsOnly(data);
return new Set(commits);
} catch (ex) {
Logger.error(ex, cc);
debugger;
return undefined;
}
}
@ -3062,91 +3000,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
};
}
@gate<LocalGitProvider['getRichRemoteProvider']>(
(repoPath, remotes, options) =>
`${repoPath}|${remotes?.map(r => r.id).join(',') ?? ''}|${options?.includeDisconnected ?? false}`,
)
@log<LocalGitProvider['getRichRemoteProvider']>({ args: { 1: remotes => remotes?.map(r => r.name).join(',') } })
async getRichRemoteProvider(
repoPath: string,
remotes: GitRemote<RemoteProvider | RichRemoteProvider | undefined>[] | undefined,
options?: { includeDisconnected?: boolean | undefined },
): Promise<GitRemote<RichRemoteProvider> | undefined> {
if (repoPath == null) return undefined;
const cacheKey = repoPath;
let richRemote = this._remotesWithApiProviderCache.get(cacheKey);
if (richRemote != null) return richRemote;
if (richRemote === null && !options?.includeDisconnected) return undefined;
if (options?.includeDisconnected) {
richRemote = this._remotesWithApiProviderCache.get(`disconnected|${cacheKey}`);
if (richRemote !== undefined) return richRemote ?? undefined;
}
remotes = (remotes ?? (await this.getRemotesWithProviders(repoPath))).filter(
(
r: GitRemote<RemoteProvider | RichRemoteProvider | undefined>,
): r is GitRemote<RemoteProvider | RichRemoteProvider> => r.provider != null,
);
if (remotes.length === 0) return undefined;
let remote;
if (remotes.length === 1) {
remote = remotes[0];
} else {
const weightedRemotes = new Map<string, number>([
['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?.hasRichProvider()) {
this._remotesWithApiProviderCache.set(cacheKey, null);
return undefined;
}
const { provider } = remote;
const connected = provider.maybeConnected ?? (await provider.isConnected());
if (connected) {
this._remotesWithApiProviderCache.set(cacheKey, remote);
} else {
this._remotesWithApiProviderCache.set(cacheKey, null);
this._remotesWithApiProviderCache.set(`disconnected|${cacheKey}`, remote);
if (!options?.includeDisconnected) return undefined;
}
return remote;
}
@log({ args: { 1: false } })
async getRemotes(
repoPath: string | undefined,
@ -3172,21 +3025,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
}
@log()
async getRemotesWithProviders(
repoPath: string | undefined,
options?: { sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> {
if (repoPath == null) return [];
const repository = this.container.git.getRepository(repoPath);
const remotes = await (repository != null
? repository.getRemotes(options)
: this.getRemotes(repoPath, options));
return remotes.filter(r => r.provider != null) as GitRemote<RemoteProvider | RichRemoteProvider>[];
}
@gate()
@log()
getRevisionContent(repoPath: string, path: string, ref: string): Promise<Uint8Array | undefined> {
@ -3214,10 +3052,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
@log()
async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined> {
async getStatusForFile(repoPath: string, path: string): Promise<GitStatusFile | undefined> {
const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1;
const data = await Git.status__file(repoPath, fileName, porcelainVersion, {
const data = await Git.status__file(repoPath, path, porcelainVersion, {
similarityThreshold: this.container.config.advanced.similarityThreshold,
});
const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
@ -3227,10 +3065,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
@log()
async getStatusForFiles(repoPath: string, path: string): Promise<GitStatusFile[] | undefined> {
async getStatusForFiles(repoPath: string, pathOrGlob: string): Promise<GitStatusFile[] | undefined> {
const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1;
const data = await Git.status__file(repoPath, path, porcelainVersion, {
const data = await Git.status__file(repoPath, pathOrGlob, porcelainVersion, {
similarityThreshold: this.container.config.advanced.similarityThreshold,
});
const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
@ -3270,7 +3108,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
@log({ args: { 1: false } })
async getTags(
repoPath: string | undefined,
options?: { filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions },
options?: { cursor?: string; filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions },
): Promise<PagedResult<GitTag>> {
if (repoPath == null) return emptyPagedResult;
@ -3348,19 +3186,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
return branches.length !== 0 || tags.length !== 0;
}
@log<LocalGitProvider['isActiveRepoPath']>({
args: { 1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
async isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise<boolean> {
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 {
return this.supportedSchemes.includes(uri.scheme);
}
@ -3610,18 +3435,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
return (await Git.rev_parse__verify(repoPath, ref)) != null;
}
stageFile(repoPath: string, fileName: string): Promise<void>;
stageFile(repoPath: string, uri: Uri): Promise<void>;
@log()
async stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise<void> {
await Git.add(
repoPath,
typeof fileNameOrUri === 'string' ? fileNameOrUri : splitPath(fileNameOrUri.fsPath, repoPath)[0],
);
async stageFile(repoPath: string, pathOrUri: string | Uri): Promise<void> {
await Git.add(repoPath, typeof pathOrUri === 'string' ? pathOrUri : splitPath(pathOrUri.fsPath, repoPath)[0]);
}
stageDirectory(repoPath: string, directory: string): Promise<void>;
stageDirectory(repoPath: string, uri: Uri): Promise<void>;
@log()
async stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void> {
await Git.add(
@ -3630,18 +3448,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
);
}
unStageFile(repoPath: string, fileName: string): Promise<void>;
unStageFile(repoPath: string, uri: Uri): Promise<void>;
@log()
async unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise<void> {
await Git.reset(
repoPath,
typeof fileNameOrUri === 'string' ? fileNameOrUri : splitPath(fileNameOrUri.fsPath, repoPath)[0],
);
async unStageFile(repoPath: string, pathOrUri: string | Uri): Promise<void> {
await Git.reset(repoPath, typeof pathOrUri === 'string' ? pathOrUri : splitPath(pathOrUri.fsPath, repoPath)[0]);
}
unStageDirectory(repoPath: string, directory: string): Promise<void>;
unStageDirectory(repoPath: string, uri: Uri): Promise<void>;
@log()
async unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void> {
await Git.reset(

+ 64
- 0
src/errors.ts ファイルの表示

@ -44,6 +44,70 @@ export class AuthenticationError extends Error {
}
}
export class ExtensionNotFoundError extends Error {
constructor(public readonly extensionId: string, public readonly extensionName: string) {
super(
`Unable to find the ${extensionName} extension (${extensionId}). Please ensure it is installed and enabled.`,
);
Error.captureStackTrace?.(this, ExtensionNotFoundError);
}
}
export const enum OpenVirtualRepositoryErrorReason {
RemoteHubApiNotFound = 1,
NotAGitHubRepository = 2,
GitHubAuthenticationNotFound = 3,
GitHubAuthenticationDenied = 4,
}
export class OpenVirtualRepositoryError extends Error {
readonly original?: Error;
readonly reason: OpenVirtualRepositoryErrorReason | undefined;
readonly repoPath: string;
constructor(repoPath: string, reason?: OpenVirtualRepositoryErrorReason, original?: Error);
constructor(repoPath: string, message?: string, original?: Error);
constructor(
repoPath: string,
messageOrReason: string | OpenVirtualRepositoryErrorReason | undefined,
original?: Error,
) {
let message;
let reason: OpenVirtualRepositoryErrorReason | undefined;
if (messageOrReason == null) {
message = `Unable to open the virtual repository: ${repoPath}`;
} else if (typeof messageOrReason === 'string') {
message = messageOrReason;
reason = undefined;
} else {
reason = messageOrReason;
message = `Unable to open the virtual repository: ${repoPath}; `;
switch (reason) {
case OpenVirtualRepositoryErrorReason.RemoteHubApiNotFound:
message +=
'Unable to get required api from the GitHub Repositories extension. Please ensure that the GitHub Repositories extension is installed and enabled';
break;
case OpenVirtualRepositoryErrorReason.NotAGitHubRepository:
message += 'Only GitHub repositories are supported currently';
break;
case OpenVirtualRepositoryErrorReason.GitHubAuthenticationNotFound:
message += 'Unable to get required GitHub authentication';
break;
case OpenVirtualRepositoryErrorReason.GitHubAuthenticationDenied:
message += 'GitHub authentication is required';
break;
}
}
super(message);
this.original = original;
this.reason = reason;
this.repoPath = repoPath;
Error.captureStackTrace?.(this, OpenVirtualRepositoryError);
}
}
export class ProviderNotFoundError extends Error {
constructor(pathOrUri: string | Uri | undefined) {
super(

+ 20
- 26
src/git/gitProvider.ts ファイルの表示

@ -1,4 +1,4 @@
import { Disposable, Event, Range, TextEditor, Uri, WorkspaceFolder } from 'vscode';
import { Disposable, Event, Range, Uri, WorkspaceFolder } from 'vscode';
import { Commit, InputBox } from '../@types/vscode.git';
import { GitUri } from './gitUri';
import {
@ -82,15 +82,23 @@ export interface GitProvider extends Disposable {
discoverRepositories(uri: Uri): Promise<Repository[]>;
updateContext?(): void;
openRepository(folder: WorkspaceFolder, uri: Uri, root: boolean, suspended?: boolean, closed?: boolean): Repository;
openRepository(
folder: WorkspaceFolder | undefined,
uri: Uri,
root: boolean,
suspended?: boolean,
closed?: boolean,
): Repository;
openRepositoryInitWatcher?(): RepositoryInitWatcher;
getOpenScmRepositories(): Promise<ScmRepository[]>;
getOrOpenScmRepository(repoPath: string): Promise<ScmRepository | undefined>;
canHandlePathOrUri(pathOrUri: string | Uri): string | undefined;
getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri;
getBestRevisionUri(repoPath: string, path: string, ref: string | undefined): Promise<Uri | undefined>;
getRelativePath(pathOrUri: string | Uri, base: string | Uri): string;
getRevisionUri(repoPath: string, path: string, ref: string): Uri;
// getRootUri(pathOrUri: string | Uri): Uri;
getWorkingUri(repoPath: string, uri: Uri): Promise<Uri | undefined>;
addRemote(repoPath: string, name: string, url: string): Promise<void>;
@ -103,7 +111,7 @@ export interface GitProvider extends Disposable {
options?: { createBranch?: string | undefined } | { fileName?: string | undefined },
): Promise<void>;
resetCaches(
...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]
...affects: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]
): void;
excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise<Uri[]>;
fetch(
@ -154,7 +162,7 @@ export interface GitProvider extends Disposable {
): Promise<GitBlameLine | undefined>;
getBlameForRange(uri: GitUri, range: Range): Promise<GitBlameLines | undefined>;
getBlameForRangeContents(uri: GitUri, range: Range, contents: string): Promise<GitBlameLines | undefined>;
getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined;
getBlameRange(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined;
getBranch(repoPath: string): Promise<GitBranch | undefined>;
getBranches(
repoPath: string,
@ -319,27 +327,22 @@ export interface GitProvider extends Disposable {
skip?: number | undefined;
},
): Promise<GitReflog | undefined>;
getRichRemoteProvider(
repoPath: string,
remotes: GitRemote<RemoteProvider | RichRemoteProvider | undefined>[] | undefined,
options?: { includeDisconnected?: boolean | undefined },
): Promise<GitRemote<RichRemoteProvider> | undefined>;
getRemotes(
repoPath: string | undefined,
options?: { providers?: RemoteProviders; sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider | undefined>[]>;
getRemotesWithProviders(
repoPath: string | undefined,
options?: { force?: boolean; providers?: RemoteProviders; sort?: boolean | undefined },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]>;
getRevisionContent(repoPath: string, path: string, ref: string): Promise<Uint8Array | undefined>;
getStash(repoPath: string | undefined): Promise<GitStash | undefined>;
getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined>;
getStatusForFile(repoPath: string, path: string): Promise<GitStatusFile | undefined>;
getStatusForFiles(repoPath: string, pathOrGlob: string): Promise<GitStatusFile[] | undefined>;
getStatusForRepo(repoPath: string | undefined): Promise<GitStatus | undefined>;
getTags(
repoPath: string | undefined,
options?: { filter?: ((t: GitTag) => boolean) | undefined; sort?: boolean | TagSortOptions | undefined },
options?: {
cursor?: string;
filter?: ((t: GitTag) => boolean) | undefined;
sort?: boolean | TagSortOptions | undefined;
},
): Promise<PagedResult<GitTag>>;
getTreeEntryForRevision(repoPath: string, path: string, ref: string): Promise<GitTreeEntry | undefined>;
getTreeForRevision(repoPath: string, ref: string): Promise<GitTreeEntry[]>;
@ -352,7 +355,6 @@ export interface GitProvider extends Disposable {
| undefined;
},
): Promise<boolean>;
isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise<boolean>;
isTrackable(uri: Uri): boolean;
@ -378,17 +380,9 @@ export interface GitProvider extends Disposable {
validateBranchOrTagName(repoPath: string, ref: string): Promise<boolean>;
validateReference(repoPath: string, ref: string): Promise<boolean>;
stageFile(repoPath: string, fileName: string): Promise<void>;
stageFile(repoPath: string, uri: Uri): Promise<void>;
stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise<void>;
stageDirectory(repoPath: string, directory: string): Promise<void>;
stageDirectory(repoPath: string, uri: Uri): Promise<void>;
stageFile(repoPath: string, pathOrUri: string | Uri): Promise<void>;
stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void>;
unStageFile(repoPath: string, fileName: string): Promise<void>;
unStageFile(repoPath: string, uri: Uri): Promise<void>;
unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise<void>;
unStageDirectory(repoPath: string, directory: string): Promise<void>;
unStageDirectory(repoPath: string, uri: Uri): Promise<void>;
unStageFile(repoPath: string, pathOrUri: string | Uri): Promise<void>;
unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void>;
stashApply(repoPath: string, stashName: string, options?: { deleteAfter?: boolean | undefined }): Promise<void>;

+ 161
- 67
src/git/gitProviderService.ts ファイルの表示

@ -14,7 +14,6 @@ import {
WorkspaceFolder,
WorkspaceFoldersChangeEvent,
} from 'vscode';
import { isWeb } from '@env/platform';
import { resetAvatarCache } from '../avatars';
import { configuration } from '../configuration';
import {
@ -33,7 +32,7 @@ import { groupByFilterMap, groupByMap } from '../system/array';
import { gate } from '../system/decorators/gate';
import { debug, log } from '../system/decorators/log';
import { count, filter, first, flatMap, map } from '../system/iterable';
import { basename, dirname, isAbsolute, normalizePath } from '../system/path';
import { dirname, getBestPath, isAbsolute, normalizePath } from '../system/path';
import { cancellable, isPromise, PromiseCancelledError } from '../system/promise';
import { CharCode } from '../system/string';
import { VisitedPathsTrie } from '../system/trie';
@ -137,8 +136,9 @@ export class GitProviderService implements Disposable {
private readonly _disposable: Disposable;
private readonly _providers = new Map<GitProviderId, GitProvider>();
private readonly _repositories = new Repositories();
private readonly _richRemotesCache = new Map<string, GitRemote<RichRemoteProvider> | null>();
private readonly _supportedSchemes = new Set<string>();
private _visitedPaths = new VisitedPathsTrie();
private readonly _visitedPaths = new VisitedPathsTrie();
constructor(private readonly container: Container) {
this._disposable = Disposable.from(
@ -312,6 +312,16 @@ export class GitProviderService implements Disposable {
provider,
...disposables,
provider.onDidChangeRepository(e => {
if (
e.changed(
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChangeComparisonMode.Any,
)
) {
this._richRemotesCache.clear();
}
if (e.changed(RepositoryChange.Closed, RepositoryChangeComparisonMode.Any)) {
this.updateContext();
@ -556,47 +566,62 @@ export class GitProviderService implements Disposable {
this._providers.forEach(p => p.updateContext?.());
}
// private _pathToProvider = new Map<string, GitProviderResult>();
private getProvider(repoPath: string | Uri): GitProviderResult {
if (repoPath == null || (typeof repoPath !== 'string' && !this._supportedSchemes.has(repoPath.scheme))) {
debugger;
throw new ProviderNotFoundError(repoPath);
}
let id = !isWeb ? GitProviderId.Git : undefined;
if (typeof repoPath !== 'string' && repoPath.scheme === DocumentSchemes.Virtual) {
if (repoPath.authority.startsWith('github')) {
id = GitProviderId.GitHub;
} else {
throw new ProviderNotFoundError(repoPath);
}
}
if (id == null) throw new ProviderNotFoundError(repoPath);
const provider = this._providers.get(id);
if (provider == null) throw new ProviderNotFoundError(repoPath);
switch (id) {
case GitProviderId.Git:
return {
provider: provider,
path: typeof repoPath === 'string' ? repoPath : repoPath.fsPath,
};
default:
return {
provider: provider,
path: typeof repoPath === 'string' ? repoPath : repoPath.toString(),
};
// const key = repoPath.toString();
// let providerResult = this._pathToProvider.get(key);
// if (providerResult != null) return providerResult;
for (const provider of this._providers.values()) {
const path = provider.canHandlePathOrUri(repoPath);
if (path == null) continue;
const providerResult: GitProviderResult = { provider: provider, path: path };
// this._pathToProvider.set(key, providerResult);
return providerResult;
}
debugger;
throw new ProviderNotFoundError(repoPath);
// let id = !isWeb ? GitProviderId.Git : undefined;
// if (typeof repoPath !== 'string' && repoPath.scheme === DocumentSchemes.Virtual) {
// if (repoPath.authority.startsWith('github')) {
// id = GitProviderId.GitHub;
// } else {
// throw new ProviderNotFoundError(repoPath);
// }
// }
// if (id == null) throw new ProviderNotFoundError(repoPath);
// const provider = this._providers.get(id);
// if (provider == null) throw new ProviderNotFoundError(repoPath);
// switch (id) {
// case GitProviderId.Git:
// return {
// provider: provider,
// path: typeof repoPath === 'string' ? repoPath : repoPath.fsPath,
// };
// default:
// return {
// provider: provider,
// path: typeof repoPath === 'string' ? repoPath : repoPath.toString(),
// };
// }
}
getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri {
if (base == null) {
if (typeof pathOrUri === 'string') {
if (isUriRegex.test(pathOrUri)) {
debugger;
return Uri.parse(pathOrUri, true);
}
if (isUriRegex.test(pathOrUri)) return Uri.parse(pathOrUri, true);
// I think it is safe to assume this should be file://
return Uri.file(pathOrUri);
@ -651,7 +676,7 @@ export class GitProviderService implements Disposable {
ref = refOrUri.sha;
repoPath = refOrUri.repoPath!;
path = refOrUri.scheme === DocumentSchemes.File ? refOrUri.fsPath : refOrUri.path;
path = getBestPath(refOrUri);
}
const { provider, path: rp } = this.getProvider(repoPath!);
@ -700,17 +725,23 @@ export class GitProviderService implements Disposable {
@log()
resetCaches(
...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]
...affects: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]
): void {
const repoCache = cache.filter((c): c is 'branches' | 'remotes' => c === 'branches' || c === 'remotes');
if (affects.length === 0 || affects.includes('providers')) {
this._richRemotesCache.clear();
}
const repoAffects = affects.filter((c): c is 'branches' | 'remotes' => c === 'branches' || c === 'remotes');
// Delegate to the repos, if we are clearing everything or one of the per-repo caches
if (cache.length === 0 || repoCache.length > 0) {
if (affects.length === 0 || repoAffects.length > 0) {
for (const repo of this.repositories) {
repo.resetCaches(...repoCache);
repo.resetCaches(...repoAffects);
}
}
void Promise.allSettled([...this._providers.values()].map(provider => provider.resetCaches(...cache)));
for (const provider of this._providers.values()) {
provider.resetCaches(...affects);
}
}
@log<GitProviderService['excludeIgnoredUris']>({ args: { 1: uris => uris.length } })
@ -878,10 +909,10 @@ export class GitProviderService implements Disposable {
return provider.getBlameForRangeContents(uri, range, contents);
}
@log<GitProviderService['getBlameForRangeSync']>({ args: { 0: '<blame>' } })
getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
@log<GitProviderService['getBlameRange']>({ args: { 0: '<blame>' } })
getBlameRange(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
const { provider } = this.getProvider(uri);
return provider.getBlameForRangeSync(blame, uri, range);
return provider.getBlameRange(blame, uri, range);
}
@log()
@ -1457,8 +1488,81 @@ export class GitProviderService implements Disposable {
remotesOrRepoPath = remotesOrRepoPath[0].repoPath;
}
const { provider, path } = this.getProvider(remotesOrRepoPath);
return provider.getRichRemoteProvider(path, remotes, options);
if (typeof remotesOrRepoPath === 'string') {
remotesOrRepoPath = this.getAbsoluteUri(remotesOrRepoPath);
}
const cacheKey = asRepoComparisonKey(remotesOrRepoPath);
let richRemote = this._richRemotesCache.get(cacheKey);
if (richRemote != null) return richRemote;
if (richRemote === null && !options?.includeDisconnected) return undefined;
if (options?.includeDisconnected) {
richRemote = this._richRemotesCache.get(`disconnected|${cacheKey}`);
if (richRemote !== undefined) return richRemote ?? undefined;
}
remotes = (remotes ?? (await this.getRemotesWithProviders(remotesOrRepoPath))).filter(
(
r: GitRemote<RemoteProvider | RichRemoteProvider | undefined>,
): r is GitRemote<RemoteProvider | RichRemoteProvider> => r.provider != null,
);
if (remotes.length === 0) return undefined;
let remote;
if (remotes.length === 1) {
remote = remotes[0];
} else {
const weightedRemotes = new Map<string, number>([
['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?.hasRichProvider()) {
this._richRemotesCache.set(cacheKey, null);
return undefined;
}
const { provider } = remote;
const connected = provider.maybeConnected ?? (await provider.isConnected());
if (connected) {
this._richRemotesCache.set(cacheKey, remote);
} else {
this._richRemotesCache.set(cacheKey, null);
this._richRemotesCache.set(`disconnected|${cacheKey}`, remote);
if (!options?.includeDisconnected) return undefined;
}
return remote;
}
@log({ args: { 1: false } })
@ -1479,8 +1583,12 @@ export class GitProviderService implements Disposable {
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> {
if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath);
return provider.getRemotesWithProviders(path, options);
const repository = this.container.git.getRepository(repoPath);
const remotes = await (repository != null
? repository.getRemotes(options)
: this.getRemotes(repoPath, options));
return remotes.filter(r => r.provider != null) as GitRemote<RemoteProvider | RichRemoteProvider>[];
}
getBestRepository(): Repository | undefined;
@ -1546,22 +1654,8 @@ export class GitProviderService implements Disposable {
}
}
let folder: WorkspaceFolder | undefined;
if (root != null) {
folder = root.folder;
} else {
folder = workspace.getWorkspaceFolder(repoUri);
if (folder == null) {
folder = {
uri: uri,
name: basename(normalizePath(repoUri.path)),
index: this.repositoryCount,
};
}
}
Logger.log(cc, `Repository found in '${repoUri.toString(false)}'`);
repository = provider.openRepository(folder, repoUri, false);
repository = provider.openRepository(root?.folder, repoUri, false);
this._repositories.add(repository);
this._pendingRepositories.delete(key);
@ -1654,7 +1748,7 @@ export class GitProviderService implements Disposable {
@log({ args: { 1: false } })
async getTags(
repoPath: string | Uri | undefined,
options?: { filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions },
options?: { cursor?: string; filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions },
): Promise<PagedResult<GitTag>> {
if (repoPath == null) return { values: [] };
@ -1803,12 +1897,12 @@ export class GitProviderService implements Disposable {
return provider.validateReference(path, ref);
}
stageFile(repoPath: string | Uri, fileName: string): Promise<void>;
stageFile(repoPath: string | Uri, path: string): Promise<void>;
stageFile(repoPath: string | Uri, uri: Uri): Promise<void>;
@log()
stageFile(repoPath: string | Uri, fileNameOrUri: string | Uri): Promise<void> {
stageFile(repoPath: string | Uri, pathOrUri: string | Uri): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stageFile(path, fileNameOrUri);
return provider.stageFile(path, pathOrUri);
}
stageDirectory(repoPath: string | Uri, directory: string): Promise<void>;
@ -1819,12 +1913,12 @@ export class GitProviderService implements Disposable {
return provider.stageDirectory(path, directoryOrUri);
}
unStageFile(repoPath: string | Uri, fileName: string): Promise<void>;
unStageFile(repoPath: string | Uri, path: string): Promise<void>;
unStageFile(repoPath: string | Uri, uri: Uri): Promise<void>;
@log()
unStageFile(repoPath: string | Uri, fileNameOrUri: string | Uri): Promise<void> {
unStageFile(repoPath: string | Uri, pathOrUri: string | Uri): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.unStageFile(path, fileNameOrUri);
return provider.unStageFile(path, pathOrUri);
}
unStageDirectory(repoPath: string | Uri, directory: string): Promise<void>;

+ 45
- 4
src/git/gitUri.ts ファイルの表示

@ -4,9 +4,10 @@ import { UriComparer } from '../comparers';
import { DocumentSchemes } from '../constants';
import { Container } from '../container';
import { Logger } from '../logger';
import { GitHubAuthorityMetadata } from '../premium/remotehub';
import { debug } from '../system/decorators/log';
import { memoize } from '../system/decorators/memoize';
import { basename, dirname, isAbsolute, normalizePath, relative } from '../system/path';
import { basename, dirname, getBestPath, isAbsolute, normalizePath, relative } from '../system/path';
import { CharCode, truncateLeft, truncateMiddle } from '../system/string';
import { RevisionUriData } from './gitProvider';
import { GitCommit, GitFile, GitRevision } from './models';
@ -66,8 +67,34 @@ export class GitUri extends (Uri as any as UriEx) {
const metadata = decodeGitLensRevisionUriAuthority<RevisionUriData>(uri.authority);
this.repoPath = metadata.repoPath;
if (GitRevision.isUncommittedStaged(metadata.ref) || !GitRevision.isUncommitted(metadata.ref)) {
this.sha = metadata.ref;
let ref = metadata.ref;
if (commitOrRepoPath != null && typeof commitOrRepoPath !== 'string') {
ref = commitOrRepoPath.sha;
}
if (GitRevision.isUncommittedStaged(ref) || !GitRevision.isUncommitted(ref)) {
this.sha = ref;
}
return;
}
if (uri.scheme === DocumentSchemes.Virtual || uri.scheme === DocumentSchemes.GitHub) {
super(uri);
const [, owner, repo] = uri.path.split('/', 3);
this.repoPath = uri.with({ path: `/${owner}/${repo}` }).toString();
const data = decodeRemoteHubAuthority<GitHubAuthorityMetadata>(uri);
let ref = data.metadata?.ref?.id;
if (commitOrRepoPath != null && typeof commitOrRepoPath !== 'string') {
ref = commitOrRepoPath.sha;
}
if (ref && (GitRevision.isUncommittedStaged(ref) || !GitRevision.isUncommitted(ref))) {
this.sha = ref;
}
return;
@ -429,7 +456,7 @@ export class GitUri extends (Uri as any as UriEx) {
path: uri.path,
query: JSON.stringify({
// Ensure we use the fsPath here, otherwise the url won't open properly
path: uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path,
path: getBestPath(uri),
ref: '~',
}),
});
@ -454,3 +481,17 @@ export function decodeGitLensRevisionUriAuthority(authority: string): T {
export function encodeGitLensRevisionUriAuthority<T>(metadata: T): string {
return encodeUtf8Hex(JSON.stringify(metadata));
}
function decodeRemoteHubAuthority<T>(uri: Uri): { scheme: string; metadata: T | undefined } {
const [scheme, encoded] = uri.authority.split('+');
let metadata: T | undefined;
if (encoded) {
try {
const data = JSON.parse(decodeUtf8Hex(encoded));
metadata = data as T;
} catch {}
}
return { scheme: scheme, metadata: metadata };
}

+ 34
- 34
src/git/models/repository.ts ファイルの表示

@ -14,21 +14,20 @@ import {
import type { CreatePullRequestActionContext } from '../../api/gitlens';
import { executeActionCommand } from '../../commands';
import { configuration } from '../../configuration';
import { BuiltInGitCommands, BuiltInGitConfiguration, Starred, WorkspaceState } from '../../constants';
import { BuiltInGitCommands, BuiltInGitConfiguration, DocumentSchemes, Starred, WorkspaceState } from '../../constants';
import { Container } from '../../container';
import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { asRepoComparisonKey } from '../../repositories';
import { filterMap, groupByMap } from '../../system/array';
import { getFormatter } from '../../system/date';
import { gate } from '../../system/decorators/gate';
import { debug, log, logName } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { debounce } from '../../system/function';
import { filter, join, some } from '../../system/iterable';
import { basename } from '../../system/path';
import { runGitCommandInTerminal } from '../../terminal';
import { GitProviderDescriptor } from '../gitProvider';
import { GitUri } from '../gitUri';
import { RemoteProviderFactory, RemoteProviders } from '../remotes/factory';
import { RichRemoteProvider } from '../remotes/provider';
import { SearchPattern } from '../search';
@ -183,7 +182,6 @@ export class Repository implements Disposable {
readonly id: string;
readonly index: number;
readonly name: string;
readonly normalizedPath: string;
private _branch: Promise<GitBranch | undefined> | undefined;
private readonly _disposable: Disposable;
@ -203,29 +201,36 @@ export class Repository implements Disposable {
private readonly container: Container,
private readonly onDidRepositoryChange: (repo: Repository, e: RepositoryChangeEvent) => void,
public readonly provider: GitProviderDescriptor,
public readonly folder: WorkspaceFolder,
public readonly path: string,
public readonly folder: WorkspaceFolder | undefined,
public readonly uri: Uri,
public readonly root: boolean,
suspended: boolean,
closed: boolean = false,
) {
const relativePath = container.git.getRelativePath(folder.uri, path);
if (root) {
// Check if the repository is not contained by a workspace folder
const repoFolder = workspace.getWorkspaceFolder(GitUri.fromRepoPath(path));
if (repoFolder == null) {
this.formattedName = this.name = basename(path);
folder = workspace.getWorkspaceFolder(uri) ?? folder;
if (folder != null) {
this.name = folder.name;
if (root) {
this.formattedName = this.name;
} else {
this.formattedName = this.name = folder.name;
const relativePath = container.git.getRelativePath(folder.uri, uri);
this.formattedName = relativePath ? `${this.name} (${relativePath})` : this.name;
}
} else {
this.formattedName = relativePath ? `${folder.name} (${relativePath})` : folder.name;
this.name = folder.name;
this.name = basename(uri.path);
this.formattedName = this.name;
// TODO@eamodio should we create a fake workspace folder?
// folder = {
// uri: uri,
// name: this.name,
// index: container.git.repositoryCount,
// };
}
this.index = folder.index;
this.index = folder?.index ?? container.git.repositoryCount;
this.normalizedPath = (path.endsWith('/') ? path : `${path}/`).toLowerCase();
this.id = this.normalizedPath;
this.id = asRepoComparisonKey(uri);
this._suspended = suspended;
this._closed = closed;
@ -264,9 +269,8 @@ export class Repository implements Disposable {
this._disposable.dispose();
}
@memoize()
get uri(): Uri {
return this.container.git.getAbsoluteUri(this.path);
get path(): string {
return this.uri.scheme === DocumentSchemes.File ? this.uri.fsPath : this.uri.toString();
}
get etag(): number {
@ -279,8 +283,8 @@ export class Repository implements Disposable {
}
private onConfigurationChanged(e?: ConfigurationChangeEvent) {
if (configuration.changed(e, 'remotes', this.folder.uri)) {
this._providers = RemoteProviderFactory.loadProviders(configuration.get('remotes', this.folder.uri));
if (configuration.changed(e, 'remotes', this.folder?.uri)) {
this._providers = RemoteProviderFactory.loadProviders(configuration.get('remotes', this.folder?.uri));
if (e != null) {
this.resetCaches('remotes');
@ -442,11 +446,7 @@ export class Repository implements Disposable {
}
containsUri(uri: Uri) {
if (GitUri.is(uri)) {
uri = uri.repoPath != null ? this.container.git.getAbsoluteUri(uri.repoPath) : uri.documentUri();
}
return this.folder === workspace.getWorkspaceFolder(uri);
return this === this.container.git.getRepository(uri);
}
@gate()
@ -513,8 +513,8 @@ export class Repository implements Disposable {
return this.container.git.getBranches(this.path, options);
}
getChangedFilesCount(sha?: string): Promise<GitDiffShortStat | undefined> {
return this.container.git.getChangedFilesCount(this.path, sha);
getChangedFilesCount(ref?: string): Promise<GitDiffShortStat | undefined> {
return this.container.git.getChangedFilesCount(this.path, ref);
}
getCommit(ref: string): Promise<GitLogCommit | undefined> {
@ -561,7 +561,7 @@ export class Repository implements Disposable {
async getRemotes(options: { filter?: (remote: GitRemote) => boolean; sort?: boolean } = {}): Promise<GitRemote[]> {
if (this._remotes == null) {
if (this._providers == null) {
const remotesCfg = configuration.get('remotes', this.folder.uri);
const remotesCfg = configuration.get('remotes', this.folder?.uri);
this._providers = RemoteProviderFactory.loadProviders(remotesCfg);
}
@ -781,12 +781,12 @@ export class Repository implements Disposable {
this.runTerminalCommand('reset', ...args);
}
resetCaches(...cache: ('branches' | 'remotes')[]) {
if (cache.length === 0 || cache.includes('branches')) {
resetCaches(...affects: ('branches' | 'remotes')[]) {
if (affects.length === 0 || affects.includes('branches')) {
this._branch = undefined;
}
if (cache.length === 0 || cache.includes('remotes')) {
if (affects.length === 0 || affects.includes('remotes')) {
this._remotes = undefined;
this._remotesDisposable?.dispose();
this._remotesDisposable = undefined;

+ 1
- 1
src/git/remotes/github.ts ファイルの表示

@ -238,7 +238,7 @@ export class GitHubRemote extends RichRemoteProvider {
const [owner, repo] = this.splitPath();
const { include, ...opts } = options ?? {};
const GitHubPullRequest = (await import('../../github/github')).GitHubPullRequest;
const GitHubPullRequest = (await import('../../premium/github/github')).GitHubPullRequest;
return (await Container.instance.github)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, {
...opts,
include: include?.map(s => GitHubPullRequest.toState(s)),

+ 0
- 648
src/github/github.ts ファイルの表示

@ -1,648 +0,0 @@
import { Octokit } from '@octokit/core';
import { GraphqlResponseError } from '@octokit/graphql';
import { RequestError } from '@octokit/request-error';
import type { Endpoints, OctokitResponse, RequestParameters } from '@octokit/types';
import fetch from '@env/fetch';
import { isWeb } from '@env/platform';
import {
AuthenticationError,
AuthenticationErrorReason,
ProviderRequestClientError,
ProviderRequestNotFoundError,
} from '../errors';
import {
type DefaultBranch,
type IssueOrPullRequest,
type IssueOrPullRequestType,
PullRequest,
PullRequestState,
} from '../git/models';
import type { Account } from '../git/models/author';
import type { RichRemoteProvider } from '../git/remotes/provider';
import { LogCorrelationContext, Logger, LogLevel } from '../logger';
import { debug, Stopwatch } from '../system';
export class GitHubApi {
@debug<GitHubApi['getAccountForCommit']>({ args: { 0: p => p.name, 1: '<token>' } })
async getAccountForCommit(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
ref: string,
options?: {
baseUrl?: string;
avatarSize?: number;
},
): Promise<Account | undefined> {
const cc = Logger.getCorrelationContext();
interface QueryResult {
repository:
| {
object:
| {
author?: {
name: string | null;
email: string | null;
avatarUrl: string;
};
}
| null
| undefined;
}
| null
| undefined;
}
try {
const query = `query getAccountForCommit(
$owner: String!
$repo: String!
$ref: GitObjectID!
$avatarSize: Int
) {
repository(name: $repo, owner: $owner) {
object(oid: $ref) {
... on Commit {
author {
name
email
avatarUrl(size: $avatarSize)
}
}
}
}
}`;
const rsp = await this.graphql<QueryResult>(token, query, {
...options,
owner: owner,
repo: repo,
ref: ref,
});
const author = rsp?.repository?.object?.author;
if (author == null) return undefined;
return {
provider: provider,
name: author.name ?? undefined,
email: author.email ?? undefined,
avatarUrl: author.avatarUrl,
};
} catch (ex) {
return this.handleRequestError(ex, cc, undefined);
}
}
@debug<GitHubApi['getAccountForEmail']>({ args: { 0: p => p.name, 1: '<token>' } })
async getAccountForEmail(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
email: string,
options?: {
baseUrl?: string;
avatarSize?: number;
},
): Promise<Account | undefined> {
const cc = Logger.getCorrelationContext();
interface QueryResult {
search:
| {
nodes:
| {
name: string | null;
email: string | null;
avatarUrl: string;
}[]
| null
| undefined;
}
| null
| undefined;
}
try {
const query = `query getAccountForEmail(
$emailQuery: String!
$avatarSize: Int
) {
search(type: USER, query: $emailQuery, first: 1) {
nodes {
... on User {
name
email
avatarUrl(size: $avatarSize)
}
}
}
}`;
const rsp = await this.graphql<QueryResult>(token, query, {
...options,
owner: owner,
repo: repo,
emailQuery: `in:email ${email}`,
});
const author = rsp?.search?.nodes?.[0];
if (author == null) return undefined;
return {
provider: provider,
name: author.name ?? undefined,
email: author.email ?? undefined,
avatarUrl: author.avatarUrl,
};
} catch (ex) {
return this.handleRequestError(ex, cc, undefined);
}
}
@debug<GitHubApi['getDefaultBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
async getDefaultBranch(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
options?: {
baseUrl?: string;
},
): Promise<DefaultBranch | undefined> {
const cc = Logger.getCorrelationContext();
interface QueryResult {
repository: {
defaultBranchRef: {
name: string;
} | null;
} | null;
}
try {
const query = `query getDefaultBranch(
$owner: String!
$repo: String!
) {
repository(name: $repo, owner: $owner) {
defaultBranchRef {
name
}
}
}`;
const rsp = await this.graphql<QueryResult>(token, query, {
...options,
owner: owner,
repo: repo,
});
const defaultBranch = rsp?.repository?.defaultBranchRef?.name ?? undefined;
if (defaultBranch == null) return undefined;
return {
provider: provider,
name: defaultBranch,
};
} catch (ex) {
return this.handleRequestError(ex, cc, undefined);
}
}
@debug<GitHubApi['getIssueOrPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
async getIssueOrPullRequest(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
number: number,
options?: {
baseUrl?: string;
},
): Promise<IssueOrPullRequest | undefined> {
const cc = Logger.getCorrelationContext();
interface QueryResult {
repository?: { issueOrPullRequest?: GitHubIssueOrPullRequest };
}
try {
const query = `query getIssueOrPullRequest(
$owner: String!
$repo: String!
$number: Int!
) {
repository(name: $repo, owner: $owner) {
issueOrPullRequest(number: $number) {
__typename
... on Issue {
createdAt
closed
closedAt
title
url
}
... on PullRequest {
createdAt
closed
closedAt
title
url
}
}
}
}`;
const rsp = await this.graphql<QueryResult>(token, query, {
...options,
owner: owner,
repo: repo,
number: number,
});
const issue = rsp?.repository?.issueOrPullRequest;
if (issue == null) return undefined;
return {
provider: provider,
type: issue.type,
id: String(number),
date: new Date(issue.createdAt),
title: issue.title,
closed: issue.closed,
closedDate: issue.closedAt == null ? undefined : new Date(issue.closedAt),
url: issue.url,
};
} catch (ex) {
return this.handleRequestError(ex, cc, undefined);
}
}
@debug<GitHubApi['getPullRequestForBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequestForBranch(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
branch: string,
options?: {
baseUrl?: string;
avatarSize?: number;
include?: GitHubPullRequestState[];
},
): Promise<PullRequest | undefined> {
const cc = Logger.getCorrelationContext();
interface QueryResult {
repository:
| {
refs: {
nodes: {
associatedPullRequests?: {
nodes?: GitHubPullRequest[];
};
}[];
};
}
| null
| undefined;
}
try {
const query = `query getPullRequestForBranch(
$owner: String!
$repo: String!
$branch: String!
$limit: Int!
$include: [PullRequestState!]
$avatarSize: Int
) {
repository(name: $repo, owner: $owner) {
refs(query: $branch, refPrefix: "refs/heads/", first: 1) {
nodes {
associatedPullRequests(first: $limit, orderBy: {field: UPDATED_AT, direction: DESC}, states: $include) {
nodes {
author {
login
avatarUrl(size: $avatarSize)
url
}
permalink
number
title
state
updatedAt
closedAt
mergedAt
repository {
isFork
owner {
login
}
}
}
}
}
}
}
}`;
const rsp = await this.graphql<QueryResult>(token, query, {
...options,
owner: owner,
repo: repo,
branch: branch,
// Since GitHub sort doesn't seem to really work, look for a max of 10 PRs and then sort them ourselves
limit: 10,
});
// If the pr is not from a fork, keep it e.g. show root pr's on forks, otherwise, ensure the repo owners match
const prs = rsp?.repository?.refs.nodes[0]?.associatedPullRequests?.nodes?.filter(
pr => !pr.repository.isFork || pr.repository.owner.login === owner,
);
if (prs == null || prs.length === 0) return undefined;
if (prs.length > 1) {
prs.sort(
(a, b) =>
(a.repository.owner.login === owner ? -1 : 1) - (b.repository.owner.login === owner ? -1 : 1) ||
(a.state === 'OPEN' ? -1 : 1) - (b.state === 'OPEN' ? -1 : 1) ||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
);
}
return GitHubPullRequest.from(prs[0], provider);
} catch (ex) {
return this.handleRequestError(ex, cc, undefined);
}
}
@debug<GitHubApi['getPullRequestForCommit']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequestForCommit(
provider: RichRemoteProvider,
token: string,
owner: string,
repo: string,
ref: string,
options?: {
baseUrl?: string;
avatarSize?: number;
},
): Promise<PullRequest | undefined> {
const cc = Logger.getCorrelationContext();
interface QueryResult {
repository:
| {
object?: {
associatedPullRequests?: {
nodes?: GitHubPullRequest[];
};
};
}
| null
| undefined;
}
try {
const query = `query getPullRequestForCommit(
$owner: String!
$repo: String!
$ref: GitObjectID!
$avatarSize: Int
) {
repository(name: $repo, owner: $owner) {
object(oid: $ref) {
... on Commit {
associatedPullRequests(first: 2, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
author {
login
avatarUrl(size: $avatarSize)
url
}
permalink
number
title
state
updatedAt
closedAt
mergedAt
repository {
isFork
owner {
login
}
}
}
}
}
}
}
}`;
const rsp = await this.graphql<QueryResult>(token, query, {
...options,
owner: owner,
repo: repo,
ref: ref,
});
// If the pr is not from a fork, keep it e.g. show root pr's on forks, otherwise, ensure the repo owners match
const prs = rsp?.repository?.object?.associatedPullRequests?.nodes?.filter(
pr => !pr.repository.isFork || pr.repository.owner.login === owner,
);
if (prs == null || prs.length === 0) return undefined;
if (prs.length > 1) {
prs.sort(
(a, b) =>
(a.repository.owner.login === owner ? -1 : 1) - (b.repository.owner.login === owner ? -1 : 1) ||
(a.state === 'OPEN' ? -1 : 1) - (b.state === 'OPEN' ? -1 : 1) ||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
);
}
return GitHubPullRequest.from(prs[0], provider);
} catch (ex) {
return this.handleRequestError(ex, cc, undefined);
}
}
private _octokits = new Map<string, Octokit>();
private octokit(token: string, options?: ConstructorParameters<typeof Octokit>[0]): Octokit {
let octokit = this._octokits.get(token);
if (octokit == null) {
let defaults;
if (isWeb) {
function fetchCore(url: string, options: { headers?: Record<string, string> }) {
if (options.headers != null) {
// Strip out the user-agent (since it causes warnings in a webworker)
const { 'user-agent': userAgent, ...headers } = options.headers;
if (userAgent) {
options.headers = headers;
}
}
return fetch(url, options);
}
defaults = Octokit.defaults({
auth: `token ${token}`,
request: { fetch: fetchCore },
});
} else {
defaults = Octokit.defaults({ auth: `token ${token}` });
}
octokit = new defaults(options);
this._octokits.set(token, octokit);
if (Logger.logLevel === LogLevel.Debug || Logger.isDebugging) {
octokit.hook.wrap('request', async (request, options) => {
const stopwatch = new Stopwatch(`[GITHUB] ${options.method} ${options.url}`, { log: false });
try {
return await request(options);
} finally {
let message;
try {
if (typeof options.query === 'string') {
const match = /(^[^({\n]+)/.exec(options.query);
message = ` ${match?.[1].trim() ?? options.query}`;
}
} catch {}
stopwatch.stop({ message: message });
}
});
}
}
return octokit;
}
private async graphql<T>(token: string, query: string, variables: { [key: string]: any }): Promise<T | undefined> {
try {
return await this.octokit(token).graphql<T>(query, variables);
} catch (ex) {
if (ex instanceof GraphqlResponseError) {
switch (ex.errors?.[0]?.type) {
case 'NOT_FOUND':
throw new ProviderRequestNotFoundError(ex);
case 'FORBIDDEN':
throw new AuthenticationError('github', AuthenticationErrorReason.Forbidden, ex);
}
}
throw ex;
}
}
private async request<R extends string>(
token: string,
route: keyof Endpoints | R,
options?: R extends keyof Endpoints ? Endpoints[R]['parameters'] & RequestParameters : RequestParameters,
): Promise<R extends keyof Endpoints ? Endpoints[R]['response'] : OctokitResponse<unknown>> {
try {
return (await this.octokit(token).request<R>(route, options)) as any;
} catch (ex) {
if (ex instanceof RequestError) {
switch (ex.status) {
case 404: // Not found
case 410: // Gone
throw new ProviderRequestNotFoundError(ex);
// case 429: //Too Many Requests
case 401: // Unauthorized
throw new AuthenticationError('github', AuthenticationErrorReason.Unauthorized, ex);
case 403: // Forbidden
throw new AuthenticationError('github', AuthenticationErrorReason.Forbidden, ex);
case 500: // Internal Server Error
if (ex.response != null) {
// TODO@eamodio: Handle GitHub down errors
}
break;
default:
if (ex.status >= 400 && ex.status < 500) throw new ProviderRequestClientError(ex);
break;
}
}
throw ex;
}
}
private handleRequestError<T>(ex: unknown | Error, cc: LogCorrelationContext | undefined, defaultValue: T): T {
if (ex instanceof ProviderRequestNotFoundError) return defaultValue;
Logger.error(ex, cc);
debugger;
throw ex;
}
}
interface GitHubIssueOrPullRequest {
type: IssueOrPullRequestType;
number: number;
createdAt: string;
closed: boolean;
closedAt: string | null;
title: string;
url: string;
}
type GitHubPullRequestState = 'OPEN' | 'CLOSED' | 'MERGED';
interface GitHubPullRequest {
author: {
login: string;
avatarUrl: string;
url: string;
};
permalink: string;
number: number;
title: string;
state: GitHubPullRequestState;
updatedAt: string;
closedAt: string | null;
mergedAt: string | null;
repository: {
isFork: boolean;
owner: {
login: string;
};
};
}
export namespace GitHubPullRequest {
export function from(pr: GitHubPullRequest, provider: RichRemoteProvider): PullRequest {
return new PullRequest(
provider,
{
name: pr.author.login,
avatarUrl: pr.author.avatarUrl,
url: pr.author.url,
},
String(pr.number),
pr.title,
pr.permalink,
fromState(pr.state),
new Date(pr.updatedAt),
pr.closedAt == null ? undefined : new Date(pr.closedAt),
pr.mergedAt == null ? undefined : new Date(pr.mergedAt),
);
}
export function fromState(state: GitHubPullRequestState): PullRequestState {
return state === 'MERGED'
? PullRequestState.Merged
: state === 'CLOSED'
? PullRequestState.Closed
: PullRequestState.Open;
}
export function toState(state: PullRequestState): GitHubPullRequestState {
return state === PullRequestState.Merged ? 'MERGED' : state === PullRequestState.Closed ? 'CLOSED' : 'OPEN';
}
}

+ 2083
- 0
src/premium/github/githubGitProvider.ts
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 88
- 0
src/premium/remotehub.ts ファイルの表示

@ -0,0 +1,88 @@
import { extensions, Uri } from 'vscode';
import { ExtensionNotFoundError } from '../errors';
import { Logger } from '../logger';
export async function getRemoteHubApi(): Promise<RemoteHubApi>;
export async function getRemoteHubApi(silent: false): Promise<RemoteHubApi>;
export async function getRemoteHubApi(silent: boolean): Promise<RemoteHubApi | undefined>;
export async function getRemoteHubApi(silent?: boolean): Promise<RemoteHubApi | undefined> {
try {
const extension =
extensions.getExtension<RemoteHubApi>('GitHub.remotehub') ??
extensions.getExtension<RemoteHubApi>('GitHub.remotehub-insiders');
if (extension == null) {
Logger.log('GitHub Repositories extension is not installed or enabled');
throw new ExtensionNotFoundError('GitHub Repositories', 'GitHub.remotehub');
}
const api = extension.isActive ? extension.exports : await extension.activate();
return api;
} catch (ex) {
Logger.error(ex, 'Unable to get required api from the GitHub Repositories extension');
debugger;
if (silent) return undefined;
throw ex;
}
}
export interface Provider {
readonly id: 'github' | 'azdo';
readonly name: string;
}
export enum HeadType {
Branch = 0,
RemoteBranch = 1,
Tag = 2,
Commit = 3,
}
export interface Metadata {
readonly provider: Provider;
readonly repo: { owner: string; name: string } & Record<string, unknown>;
getRevision(): Promise<{ type: HeadType; name: string; revision: string }>;
}
// export type CreateUriOptions = Omit<Metadata, 'provider' | 'branch'>;
export interface RemoteHubApi {
getMetadata(uri: Uri): Promise<Metadata | undefined>;
// createProviderUri(provider: string, options: CreateUriOptions, path: string): Uri | undefined;
getProvider(uri: Uri): Provider | undefined;
getProviderUri(uri: Uri): Uri;
getProviderRootUri(uri: Uri): Uri;
isProviderUri(uri: Uri, provider?: string): boolean;
// createVirtualUri(provider: string, options: CreateUriOptions, path: string): Uri | undefined;
getVirtualUri(uri: Uri): Uri;
getVirtualWorkspaceUri(uri: Uri): Uri | undefined;
/**
* Returns whether RemoteHub has the full workspace contents for a vscode-vfs:// URI.
* This will download workspace contents if fetching full workspace contents is enabled
* for the requested URI and the contents are not already available locally.
* @param workspaceUri A vscode-vfs:// URI for a RemoteHub workspace folder.
* @returns boolean indicating whether the workspace contents were successfully loaded.
*/
loadWorkspaceContents(workspaceUri: Uri): Promise<boolean>;
}
export interface RepositoryRef {
type: RepositoryRefType;
id: string;
}
export const enum RepositoryRefType {
Branch = 0,
Tag = 1,
Commit = 2,
PullRequest = 3,
Tree = 4,
}
export interface GitHubAuthorityMetadata {
v: 1;
ref?: RepositoryRef;
}

+ 5
- 2
src/repositories.ts ファイルの表示

@ -34,13 +34,16 @@ export function normalizeRepoUri(uri: Uri): { path: string; ignoreCase: boolean
return { path: path, ignoreCase: !isLinux };
case DocumentSchemes.Virtual:
case DocumentSchemes.GitHub:
case DocumentSchemes.GitHub: {
path = uri.path;
if (path.charCodeAt(path.length - 1) === slash) {
path = path.slice(0, -1);
}
return { path: uri.authority ? `${uri.authority}${path}` : path.slice(1), ignoreCase: false };
// TODO@eamodio Revisit this, as we can't strip off the authority details (e.g. metadata) ultimately (since you in theory could have a workspace with more than 1 virtual repo which are the same except for the authority)
const authority = uri.authority?.split('+', 1)[0];
return { path: authority ? `${authority}${path}` : path.slice(1), ignoreCase: false };
}
default:
path = uri.path;
if (path.charCodeAt(path.length - 1) === slash) {

+ 8
- 2
src/system/path.ts ファイルの表示

@ -1,6 +1,7 @@
import { basename, dirname } from 'path';
import { Uri } from 'vscode';
import { isLinux, isWindows } from '@env/platform';
import { DocumentSchemes } from '../constants';
// TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies
// import { CharCode } from './string';
@ -9,6 +10,7 @@ export { basename, dirname, extname, isAbsolute, join as joinPaths } from 'path'
const driveLetterNormalizeRegex = /(?<=^\/?)([A-Z])(?=:\/)/;
const pathNormalizeRegex = /\\/g;
const slash = 47; //slash;
const uriSchemeRegex = /^(\w[\w\d+.-]{1,}?):\/\//;
export function commonBase(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): string | undefined {
const index = commonBaseIndex(s1, s2, delimiter, ignoreCase);
@ -37,6 +39,10 @@ export function commonBaseIndex(s1: string, s2: string, delimiter: string, ignor
return index;
}
export function getBestPath(uri: Uri): string {
return uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path;
}
export function isChild(path: string, base: string | Uri): boolean;
export function isChild(uri: Uri, base: string | Uri): boolean;
export function isChild(pathOrUri: string | Uri, base: string | Uri): boolean {
@ -120,8 +126,8 @@ export function normalizePath(path: string): string {
}
export function relative(from: string, to: string, ignoreCase?: boolean): string {
from = normalizePath(from);
to = normalizePath(to);
from = uriSchemeRegex.test(from) ? Uri.parse(from, true).path : normalizePath(from);
to = uriSchemeRegex.test(to) ? Uri.parse(to, true).path : normalizePath(to);
const index = commonBaseIndex(`${to}/`, `${from}/`, '/', ignoreCase);
return index > 0 ? to.substring(index + 1) : to;

+ 12
- 6
src/views/nodes/fileHistoryNode.ts ファイルの表示

@ -11,7 +11,11 @@ import {
RepositoryFileSystemChangeEvent,
} from '../../git/models';
import { Logger } from '../../logger';
import { Arrays, debug, gate, Iterables, memoize } from '../../system';
import { uniqueBy } from '../../system/array';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { filterMap, flatMap } from '../../system/iterable';
import { basename, joinPaths } from '../../system/path';
import { FileHistoryView } from '../fileHistoryView';
import { CommitNode } from './commitNode';
@ -75,8 +79,8 @@ export class FileHistoryNode extends SubscribeableViewNode impl
if (fileStatuses?.length) {
if (this.folder) {
const commits = Arrays.uniqueBy(
[...Iterables.flatMap(fileStatuses, f => f.toPsuedoCommits(currentUser))],
const commits = uniqueBy(
[...flatMap(fileStatuses, f => f.toPsuedoCommits(currentUser))],
c => c.sha,
(original, c) => void original.files.push(...c.files),
);
@ -97,7 +101,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl
if (log != null) {
children.push(
...insertDateMarkers(
Iterables.map(log.commits.values(), c =>
filterMap(log.commits.values(), c =>
this.folder
? new CommitNode(
this.view as any,
@ -110,11 +114,13 @@ export class FileHistoryNode extends SubscribeableViewNode impl
expand: false,
},
)
: new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
: c.files.length
? new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
branch: this.branch,
getBranchAndTagTips: getBranchAndTagTips,
unpublished: unpublishedCommits?.has(c.ref),
}),
})
: undefined,
),
this,
),

+ 21
- 6
src/views/nodes/fileHistoryTrackerNode.ts ファイルの表示

@ -1,11 +1,13 @@
import { Disposable, FileType, TextEditor, TreeItem, TreeItemCollapsibleState, window, workspace } from 'vscode';
import { UriComparer } from '../../comparers';
import { ContextKeys, setContext } from '../../constants';
import { ContextKeys, DocumentSchemes, setContext } from '../../constants';
import { GitCommitish, GitUri } from '../../git/gitUri';
import { GitReference, GitRevision } from '../../git/models';
import { Logger } from '../../logger';
import { ReferencePicker } from '../../quickpicks';
import { debug, Functions, gate, log } from '../../system';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { debounce, Deferrable } from '../../system/function';
import { FileHistoryView } from '../fileHistoryView';
import { FileHistoryNode } from './fileHistoryNode';
import { ContextValues, SubscribeableViewNode, ViewNode } from './viewNode';
@ -219,17 +221,30 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode
@debug()
protected subscribe() {
return Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 250), this),
);
return Disposable.from(window.onDidChangeActiveTextEditor(debounce(this.onActiveEditorChanged, 250), this));
}
protected override etag(): number {
return 0;
}
private _triggerChangeDebounced: Deferrable<() => Promise<void>> | undefined;
@debug({ args: false })
private onActiveEditorChanged(_editor: TextEditor | undefined) {
private onActiveEditorChanged(editor: TextEditor | undefined) {
// If we are losing the active editor, give more time before assuming its really gone
// For virtual repositories the active editor event takes a while to fire
// Ultimately we need to be using the upcoming Tabs api to avoid this
if (
editor == null &&
(this._uri?.scheme === DocumentSchemes.Virtual || this._uri?.scheme === DocumentSchemes.GitHub)
) {
if (this._triggerChangeDebounced == null) {
this._triggerChangeDebounced = debounce(() => this.triggerChange(), 1500);
}
void this._triggerChangeDebounced();
return;
}
void this.triggerChange();
}

+ 13
- 10
src/views/nodes/lineHistoryNode.ts ファイルの表示

@ -14,7 +14,10 @@ import {
RepositoryFileSystemChangeEvent,
} from '../../git/models';
import { Logger } from '../../logger';
import { debug, gate, Iterables, memoize } from '../../system';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { filterMap } from '../../system/iterable';
import { FileHistoryView } from '../fileHistoryView';
import { LineHistoryView } from '../lineHistoryView';
import { LoadMoreNode, MessageNode } from './common';
@ -203,15 +206,15 @@ export class LineHistoryNode
if (log != null) {
children.push(
...insertDateMarkers(
Iterables.filterMap(
log.commits.values(),
c =>
new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
branch: this.branch,
getBranchAndTagTips: getBranchAndTagTips,
selection: selection,
unpublished: unpublishedCommits?.has(c.ref),
}),
filterMap(log.commits.values(), c =>
c.files.length
? new FileRevisionAsCommitNode(this.view, this, c.files[0], c, {
branch: this.branch,
getBranchAndTagTips: getBranchAndTagTips,
selection: selection,
unpublished: unpublishedCommits?.has(c.ref),
})
: undefined,
),
this,
),

+ 14
- 14
src/views/nodes/repositoriesNode.ts ファイルの表示

@ -2,7 +2,9 @@ import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, window } fr
import { RepositoriesChangeEvent } from '../../git/gitProviderService';
import { GitUri } from '../../git/gitUri';
import { Logger } from '../../logger';
import { debug, Functions, gate } from '../../system';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { debounce } from '../../system/function';
import { RepositoriesView } from '../repositoriesView';
import { MessageNode } from './common';
import { RepositoryNode } from './repositoryNode';
@ -23,7 +25,7 @@ export class RepositoriesNode extends SubscribeableViewNode {
@debug()
private resetChildren() {
if (this._children === undefined) return;
if (this._children == null) return;
for (const child of this._children) {
if (child instanceof RepositoryNode) {
@ -34,7 +36,7 @@ export class RepositoriesNode extends SubscribeableViewNode {
}
getChildren(): ViewNode[] {
if (this._children === undefined) {
if (this._children == null) {
const repositories = this.view.container.git.openRepositories;
if (repositories.length === 0) return [new MessageNode(this.view, this, 'No repositories could be found.')];
@ -54,7 +56,7 @@ export class RepositoriesNode extends SubscribeableViewNode {
@gate()
@debug()
override async refresh(reset: boolean = false) {
if (this._children === undefined) return;
if (this._children == null) return;
if (reset) {
this.resetChildren();
@ -65,7 +67,7 @@ export class RepositoriesNode extends SubscribeableViewNode {
}
const repositories = this.view.container.git.openRepositories;
if (repositories.length === 0 && (this._children === undefined || this._children.length === 0)) return;
if (repositories.length === 0 && (this._children == null || this._children.length === 0)) return;
if (repositories.length === 0) {
this._children = [new MessageNode(this.view, this, 'No repositories could be found.')];
@ -74,9 +76,9 @@ export class RepositoriesNode extends SubscribeableViewNode {
const children = [];
for (const repo of repositories) {
const normalizedPath = repo.normalizedPath;
const child = (this._children as RepositoryNode[]).find(c => c.repo.normalizedPath === normalizedPath);
if (child !== undefined) {
const id = repo.id;
const child = (this._children as RepositoryNode[]).find(c => c.repo.id === id);
if (child != null) {
children.push(child);
void child.refresh();
} else {
@ -100,9 +102,7 @@ export class RepositoriesNode extends SubscribeableViewNode {
const subscriptions = [this.view.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this)];
if (this.view.config.autoReveal) {
subscriptions.push(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
);
subscriptions.push(window.onDidChangeActiveTextEditor(debounce(this.onActiveEditorChanged, 500), this));
}
return Disposable.from(...subscriptions);
@ -114,7 +114,7 @@ export class RepositoriesNode extends SubscribeableViewNode {
@debug({ args: false })
private onActiveEditorChanged(editor: TextEditor | undefined) {
if (editor == null || this._children === undefined || this._children.length === 1) {
if (editor == null || this._children == null || this._children.length === 1) {
return;
}
@ -123,11 +123,11 @@ export class RepositoriesNode extends SubscribeableViewNode {
const node = this._children.find(n => n instanceof RepositoryNode && n.repo.containsUri(uri)) as
| RepositoryNode
| undefined;
if (node === undefined) return;
if (node == null) return;
// Check to see if this repo has a descendent that is already selected
let parent = this.view.selection.length === 0 ? undefined : this.view.selection[0];
while (parent !== undefined) {
while (parent != null) {
if (parent === node) return;
parent = parent.getParent();

+ 2
- 2
src/vsls/guest.ts ファイルの表示

@ -1,4 +1,4 @@
import { CancellationToken, Disposable, window, WorkspaceFolder } from 'vscode';
import { CancellationToken, Disposable, Uri, window, WorkspaceFolder } from 'vscode';
import type { LiveShare, SharedServiceProxy } from '../@types/vsls';
import { Container } from '../container';
import { GitCommandOptions } from '../git/commandOptions';
@ -80,7 +80,7 @@ export class VslsGuestService implements Disposable {
// TODO@eamodio add live share provider
undefined!,
folder,
r.path,
Uri.parse(r.uri),
r.root,
!window.state.focused,
r.closed,

+ 3
- 3
src/vsls/host.ts ファイルの表示

@ -216,12 +216,12 @@ export class VslsHostService implements Disposable {
const repos = [
...filterMap(this.container.git.repositories, r => {
if (!r.normalizedPath.startsWith(normalized)) return undefined;
if (!r.id.startsWith(normalized)) return undefined;
const vslsUri = this.convertLocalUriToShared(r.folder.uri);
const vslsUri = this.convertLocalUriToShared(r.folder?.uri ?? r.uri);
return {
folderUri: vslsUri.toString(true),
path: vslsUri.path,
uri: vslsUri.toString(),
root: r.root,
closed: r.closed,
};

+ 1
- 1
src/vsls/protocol.ts ファイルの表示

@ -19,7 +19,7 @@ export const GitCommandRequestType = new RequestType
export interface RepositoryProxy {
folderUri: string;
path: string;
uri: string;
root: boolean;
closed: boolean;
}

読み込み中…
キャンセル
保存