Merge pull request #3068 from github/koesie10/find-github-repository

Add finding of GitHub repositories in workspace
This commit is contained in:
Koen Vlaswinkel
2023-11-17 14:37:53 +01:00
committed by GitHub
6 changed files with 1026 additions and 2 deletions

View File

@@ -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": [

View 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",
}

View File

@@ -0,0 +1,49 @@
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> {
// Start the check and downloading the database asynchronously. We don't want to block on this
// in extension activation since this makes network requests and waits for user input.
void this.promptGitHubRepositoryDownload().catch((e: unknown) => {
const error = redactableError(
asError(e),
)`Failed to prompt for GitHub repository download`;
void this.app.logger.log(error.fullMessageWithStack);
this.app.telemetry?.sendError(error);
});
}
private async promptGitHubRepositoryDownload(): Promise<void> {
const githubRepositoryResult = await findGitHubRepositoryForWorkspace();
if (githubRepositoryResult.isFailure) {
void this.app.logger.log(
`Did not find a 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}'`,
);
}
}

View File

@@ -0,0 +1,182 @@
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<
ValueResult<GitExtensionAPI, string>
> {
const gitExtension = extensions.getExtension<GitExtension>("vscode.git");
if (!gitExtension) {
return ValueResult.fail(["Git extension not found"]);
}
if (!gitExtension.isActive) {
await gitExtension.activate();
}
const gitExtensionExports = gitExtension.exports;
if (!gitExtensionExports.enabled) {
return ValueResult.fail(["Git extension is not enabled"]);
}
const git = gitExtensionExports.getAPI(1);
if (git.state === "initialized") {
return ValueResult.ok(git);
}
return new Promise((resolve) => {
git.onDidChangeState((state) => {
if (state === "initialized") {
resolve(ValueResult.ok(git));
}
});
});
}
async function findRepositoryForWorkspaceFolder(
git: GitExtensionAPI,
workspaceFolderUri: Uri,
): Promise<Repository | undefined> {
return git.repositories.find(
(repo) => repo.rootUri.toString() === workspaceFolderUri.toString(),
);
}
/**
* Finds the primary remote fetch URL for a repository.
*
* The priority is:
* 1. The remote associated with the current branch
* 2. The remote named "origin"
* 3. The first remote
*
* If none of these are found, undefined is returned.
*
* This is just a heuristic. We may not find the correct remote in all cases,
* for example when the user has defined an alias in their SSH or Git config.
*
* @param repository The repository to find the remote for.
*/
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 has not yet retrieved
// the state for all Git repositories.
// This can happen on Codespaces where the Git extension is initialized before the
// filesystem is ready.
for (let count = 0; count < 5; count++) {
const remoteName = repository.state.HEAD?.upstream?.remote ?? "origin";
const upstreamRemoteUrl = repository.state.remotes.find(
(remote) => remote.name === remoteName,
)?.fetchUrl;
if (upstreamRemoteUrl) {
return upstreamRemoteUrl;
}
if (remoteName !== "origin") {
const originRemoteUrl = repository.state.remotes.find(
(remote) => remote.name === "origin",
)?.fetchUrl;
if (originRemoteUrl) {
return originRemoteUrl;
}
}
// Maybe they have a different remote that is not named origin and is not the
// upstream of the current branch. If so, just select the first one.
const firstRemoteUrl = repository.state.remotes[0]?.fetchUrl;
if (firstRemoteUrl) {
return firstRemoteUrl;
}
// Wait for 5 seconds before trying again.
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 gitResult = await getGitExtensionAPI();
if (gitResult.isFailure) {
return ValueResult.fail(gitResult.errors);
}
const git = gitResult.value;
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 });
}

View File

@@ -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,

View File

@@ -0,0 +1,352 @@
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: "fork",
},
},
remotes: [
{
name: "origin",
fetchUrl: "https://github.com/codeql/test-incorrect.git",
},
{
name: "fork",
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>>({
isActive: true,
exports: {
enabled: true,
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 not found"]),
);
});
});
describe("when the git extension is not activated", () => {
const activate = jest.fn();
beforeEach(() => {
getExtensionSpy.mockReturnValue(
mockedObject<Extension<GitExtension>>({
isActive: false,
activate,
exports: {
enabled: true,
getAPI: getAPISpy,
},
}),
);
});
it("returns the GitHub repository name with owner", async () => {
expect(await findGitHubRepositoryForWorkspace()).toEqual(
ValueResult.ok({
owner: "codeql",
name: "test",
}),
);
expect(activate).toHaveBeenCalledTimes(1);
});
});
describe("when the git extension is disabled by the setting", () => {
beforeEach(() => {
getExtensionSpy.mockReturnValue(
mockedObject<Extension<GitExtension>>({
isActive: true,
exports: {
enabled: false,
getAPI: getAPISpy,
},
}),
);
});
it("returns an error", async () => {
expect(await findGitHubRepositoryForWorkspace()).toEqual(
ValueResult.fail(["Git extension is not enabled"]),
);
expect(getAPISpy).not.toHaveBeenCalled();
});
});
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 but origin remote exists", () => {
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-incorrect.git",
},
{
name: "origin",
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 current branch does not have a remote and no origin 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",
},
{
name: "fork",
fetchUrl: "https://github.com/github/codeql-incorrect.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"]),
);
});
});
});