-
Notifications
You must be signed in to change notification settings - Fork 176
Expand file tree
/
Copy pathrepository.ts
More file actions
322 lines (261 loc) · 9.23 KB
/
repository.ts
File metadata and controls
322 lines (261 loc) · 9.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import * as vscode from "vscode";
import {Octokit} from "@octokit/rest";
import {canReachGitHubAPI} from "../api/canReachGitHubAPI";
import {handleSamlError} from "../api/handleSamlError";
import {getSession} from "../auth/auth";
import {getRemoteName, useEnterprise} from "../configuration/configuration";
import {Protocol} from "../external/protocol";
import {logDebug, logError} from "../log";
import {API, GitExtension, RefType, RepositoryState} from "../typings/git";
import {RepositoryPermission, getRepositoryPermission} from "./repository-permissions";
import {getGitHubApiUri} from "../configuration/configuration";
interface GitHubUrls {
workspaceUri: vscode.Uri;
url: string;
protocol: Protocol;
}
async function getGitExtension(): Promise<API | undefined> {
const gitExtension = vscode.extensions.getExtension<GitExtension>("vscode.git");
if (gitExtension) {
if (!gitExtension.isActive) {
await gitExtension.activate();
}
const git = gitExtension.exports.getAPI(1);
if (git.state !== "initialized") {
// Wait for the plugin to be initialized
await new Promise<void>(resolve => {
if (git.state === "initialized") {
resolve();
} else {
const listener = git.onDidChangeState(state => {
if (state === "initialized") {
resolve();
}
listener.dispose();
});
}
});
}
return git;
}
}
export async function getGitHead(): Promise<string | undefined> {
const git = await getGitExtension();
if (git && git.repositories.length > 0) {
const head = git.repositories[0].state.HEAD;
if (head && head.name && head.type === RefType.Head) {
return `refs/heads/${head.name}`;
}
}
}
export async function getGitHubUrls(): Promise<GitHubUrls[] | null> {
const git = await getGitExtension();
if (git && git.repositories.length > 0) {
logDebug("Found git extension");
const remoteName = getRemoteName();
const p = await Promise.all(
git.repositories.map(async r => {
logDebug("Find `origin` remote for repository", r.rootUri.path);
await r.status();
// Try to get "origin" remote first
let remote = r.state.remotes.filter(remote => remote.name === remoteName);
// If "origin" does not exist, automatically get another remote
if (r.state.remotes.length !== 0 && remote.length === 0) {
remote = [r.state.remotes[0]];
}
if (
remote.length > 0 &&
(remote[0].pushUrl?.indexOf("github.com") !== -1 ||
(useEnterprise() && remote[0].pushUrl?.indexOf(new URL(getGitHubApiUri()).host) !== -1))
) {
const url = remote[0].pushUrl;
return {
workspaceUri: r.rootUri,
url,
protocol: new Protocol(url as string)
};
}
logDebug(`Remote "${remoteName}" not found, skipping repository`);
return undefined;
})
);
return p.filter(x => !!x) as GitHubUrls[];
}
// If we cannot find the git extension, assume for now that we are running a web context,
// for instance, github.dev. I think ideally we'd check the workspace URIs first, but this
// works for now. We'll revisit later.
// if (!git) {
// Support for virtual workspaces
const isVirtualWorkspace =
vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.every(f => f.uri.scheme !== "file");
if (isVirtualWorkspace) {
logDebug("Found virtual workspace");
const ghFolder = vscode.workspace.workspaceFolders?.find(
x => x.uri.scheme === "vscode-vfs" && x.uri.authority === "github"
);
if (ghFolder) {
logDebug("Found virtual GitHub workspace folder");
const url = `https://github.com/${ghFolder.uri.path}`;
const urls: [GitHubUrls] = [
{
workspaceUri: ghFolder.uri,
url: url,
protocol: new Protocol(url)
}
];
return urls;
}
}
return null;
}
export interface GitHubRepoContext {
client: Octokit;
repositoryState: RepositoryState | undefined;
workspaceUri: vscode.Uri;
id: number;
owner: string;
name: string;
organizationOwned: boolean;
defaultBranch: string;
permissionLevel: RepositoryPermission;
}
export interface GitHubContext {
repos: GitHubRepoContext[];
reposByUri: Map<string, GitHubRepoContext>;
reposByOwnerAndName: Map<string, GitHubRepoContext>;
username: string;
}
let gitHubContext: Promise<GitHubContext | undefined> | undefined;
export async function getGitHubContext(): Promise<GitHubContext | undefined> {
if (gitHubContext) {
return gitHubContext;
}
if (!(await canReachGitHubAPI())) {
logError(new Error("Cannot fetch github context"));
return undefined;
}
try {
const git = await getGitExtension();
const allProtocolInfos = await getGitHubUrls();
// Filter out wiki repositories because the GET call will fail and throw an error
const protocolInfos = allProtocolInfos?.filter(info => !info.protocol.repositoryName.match(/\.wiki$/));
if (!protocolInfos) {
logDebug("Could not get protocol infos");
return undefined;
}
logDebug("Found protocol infos", protocolInfos.length.toString());
const session = await getSession();
if (!session) {
// User is not signed in, getSession will prompt them to sign in
return undefined;
}
const username = session.account.label;
const repos = await handleSamlError(session, async (client: Octokit) => {
return await Promise.all(
protocolInfos.map(async (protocolInfo): Promise<GitHubRepoContext> => {
logDebug("Getting infos for repository", protocolInfo.url);
const repoInfo = await client.repos.get({
repo: protocolInfo.protocol.repositoryName,
owner: protocolInfo.protocol.owner
});
const repo = git && git.getRepository(protocolInfo.workspaceUri);
return {
workspaceUri: protocolInfo.workspaceUri,
client,
repositoryState: repo?.state,
name: protocolInfo.protocol.repositoryName,
owner: protocolInfo.protocol.owner,
id: repoInfo.data.id,
defaultBranch: `refs/heads/${repoInfo.data.default_branch}`,
organizationOwned: repoInfo.data.owner.type === "Organization",
permissionLevel: getRepositoryPermission(repoInfo.data.permissions)
};
})
);
});
gitHubContext = Promise.resolve({
repos,
reposByUri: new Map(repos.map(r => [r.workspaceUri.toString(), r])),
reposByOwnerAndName: new Map(repos.map(r => [`${r.owner}/${r.name}`.toLocaleLowerCase(), r])),
username
});
} catch (e) {
// Reset the context so the next attempt will try this flow again
gitHubContext = undefined;
logError(e as Error, "Error getting GitHub context");
// Rethrow original error
throw e;
}
return gitHubContext;
}
export function resetGitHubContext() {
gitHubContext = undefined;
}
export async function getGitHubContextForRepo(owner: string, name: string): Promise<GitHubRepoContext | undefined> {
const gitHubContext = await getGitHubContext();
if (!gitHubContext) {
return undefined;
}
const searchKey = `${owner}/${name}`.toLocaleLowerCase();
return gitHubContext.reposByOwnerAndName.get(searchKey);
}
export async function getGitHubContextForWorkspaceUri(
workspaceUri: vscode.Uri
): Promise<GitHubRepoContext | undefined> {
const gitHubContext = await getGitHubContext();
if (!gitHubContext) {
return undefined;
}
return gitHubContext.reposByUri.get(workspaceUri.toString());
}
export async function getGitHubContextForDocumentUri(documentUri: vscode.Uri): Promise<GitHubRepoContext | undefined> {
const gitHubContext = await getGitHubContext();
if (!gitHubContext) {
return undefined;
}
const workspaceUri = vscode.workspace.getWorkspaceFolder(documentUri);
if (!workspaceUri) {
return;
}
return getGitHubContextForWorkspaceUri(workspaceUri.uri);
}
export function getCurrentBranch(state: RepositoryState | undefined): string | undefined {
if (!state) {
return;
}
const head = state.HEAD;
if (!head) {
return;
}
if (head.type != RefType.Head) {
return;
}
return head.name;
}
/**
* Get the Git repository folder URI
* @param startUri The starting URI to search from
* @returns The URI of the Git repository folder, or undefined if not found
*/
export async function getGitRepositoryFolderUri(startUri: vscode.Uri): Promise<vscode.Uri | undefined> {
let currentPath = startUri;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const gitPath = vscode.Uri.joinPath(currentPath, ".git");
// Check if .git exists
await vscode.workspace.fs.stat(gitPath);
// If we reach here, .git exists, so return the current path
return currentPath;
} catch (error) {
// .git doesn't exist, move up to the parent directory
const parentPath = vscode.Uri.joinPath(currentPath, "..");
// Check if we've reached the root
if (parentPath.toString() === currentPath.toString()) {
// We've reached the root without finding a .git folder
return undefined;
}
currentPath = parentPath;
}
}
}