Skip to content

Commit 2124448

Browse files
authored
Add url handling for checking out a PR (#8070)
* Add url handling for checking out a PR * Copilot review
1 parent b0b51eb commit 2124448

File tree

8 files changed

+225
-18
lines changed

8 files changed

+225
-18
lines changed

src/@types/git.d.ts

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode';
6+
import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken, SourceControlHistoryItem } from 'vscode';
77
export { ProviderResult } from 'vscode';
88

99
export interface Git {
@@ -16,7 +16,8 @@ export interface InputBox {
1616

1717
export const enum ForcePushMode {
1818
Force,
19-
ForceWithLease
19+
ForceWithLease,
20+
ForceWithLeaseIfIncludes,
2021
}
2122

2223
export const enum RefType {
@@ -29,12 +30,14 @@ export interface Ref {
2930
readonly type: RefType;
3031
readonly name?: string;
3132
readonly commit?: string;
33+
readonly commitDetails?: Commit;
3234
readonly remote?: string;
3335
}
3436

3537
export interface UpstreamRef {
3638
readonly remote: string;
3739
readonly name: string;
40+
readonly commit?: string;
3841
}
3942

4043
export interface Branch extends Ref {
@@ -43,6 +46,12 @@ export interface Branch extends Ref {
4346
readonly behind?: number;
4447
}
4548

49+
export interface CommitShortStat {
50+
readonly files: number;
51+
readonly insertions: number;
52+
readonly deletions: number;
53+
}
54+
4655
export interface Commit {
4756
readonly hash: string;
4857
readonly message: string;
@@ -51,6 +60,7 @@ export interface Commit {
5160
readonly authorName?: string;
5261
readonly authorEmail?: string;
5362
readonly commitDate?: Date;
63+
readonly shortStat?: CommitShortStat;
5464
}
5565

5666
export interface Submodule {
@@ -78,6 +88,8 @@ export const enum Status {
7888
UNTRACKED,
7989
IGNORED,
8090
INTENT_TO_ADD,
91+
INTENT_TO_RENAME,
92+
TYPE_CHANGED,
8193

8294
ADDED_BY_US,
8395
ADDED_BY_THEM,
@@ -103,13 +115,15 @@ export interface Change {
103115

104116
export interface RepositoryState {
105117
readonly HEAD: Branch | undefined;
118+
readonly refs: Ref[];
106119
readonly remotes: Remote[];
107120
readonly submodules: Submodule[];
108121
readonly rebaseCommit: Commit | undefined;
109122

110123
readonly mergeChanges: Change[];
111124
readonly indexChanges: Change[];
112125
readonly workingTreeChanges: Change[];
126+
readonly untrackedChanges: Change[];
113127

114128
readonly onDidChange: Event<void>;
115129
}
@@ -126,6 +140,16 @@ export interface LogOptions {
126140
/** Max number of log entries to retrieve. If not specified, the default is 32. */
127141
readonly maxEntries?: number;
128142
readonly path?: string;
143+
/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
144+
readonly range?: string;
145+
readonly reverse?: boolean;
146+
readonly sortByAuthorDate?: boolean;
147+
readonly shortStats?: boolean;
148+
readonly author?: string;
149+
readonly grep?: string;
150+
readonly refNames?: string[];
151+
readonly maxParents?: number;
152+
readonly skip?: number;
129153
}
130154

131155
export interface CommitOptions {
@@ -155,10 +179,27 @@ export interface FetchOptions {
155179
depth?: number;
156180
}
157181

182+
export interface InitOptions {
183+
defaultBranch?: string;
184+
}
185+
186+
export interface CloneOptions {
187+
parentPath?: Uri;
188+
/**
189+
* ref is only used if the repository cache is missed.
190+
*/
191+
ref?: string;
192+
recursive?: boolean;
193+
/**
194+
* If no postCloneAction is provided, then the users setting for git.openAfterClone is used.
195+
*/
196+
postCloneAction?: 'none';
197+
}
198+
158199
export interface RefQuery {
159200
readonly contains?: string;
160201
readonly count?: number;
161-
readonly pattern?: string;
202+
readonly pattern?: string | string[];
162203
readonly sort?: 'alphabetically' | 'committerdate';
163204
}
164205

@@ -173,9 +214,13 @@ export interface Repository {
173214
readonly state: RepositoryState;
174215
readonly ui: RepositoryUIState;
175216

217+
readonly onDidCommit: Event<void>;
218+
readonly onDidCheckout: Event<void>;
219+
176220
getConfigs(): Promise<{ key: string; value: string; }[]>;
177221
getConfig(key: string): Promise<string>;
178222
setConfig(key: string, value: string): Promise<string>;
223+
unsetConfig(key: string): Promise<string>;
179224
getGlobalConfig(key: string): Promise<string>;
180225

181226
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
@@ -211,9 +256,11 @@ export interface Repository {
211256
getBranchBase(name: string): Promise<Branch | undefined>;
212257
setBranchUpstream(name: string, upstream: string): Promise<void>;
213258

259+
checkIgnore(paths: string[]): Promise<Set<string>>;
260+
214261
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
215262

216-
getMergeBase(ref1: string, ref2: string): Promise<string>;
263+
getMergeBase(ref1: string, ref2: string): Promise<string | undefined>;
217264

218265
tag(name: string, upstream: string): Promise<void>;
219266
deleteTag(name: string): Promise<void>;
@@ -236,6 +283,10 @@ export interface Repository {
236283
commit(message: string, opts?: CommitOptions): Promise<void>;
237284
merge(ref: string): Promise<void>;
238285
mergeAbort(): Promise<void>;
286+
287+
applyStash(index?: number): Promise<void>;
288+
popStash(index?: number): Promise<void>;
289+
dropStash(index?: number): Promise<void>;
239290
}
240291

241292
export interface RemoteSource {
@@ -276,6 +327,38 @@ export interface PushErrorHandler {
276327
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
277328
}
278329

330+
export interface BranchProtection {
331+
readonly remote: string;
332+
readonly rules: BranchProtectionRule[];
333+
}
334+
335+
export interface BranchProtectionRule {
336+
readonly include?: string[];
337+
readonly exclude?: string[];
338+
}
339+
340+
export interface BranchProtectionProvider {
341+
onDidChangeBranchProtection: Event<Uri>;
342+
provideBranchProtection(): BranchProtection[];
343+
}
344+
345+
export interface AvatarQueryCommit {
346+
readonly hash: string;
347+
readonly authorName?: string;
348+
readonly authorEmail?: string;
349+
}
350+
351+
export interface AvatarQuery {
352+
readonly commits: AvatarQueryCommit[];
353+
readonly size: number;
354+
}
355+
356+
export interface SourceControlHistoryItemDetailsProvider {
357+
provideAvatar(repository: Repository, query: AvatarQuery): ProviderResult<Map<string, string | undefined>>;
358+
provideHoverCommands(repository: Repository): ProviderResult<Command[]>;
359+
provideMessageLinks(repository: Repository, message: string): ProviderResult<string>;
360+
}
361+
279362
export type APIState = 'uninitialized' | 'initialized';
280363

281364
export interface PublishEvent {
@@ -294,14 +377,24 @@ export interface GitAPI {
294377

295378
toGitUri(uri: Uri, ref: string): Uri;
296379
getRepository(uri: Uri): Repository | null;
297-
init(root: Uri): Promise<Repository | null>;
298-
openRepository(root: Uri): Promise<Repository | null>
380+
getRepositoryRoot(uri: Uri): Promise<Uri | null>;
381+
getRepositoryWorkspace(uri: Uri): Promise<Uri[] | null>;
382+
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
383+
/**
384+
* Checks the cache of known cloned repositories, and clones if the repository is not found.
385+
* Make sure to pass `postCloneAction` 'none' if you want to have the uri where you can find the repository returned.
386+
* @returns The URI of a folder or workspace file which, when opened, will open the cloned repository.
387+
*/
388+
clone(uri: Uri, options?: CloneOptions): Promise<Uri | null>;
389+
openRepository(root: Uri): Promise<Repository | null>;
299390

300391
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
301392
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
302393
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
303394
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
304395
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
396+
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
397+
registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable;
305398
}
306399

307400
export interface GitExtension {
@@ -319,21 +412,25 @@ export interface GitExtension {
319412
* @param version Version number.
320413
* @returns API instance
321414
*/
322-
getAPI(version: 1): GitAPI;
415+
getAPI(version: 1): API;
323416
}
324417

325418
export const enum GitErrorCodes {
326419
BadConfigFile = 'BadConfigFile',
420+
BadRevision = 'BadRevision',
327421
AuthenticationFailed = 'AuthenticationFailed',
328422
NoUserNameConfigured = 'NoUserNameConfigured',
329423
NoUserEmailConfigured = 'NoUserEmailConfigured',
330424
NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
331425
NotAGitRepository = 'NotAGitRepository',
426+
NotASafeGitRepository = 'NotASafeGitRepository',
332427
NotAtRepositoryRoot = 'NotAtRepositoryRoot',
333428
Conflict = 'Conflict',
334429
StashConflict = 'StashConflict',
335430
UnmergedChanges = 'UnmergedChanges',
336431
PushRejected = 'PushRejected',
432+
ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected',
433+
ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected',
337434
RemoteConnectionError = 'RemoteConnectionError',
338435
DirtyWorkTree = 'DirtyWorkTree',
339436
CantOpenResource = 'CantOpenResource',
@@ -361,5 +458,10 @@ export const enum GitErrorCodes {
361458
EmptyCommitMessage = 'EmptyCommitMessage',
362459
BranchFastForwardRejected = 'BranchFastForwardRejected',
363460
BranchNotYetBorn = 'BranchNotYetBorn',
364-
TagConflict = 'TagConflict'
461+
TagConflict = 'TagConflict',
462+
CherryPickEmpty = 'CherryPickEmpty',
463+
CherryPickConflict = 'CherryPickConflict',
464+
WorktreeContainsChanges = 'WorktreeContainsChanges',
465+
WorktreeAlreadyExists = 'WorktreeAlreadyExists',
466+
WorktreeBranchAlreadyUsed = 'WorktreeBranchAlreadyUsed'
365467
}

src/api/api.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export interface Repository {
188188
setBranchUpstream(name: string, upstream: string): Promise<void>;
189189
getRefs?(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]>; // Optional, because Remote Hub doesn't support this
190190

191-
getMergeBase(ref1: string, ref2: string): Promise<string>;
191+
getMergeBase(ref1: string, ref2: string): Promise<string | undefined>;
192192

193193
status(): Promise<void>;
194194
checkout(treeish: string): Promise<void>;
@@ -239,6 +239,7 @@ export interface IGit {
239239
readonly onDidPublish?: Event<PublishEvent>;
240240

241241
registerPostCommitCommandsProvider?(provider: PostCommitCommandsProvider): Disposable;
242+
getRepositoryWorkspace?(uri: Uri): Promise<Uri[] | null>;
242243
}
243244

244245
export interface TitleAndDescriptionProvider {

src/api/api1.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ export class GitApiImpl extends Disposable implements API, IGit {
8484
super();
8585
}
8686

87+
async getRepositoryWorkspace(uri: vscode.Uri): Promise<vscode.Uri[] | null> {
88+
for (const [, provider] of this._providers) {
89+
if (provider.getRepositoryWorkspace) {
90+
return provider.getRepositoryWorkspace(uri);
91+
}
92+
}
93+
return null;
94+
}
95+
96+
8797
public get repositories(): Repository[] {
8898
const ret: Repository[] = [];
8999

src/common/uri.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ function validateOpenWebviewParams(owner?: string, repo?: string, number?: strin
636636
export enum UriHandlerPaths {
637637
OpenIssueWebview = '/open-issue-webview',
638638
OpenPullRequestWebview = '/open-pull-request-webview',
639+
CheckoutPullRequest = '/checkout-pull-request'
639640
}
640641

641642
export interface OpenIssueWebviewUriParams {
@@ -676,11 +677,11 @@ export async function toOpenPullRequestWebviewUri(params: OpenPullRequestWebview
676677
return vscode.env.asExternalUri(vscode.Uri.from({ scheme: vscode.env.uriScheme, authority: EXTENSION_ID, path: UriHandlerPaths.OpenPullRequestWebview, query }));
677678
}
678679

679-
export function fromOpenPullRequestWebviewUri(uri: vscode.Uri): OpenPullRequestWebviewUriParams | undefined {
680+
export function fromOpenOrCheckoutPullRequestWebviewUri(uri: vscode.Uri): OpenPullRequestWebviewUriParams | undefined {
680681
if (compareIgnoreCase(uri.authority, EXTENSION_ID) !== 0) {
681682
return;
682683
}
683-
if (uri.path !== UriHandlerPaths.OpenPullRequestWebview) {
684+
if (uri.path !== UriHandlerPaths.OpenPullRequestWebview && uri.path !== UriHandlerPaths.CheckoutPullRequest) {
684685
return;
685686
}
686687
try {

src/extension.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { NotificationsFeatureRegister } from './notifications/notificationsFeatu
3939
import { NotificationsManager } from './notifications/notificationsManager';
4040
import { NotificationsProvider } from './notifications/notificationsProvider';
4141
import { ThemeWatcher } from './themeWatcher';
42-
import { UriHandler } from './uriHandler';
42+
import { resumePendingCheckout, UriHandler } from './uriHandler';
4343
import { CommentDecorationProvider } from './view/commentDecorationProvider';
4444
import { CompareChanges } from './view/compareChangesTreeDataProvider';
4545
import { CreatePullRequestHelper } from './view/createPullRequestHelper';
@@ -267,8 +267,11 @@ async function init(
267267

268268
registerPostCommitCommandsProvider(reposManager, git);
269269

270+
// Resume any pending checkout request stored before workspace reopened.
271+
await resumePendingCheckout(context, reposManager);
272+
270273
initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel);
271-
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context)));
274+
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context, git)));
272275

273276
// Make sure any compare changes tabs, which come from the create flow, are closed.
274277
CompareChanges.closeTabs();

src/gitProviders/builtinGit.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export class BuiltinGitProvider extends Disposable implements IGit {
4646
this._register(this._gitAPI.onDidChangeState(e => this._onDidChangeState.fire(e)));
4747
this._register(this._gitAPI.onDidPublish(e => this._onDidPublish.fire(e)));
4848
}
49+
getRepositoryWorkspace(uri: vscode.Uri): Promise<vscode.Uri[] | null> {
50+
return this._gitAPI.getRepositoryWorkspace(uri);
51+
}
4952

5053
static async createProvider(): Promise<BuiltinGitProvider | undefined> {
5154
const extension = vscode.extensions.getExtension<GitExtension>('vscode.git');

src/gitProviders/vslsguest.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode';
77

88
import { LiveShare, SharedServiceProxy } from 'vsls/vscode.js';
9-
import { Branch, Change, Commit, Remote, RepositoryState, Submodule } from '../@types/git';
9+
import { Branch, Change, Commit, Ref, Remote, RepositoryState, Submodule } from '../@types/git';
1010
import { IGit, Repository } from '../api/api';
1111
import { Disposable } from '../common/lifecycle';
1212
import {
@@ -149,6 +149,8 @@ class LiveShareRepositoryState implements RepositoryState {
149149
this.HEAD = state.HEAD;
150150
this.remotes = state.remotes;
151151
}
152+
refs: Ref[] = [];
153+
untrackedChanges: Change[] = [];
152154

153155
public update(state: RepositoryState) {
154156
this.HEAD = state.HEAD;

0 commit comments

Comments
 (0)