Add finding of GitHub repositories in workspace
This commit is contained in:
@@ -21,7 +21,8 @@
|
||||
"Programming Languages"
|
||||
],
|
||||
"extensionDependencies": [
|
||||
"hbenl.vscode-test-explorer"
|
||||
"hbenl.vscode-test-explorer",
|
||||
"vscode.git"
|
||||
],
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
@@ -38,7 +39,8 @@
|
||||
"onWebviewPanel:resultsView",
|
||||
"onWebviewPanel:codeQL.variantAnalysis",
|
||||
"onWebviewPanel:codeQL.dataFlowPaths",
|
||||
"onFileSystem:codeql-zip-archive"
|
||||
"onFileSystem:codeql-zip-archive",
|
||||
"workspaceContains:.git"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
"files": [
|
||||
|
||||
436
extensions/ql-vscode/src/common/vscode/extension/git.ts
Normal file
436
extensions/ql-vscode/src/common/vscode/extension/git.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
// From https://github.com/microsoft/vscode/blob/5e27a2845a87be4b4bede3e51073f94609445e51/extensions/git/src/api/git.d.ts
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
Uri,
|
||||
Event,
|
||||
Disposable,
|
||||
ProviderResult,
|
||||
Command,
|
||||
CancellationToken,
|
||||
ThemeIcon,
|
||||
} from "vscode";
|
||||
|
||||
interface Git {
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
interface InputBox {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const enum ForcePushMode {
|
||||
Force,
|
||||
ForceWithLease,
|
||||
ForceWithLeaseIfIncludes,
|
||||
}
|
||||
|
||||
const enum RefType {
|
||||
Head,
|
||||
RemoteHead,
|
||||
Tag,
|
||||
}
|
||||
|
||||
interface Ref {
|
||||
readonly type: RefType;
|
||||
readonly name?: string;
|
||||
readonly commit?: string;
|
||||
readonly remote?: string;
|
||||
}
|
||||
|
||||
interface UpstreamRef {
|
||||
readonly remote: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
interface Branch extends Ref {
|
||||
readonly upstream?: UpstreamRef;
|
||||
readonly ahead?: number;
|
||||
readonly behind?: number;
|
||||
}
|
||||
|
||||
interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
readonly parents: string[];
|
||||
readonly authorDate?: Date;
|
||||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
readonly commitDate?: Date;
|
||||
}
|
||||
|
||||
interface Submodule {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
interface Remote {
|
||||
readonly name: string;
|
||||
readonly fetchUrl?: string;
|
||||
readonly pushUrl?: string;
|
||||
readonly isReadOnly: boolean;
|
||||
}
|
||||
|
||||
const enum Status {
|
||||
INDEX_MODIFIED,
|
||||
INDEX_ADDED,
|
||||
INDEX_DELETED,
|
||||
INDEX_RENAMED,
|
||||
INDEX_COPIED,
|
||||
|
||||
MODIFIED,
|
||||
DELETED,
|
||||
UNTRACKED,
|
||||
IGNORED,
|
||||
INTENT_TO_ADD,
|
||||
INTENT_TO_RENAME,
|
||||
TYPE_CHANGED,
|
||||
|
||||
ADDED_BY_US,
|
||||
ADDED_BY_THEM,
|
||||
DELETED_BY_US,
|
||||
DELETED_BY_THEM,
|
||||
BOTH_ADDED,
|
||||
BOTH_DELETED,
|
||||
BOTH_MODIFIED,
|
||||
}
|
||||
|
||||
interface Change {
|
||||
/**
|
||||
* Returns either `originalUri` or `renameUri`, depending
|
||||
* on whether this change is a rename change. When
|
||||
* in doubt always use `uri` over the other two alternatives.
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
readonly originalUri: Uri;
|
||||
readonly renameUri: Uri | undefined;
|
||||
readonly status: Status;
|
||||
}
|
||||
|
||||
interface RepositoryState {
|
||||
readonly HEAD: Branch | undefined;
|
||||
readonly refs: Ref[];
|
||||
readonly remotes: Remote[];
|
||||
readonly submodules: Submodule[];
|
||||
readonly rebaseCommit: Commit | undefined;
|
||||
|
||||
readonly mergeChanges: Change[];
|
||||
readonly indexChanges: Change[];
|
||||
readonly workingTreeChanges: Change[];
|
||||
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
interface RepositoryUIState {
|
||||
readonly selected: boolean;
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log options.
|
||||
*/
|
||||
interface LogOptions {
|
||||
/** Max number of log entries to retrieve. If not specified, the default is 32. */
|
||||
readonly maxEntries?: number;
|
||||
readonly path?: string;
|
||||
/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
|
||||
readonly range?: string;
|
||||
readonly reverse?: boolean;
|
||||
readonly sortByAuthorDate?: boolean;
|
||||
}
|
||||
|
||||
interface CommitOptions {
|
||||
all?: boolean | "tracked";
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
empty?: boolean;
|
||||
noVerify?: boolean;
|
||||
requireUserConfig?: boolean;
|
||||
useEditor?: boolean;
|
||||
verbose?: boolean;
|
||||
/**
|
||||
* string - execute the specified command after the commit operation
|
||||
* undefined - execute the command specified in git.postCommitCommand
|
||||
* after the commit operation
|
||||
* null - do not execute any command after the commit operation
|
||||
*/
|
||||
postCommitCommand?: string | null;
|
||||
}
|
||||
|
||||
interface FetchOptions {
|
||||
remote?: string;
|
||||
ref?: string;
|
||||
all?: boolean;
|
||||
prune?: boolean;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
interface InitOptions {
|
||||
defaultBranch?: string;
|
||||
}
|
||||
|
||||
interface RefQuery {
|
||||
readonly contains?: string;
|
||||
readonly count?: number;
|
||||
readonly pattern?: string;
|
||||
readonly sort?: "alphabetically" | "committerdate";
|
||||
}
|
||||
|
||||
interface BranchQuery extends RefQuery {
|
||||
readonly remote?: boolean;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
readonly rootUri: Uri;
|
||||
readonly inputBox: InputBox;
|
||||
readonly state: RepositoryState;
|
||||
readonly ui: RepositoryUIState;
|
||||
|
||||
getConfigs(): Promise<Array<{ key: string; value: string }>>;
|
||||
getConfig(key: string): Promise<string>;
|
||||
setConfig(key: string, value: string): Promise<string>;
|
||||
getGlobalConfig(key: string): Promise<string>;
|
||||
|
||||
getObjectDetails(
|
||||
treeish: string,
|
||||
path: string,
|
||||
): Promise<{ mode: string; object: string; size: number }>;
|
||||
detectObjectType(
|
||||
object: string,
|
||||
): Promise<{ mimetype: string; encoding?: string }>;
|
||||
buffer(ref: string, path: string): Promise<Buffer>;
|
||||
show(ref: string, path: string): Promise<string>;
|
||||
getCommit(ref: string): Promise<Commit>;
|
||||
|
||||
add(paths: string[]): Promise<void>;
|
||||
revert(paths: string[]): Promise<void>;
|
||||
clean(paths: string[]): Promise<void>;
|
||||
|
||||
apply(patch: string, reverse?: boolean): Promise<void>;
|
||||
diff(cached?: boolean): Promise<string>;
|
||||
diffWithHEAD(): Promise<Change[]>;
|
||||
diffWithHEAD(path: string): Promise<string>;
|
||||
diffWith(ref: string): Promise<Change[]>;
|
||||
diffWith(ref: string, path: string): Promise<string>;
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWith(ref: string): Promise<Change[]>;
|
||||
diffIndexWith(ref: string, path: string): Promise<string>;
|
||||
diffBlobs(object1: string, object2: string): Promise<string>;
|
||||
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
|
||||
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
|
||||
|
||||
hashObject(data: string): Promise<string>;
|
||||
|
||||
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
|
||||
deleteBranch(name: string, force?: boolean): Promise<void>;
|
||||
getBranch(name: string): Promise<Branch>;
|
||||
getBranches(
|
||||
query: BranchQuery,
|
||||
cancellationToken?: CancellationToken,
|
||||
): Promise<Ref[]>;
|
||||
getBranchBase(name: string): Promise<Branch | undefined>;
|
||||
setBranchUpstream(name: string, upstream: string): Promise<void>;
|
||||
|
||||
getRefs(
|
||||
query: RefQuery,
|
||||
cancellationToken?: CancellationToken,
|
||||
): Promise<Ref[]>;
|
||||
|
||||
getMergeBase(ref1: string, ref2: string): Promise<string>;
|
||||
|
||||
tag(name: string, upstream: string): Promise<void>;
|
||||
deleteTag(name: string): Promise<void>;
|
||||
|
||||
status(): Promise<void>;
|
||||
checkout(treeish: string): Promise<void>;
|
||||
|
||||
addRemote(name: string, url: string): Promise<void>;
|
||||
removeRemote(name: string): Promise<void>;
|
||||
renameRemote(name: string, newName: string): Promise<void>;
|
||||
|
||||
fetch(options?: FetchOptions): Promise<void>;
|
||||
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
|
||||
pull(unshallow?: boolean): Promise<void>;
|
||||
push(
|
||||
remoteName?: string,
|
||||
branchName?: string,
|
||||
setUpstream?: boolean,
|
||||
force?: ForcePushMode,
|
||||
): Promise<void>;
|
||||
|
||||
blame(path: string): Promise<string>;
|
||||
log(options?: LogOptions): Promise<Commit[]>;
|
||||
|
||||
commit(message: string, opts?: CommitOptions): Promise<void>;
|
||||
}
|
||||
|
||||
interface RemoteSource {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly url: string | string[];
|
||||
}
|
||||
|
||||
interface RemoteSourceProvider {
|
||||
readonly name: string;
|
||||
readonly icon?: string; // codicon name
|
||||
readonly supportsQuery?: boolean;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
getBranches?(url: string): ProviderResult<string[]>;
|
||||
publishRepository?(repository: Repository): Promise<void>;
|
||||
}
|
||||
|
||||
interface RemoteSourcePublisher {
|
||||
readonly name: string;
|
||||
readonly icon?: string; // codicon name
|
||||
publishRepository(repository: Repository): Promise<void>;
|
||||
}
|
||||
|
||||
interface Credentials {
|
||||
readonly username: string;
|
||||
readonly password: string;
|
||||
}
|
||||
|
||||
interface CredentialsProvider {
|
||||
getCredentials(host: Uri): ProviderResult<Credentials>;
|
||||
}
|
||||
|
||||
interface PostCommitCommandsProvider {
|
||||
getCommands(repository: Repository): Command[];
|
||||
}
|
||||
|
||||
interface PushErrorHandler {
|
||||
handlePushError(
|
||||
repository: Repository,
|
||||
remote: Remote,
|
||||
refspec: string,
|
||||
error: Error & { gitErrorCode: GitErrorCodes },
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface BranchProtection {
|
||||
readonly remote: string;
|
||||
readonly rules: BranchProtectionRule[];
|
||||
}
|
||||
|
||||
interface BranchProtectionRule {
|
||||
readonly include?: string[];
|
||||
readonly exclude?: string[];
|
||||
}
|
||||
|
||||
interface BranchProtectionProvider {
|
||||
onDidChangeBranchProtection: Event<Uri>;
|
||||
provideBranchProtection(): BranchProtection[];
|
||||
}
|
||||
|
||||
interface CommitMessageProvider {
|
||||
readonly title: string;
|
||||
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
|
||||
provideCommitMessage(
|
||||
repository: Repository,
|
||||
changes: string[],
|
||||
cancellationToken?: CancellationToken,
|
||||
): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
type APIState = "uninitialized" | "initialized";
|
||||
|
||||
interface PublishEvent {
|
||||
repository: Repository;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
export interface API {
|
||||
readonly state: APIState;
|
||||
readonly onDidChangeState: Event<APIState>;
|
||||
readonly onDidPublish: Event<PublishEvent>;
|
||||
readonly git: Git;
|
||||
readonly repositories: Repository[];
|
||||
readonly onDidOpenRepository: Event<Repository>;
|
||||
readonly onDidCloseRepository: Event<Repository>;
|
||||
|
||||
toGitUri(uri: Uri, ref: string): Uri;
|
||||
getRepository(uri: Uri): Repository | null;
|
||||
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
|
||||
openRepository(root: Uri): Promise<Repository | null>;
|
||||
|
||||
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||
registerPostCommitCommandsProvider(
|
||||
provider: PostCommitCommandsProvider,
|
||||
): Disposable;
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
|
||||
registerBranchProtectionProvider(
|
||||
root: Uri,
|
||||
provider: BranchProtectionProvider,
|
||||
): Disposable;
|
||||
registerCommitMessageProvider(provider: CommitMessageProvider): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
readonly enabled: boolean;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
|
||||
/**
|
||||
* Returns a specific API version.
|
||||
*
|
||||
* Throws error if git extension is disabled. You can listen to the
|
||||
* [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
|
||||
* to know when the extension becomes enabled/disabled.
|
||||
*
|
||||
* @param version Version number.
|
||||
* @returns API instance
|
||||
*/
|
||||
getAPI(version: 1): API;
|
||||
}
|
||||
|
||||
const enum GitErrorCodes {
|
||||
BadConfigFile = "BadConfigFile",
|
||||
AuthenticationFailed = "AuthenticationFailed",
|
||||
NoUserNameConfigured = "NoUserNameConfigured",
|
||||
NoUserEmailConfigured = "NoUserEmailConfigured",
|
||||
NoRemoteRepositorySpecified = "NoRemoteRepositorySpecified",
|
||||
NotAGitRepository = "NotAGitRepository",
|
||||
NotAtRepositoryRoot = "NotAtRepositoryRoot",
|
||||
Conflict = "Conflict",
|
||||
StashConflict = "StashConflict",
|
||||
UnmergedChanges = "UnmergedChanges",
|
||||
PushRejected = "PushRejected",
|
||||
ForcePushWithLeaseRejected = "ForcePushWithLeaseRejected",
|
||||
ForcePushWithLeaseIfIncludesRejected = "ForcePushWithLeaseIfIncludesRejected",
|
||||
RemoteConnectionError = "RemoteConnectionError",
|
||||
DirtyWorkTree = "DirtyWorkTree",
|
||||
CantOpenResource = "CantOpenResource",
|
||||
GitNotFound = "GitNotFound",
|
||||
CantCreatePipe = "CantCreatePipe",
|
||||
PermissionDenied = "PermissionDenied",
|
||||
CantAccessRemote = "CantAccessRemote",
|
||||
RepositoryNotFound = "RepositoryNotFound",
|
||||
RepositoryIsLocked = "RepositoryIsLocked",
|
||||
BranchNotFullyMerged = "BranchNotFullyMerged",
|
||||
NoRemoteReference = "NoRemoteReference",
|
||||
InvalidBranchName = "InvalidBranchName",
|
||||
BranchAlreadyExists = "BranchAlreadyExists",
|
||||
NoLocalChanges = "NoLocalChanges",
|
||||
NoStashFound = "NoStashFound",
|
||||
LocalChangesOverwritten = "LocalChangesOverwritten",
|
||||
NoUpstreamBranch = "NoUpstreamBranch",
|
||||
IsInSubmodule = "IsInSubmodule",
|
||||
WrongCase = "WrongCase",
|
||||
CantLockRef = "CantLockRef",
|
||||
CantRebaseMultipleBranches = "CantRebaseMultipleBranches",
|
||||
PatchDoesNotApply = "PatchDoesNotApply",
|
||||
NoPathFound = "NoPathFound",
|
||||
UnknownPath = "UnknownPath",
|
||||
EmptyCommitMessage = "EmptyCommitMessage",
|
||||
BranchFastForwardRejected = "BranchFastForwardRejected",
|
||||
BranchNotYetBorn = "BranchNotYetBorn",
|
||||
TagConflict = "TagConflict",
|
||||
}
|
||||
46
extensions/ql-vscode/src/databases/github-database-module.ts
Normal file
46
extensions/ql-vscode/src/databases/github-database-module.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
import { App } from "../common/app";
|
||||
import { findGitHubRepositoryForWorkspace } from "./github-repository-finder";
|
||||
import { redactableError } from "../common/errors";
|
||||
import { asError } from "../common/helpers-pure";
|
||||
|
||||
export class GithubDatabaseModule extends DisposableObject {
|
||||
private constructor(private readonly app: App) {
|
||||
super();
|
||||
}
|
||||
|
||||
public static async initialize(app: App): Promise<GithubDatabaseModule> {
|
||||
const githubDatabaseModule = new GithubDatabaseModule(app);
|
||||
app.subscriptions.push(githubDatabaseModule);
|
||||
|
||||
await githubDatabaseModule.initialize();
|
||||
return githubDatabaseModule;
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
void this.promptGitHubRepositoryDownload().catch((e: unknown) => {
|
||||
this.app.telemetry?.sendError(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Failed to prompt for GitHub repository download`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async promptGitHubRepositoryDownload(): Promise<void> {
|
||||
const githubRepositoryResult = await findGitHubRepositoryForWorkspace();
|
||||
if (githubRepositoryResult.isFailure) {
|
||||
void this.app.logger.log(
|
||||
`Failed to find GitHub repository for workspace: ${githubRepositoryResult.errors.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const githubRepository = githubRepositoryResult.value;
|
||||
void this.app.logger.log(
|
||||
`Found GitHub repository for workspace: '${githubRepository.owner}/${githubRepository.name}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
139
extensions/ql-vscode/src/databases/github-repository-finder.ts
Normal file
139
extensions/ql-vscode/src/databases/github-repository-finder.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
API as GitExtensionAPI,
|
||||
GitExtension,
|
||||
Repository,
|
||||
} from "../common/vscode/extension/git";
|
||||
import { extensions, Uri } from "vscode";
|
||||
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
|
||||
import { ValueResult } from "../common/value-result";
|
||||
|
||||
// Based on https://github.com/microsoft/sarif-vscode-extension/blob/a1740e766122c1759d9f39d580c18b82d9e0dea4/src/extension/index.activateGithubAnalyses.ts
|
||||
|
||||
async function getGitExtensionAPI(): Promise<GitExtensionAPI | undefined> {
|
||||
const gitExtension =
|
||||
extensions.getExtension<GitExtension>("vscode.git")?.exports;
|
||||
if (!gitExtension) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const git = gitExtension.getAPI(1);
|
||||
if (git.state === "initialized") {
|
||||
return git;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
git.onDidChangeState((state) => {
|
||||
if (state === "initialized") {
|
||||
resolve(git);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findRepositoryForWorkspaceFolder(
|
||||
git: GitExtensionAPI,
|
||||
workspaceFolderUri: Uri,
|
||||
): Promise<Repository | undefined> {
|
||||
return git.repositories.find(
|
||||
(repo) => repo.rootUri.toString() === workspaceFolderUri.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
async function findRemote(repository: Repository): Promise<string | undefined> {
|
||||
// Try to retrieve the remote 5 times with a 5 second delay between each attempt.
|
||||
// This is to account for the case where the Git extension is still initializing.
|
||||
for (let count = 0; count < 5; count++) {
|
||||
const remoteName = repository.state.HEAD?.upstream?.remote ?? "origin";
|
||||
const originRemoteUrl = repository.state.remotes.find(
|
||||
(remote) => remote.name === remoteName,
|
||||
)?.fetchUrl;
|
||||
if (originRemoteUrl) {
|
||||
return originRemoteUrl;
|
||||
}
|
||||
|
||||
const firstRemoteUrl = repository.state.remotes[0]?.fetchUrl;
|
||||
if (firstRemoteUrl) {
|
||||
return firstRemoteUrl;
|
||||
}
|
||||
|
||||
// Wait for Git to initialize.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Example: https://github.com/github/vscode-codeql.git
|
||||
const githubHTTPSRegex =
|
||||
/https:\/\/github\.com\/(?<owner>[^/]+)\/(?<name>[^/]+)/;
|
||||
|
||||
// Example: git@github.com:github/vscode-codeql.git
|
||||
const githubSSHRegex = /git@github\.com:(?<owner>[^/]+)\/(?<name>[^/]+)/;
|
||||
|
||||
function findGitHubRepositoryForRemote(remoteUrl: string):
|
||||
| {
|
||||
owner: string;
|
||||
name: string;
|
||||
}
|
||||
| undefined {
|
||||
const match =
|
||||
remoteUrl.match(githubHTTPSRegex) ?? remoteUrl.match(githubSSHRegex);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const owner = match.groups?.owner;
|
||||
let name = match.groups?.name;
|
||||
|
||||
if (!owner || !name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If a repository ends with ".git", remove it.
|
||||
if (name.endsWith(".git")) {
|
||||
name = name.slice(0, -4);
|
||||
}
|
||||
|
||||
return {
|
||||
owner,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findGitHubRepositoryForWorkspace(): Promise<
|
||||
ValueResult<{ owner: string; name: string }, string>
|
||||
> {
|
||||
const git = await getGitExtensionAPI();
|
||||
if (!git) {
|
||||
return ValueResult.fail(["Git extension is not installed or initialized"]);
|
||||
}
|
||||
|
||||
const primaryWorkspaceFolder = getOnDiskWorkspaceFoldersObjects()[0]?.uri;
|
||||
if (!primaryWorkspaceFolder) {
|
||||
return ValueResult.fail(["No workspace folder found"]);
|
||||
}
|
||||
|
||||
const primaryRepository = await findRepositoryForWorkspaceFolder(
|
||||
git,
|
||||
primaryWorkspaceFolder,
|
||||
);
|
||||
if (!primaryRepository) {
|
||||
return ValueResult.fail([
|
||||
"No Git repository found in primary workspace folder",
|
||||
]);
|
||||
}
|
||||
|
||||
const remoteUrl = await findRemote(primaryRepository);
|
||||
if (!remoteUrl) {
|
||||
return ValueResult.fail(["No remote found"]);
|
||||
}
|
||||
|
||||
const repoNwo = findGitHubRepositoryForRemote(remoteUrl);
|
||||
if (!repoNwo) {
|
||||
return ValueResult.fail(["Remote is not a GitHub repository"]);
|
||||
}
|
||||
|
||||
const { owner, name } = repoNwo;
|
||||
|
||||
return ValueResult.ok({ owner, name });
|
||||
}
|
||||
@@ -137,6 +137,7 @@ import { QueriesModule } from "./queries-panel/queries-module";
|
||||
import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider";
|
||||
import { LanguageContextStore } from "./language-context-store";
|
||||
import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel";
|
||||
import { GithubDatabaseModule } from "./databases/github-database-module";
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -870,6 +871,8 @@ async function activateWithInstalledDistribution(
|
||||
),
|
||||
);
|
||||
|
||||
await GithubDatabaseModule.initialize(app);
|
||||
|
||||
void extLogger.log("Initializing query history.");
|
||||
const queryHistoryDirs: QueryHistoryDirs = {
|
||||
localQueriesDirPath: queryStorageDir,
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Extension, extensions, Uri } from "vscode";
|
||||
import * as workspaceFolders from "../../../../src/common/vscode/workspace-folders";
|
||||
import {
|
||||
GitExtension,
|
||||
API as GitExtensionAPI,
|
||||
} from "../../../../src/common/vscode/extension/git";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
import { findGitHubRepositoryForWorkspace } from "../../../../src/databases/github-repository-finder";
|
||||
import { ValueResult } from "../../../../src/common/value-result";
|
||||
|
||||
describe("findGitHubRepositoryForWorkspace", () => {
|
||||
let mockGitExtensionAPI: GitExtensionAPI;
|
||||
|
||||
let getOnDiskWorkspaceFolderObjectsSpy: jest.SpiedFunction<
|
||||
typeof workspaceFolders.getOnDiskWorkspaceFoldersObjects
|
||||
>;
|
||||
let getExtensionSpy: jest.SpiedFunction<typeof extensions.getExtension>;
|
||||
const getAPISpy: jest.MockedFunction<GitExtension["getAPI"]> = jest.fn();
|
||||
|
||||
const repositories = [
|
||||
{
|
||||
rootUri: Uri.file("a/b/c"),
|
||||
state: {
|
||||
HEAD: {
|
||||
name: "main",
|
||||
upstream: {
|
||||
name: "origin",
|
||||
remote: "origin",
|
||||
},
|
||||
},
|
||||
remotes: [
|
||||
{
|
||||
name: "origin",
|
||||
fetchUrl: "https://github.com/codeql/test.git",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockGitExtensionAPI = mockedObject<GitExtensionAPI>({
|
||||
state: "initialized",
|
||||
repositories,
|
||||
});
|
||||
|
||||
getOnDiskWorkspaceFolderObjectsSpy = jest.spyOn(
|
||||
workspaceFolders,
|
||||
"getOnDiskWorkspaceFoldersObjects",
|
||||
);
|
||||
getExtensionSpy = jest.spyOn(extensions, "getExtension");
|
||||
|
||||
getOnDiskWorkspaceFolderObjectsSpy.mockReturnValue([
|
||||
{
|
||||
name: "workspace1",
|
||||
uri: Uri.file("/a/b/c"),
|
||||
index: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
getExtensionSpy.mockReturnValue(
|
||||
mockedObject<Extension<GitExtension>>({
|
||||
exports: {
|
||||
getAPI: getAPISpy,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
getAPISpy.mockReturnValue(mockGitExtensionAPI);
|
||||
});
|
||||
|
||||
it("returns the GitHub repository name with owner", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.ok({
|
||||
owner: "codeql",
|
||||
name: "test",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("when the git extension is not installed or disabled", () => {
|
||||
beforeEach(() => {
|
||||
getExtensionSpy.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("returns an error", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.fail(["Git extension is not installed or initialized"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the git extension is not yet initialized", () => {
|
||||
beforeEach(() => {
|
||||
const onDidChangeState = jest.fn();
|
||||
|
||||
onDidChangeState.mockImplementation((callback) => {
|
||||
callback("initialized");
|
||||
});
|
||||
|
||||
mockGitExtensionAPI = mockedObject<GitExtensionAPI>({
|
||||
state: "uninitialized",
|
||||
onDidChangeState,
|
||||
repositories,
|
||||
});
|
||||
|
||||
getAPISpy.mockReturnValue(mockGitExtensionAPI);
|
||||
});
|
||||
|
||||
it("returns the GitHub repository name with owner", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.ok({
|
||||
owner: "codeql",
|
||||
name: "test",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are no workspace folders", () => {
|
||||
beforeEach(() => {
|
||||
getOnDiskWorkspaceFolderObjectsSpy.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("returns an error", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.fail(["No workspace folder found"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the workspace folder does not match a Git repository", () => {
|
||||
beforeEach(() => {
|
||||
getOnDiskWorkspaceFolderObjectsSpy.mockReturnValue([
|
||||
{
|
||||
name: "workspace1",
|
||||
uri: Uri.file("/a/b/d"),
|
||||
index: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an error", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.fail([
|
||||
"No Git repository found in primary workspace folder",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the current branch does not have a remote", () => {
|
||||
beforeEach(() => {
|
||||
mockGitExtensionAPI = mockedObject<GitExtensionAPI>({
|
||||
state: "initialized",
|
||||
repositories: [
|
||||
{
|
||||
...repositories[0],
|
||||
state: {
|
||||
...repositories[0].state,
|
||||
HEAD: {
|
||||
...repositories[0].state.HEAD,
|
||||
upstream: undefined,
|
||||
},
|
||||
remotes: [
|
||||
{
|
||||
name: "upstream",
|
||||
fetchUrl: "https://github.com/github/codeql.git",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
getAPISpy.mockReturnValue(mockGitExtensionAPI);
|
||||
});
|
||||
|
||||
it("returns the GitHub repository name with owner", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.ok({
|
||||
owner: "github",
|
||||
name: "codeql",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the remote is an SSH GitHub URL", () => {
|
||||
beforeEach(() => {
|
||||
mockGitExtensionAPI = mockedObject<GitExtensionAPI>({
|
||||
state: "initialized",
|
||||
repositories: [
|
||||
{
|
||||
...repositories[0],
|
||||
state: {
|
||||
...repositories[0].state,
|
||||
remotes: [
|
||||
{
|
||||
name: "origin",
|
||||
fetchUrl: "git@github.com:github/codeql.git",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
getAPISpy.mockReturnValue(mockGitExtensionAPI);
|
||||
});
|
||||
|
||||
it("returns the GitHub repository name with owner", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.ok({
|
||||
owner: "github",
|
||||
name: "codeql",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the remote does not match a GitHub repository", () => {
|
||||
beforeEach(() => {
|
||||
mockGitExtensionAPI = mockedObject<GitExtensionAPI>({
|
||||
state: "initialized",
|
||||
repositories: [
|
||||
{
|
||||
...repositories[0],
|
||||
state: {
|
||||
...repositories[0].state,
|
||||
remotes: [
|
||||
{
|
||||
name: "origin",
|
||||
fetchUrl: "https://example.com/codeql/test.git",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
getAPISpy.mockReturnValue(mockGitExtensionAPI);
|
||||
});
|
||||
|
||||
it("returns an error", async () => {
|
||||
expect(await findGitHubRepositoryForWorkspace()).toEqual(
|
||||
ValueResult.fail(["Remote is not a GitHub repository"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user