diff --git a/CHANGELOG.md b/CHANGELOG.md index dbadeb3..314fbbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Creates new worktrees from the "main" repo, if already in a worktree - Shows the _Worktrees_ view after creating a new worktree - Adds a new _remote_ command to the _Git Command Palette_ to add, prune, and remove remotes +- Adds a _Create Worktree for Pull Request via GitLens..._ context menu command on pull requests in the _GitHub Pull Requests and Issues_ extension's views ### Changed diff --git a/package.json b/package.json index b3c0e75..7a51d17 100644 --- a/package.json +++ b/package.json @@ -5460,6 +5460,11 @@ "icon": "$(add)" }, { + "command": "gitlens.ghpr.views.createWorktree", + "title": "Create Worktree for Pull Request via GitLens...", + "category": "GitLens" + }, + { "command": "gitlens.views.title.createWorktree", "title": "Create Worktree...", "category": "GitLens", @@ -7852,6 +7857,10 @@ "when": "false" }, { + "command": "gitlens.ghpr.views.createWorktree", + "when": "false" + }, + { "command": "gitlens.views.title.createWorktree", "when": "false" }, @@ -11236,6 +11245,11 @@ "command": "gitlens.views.setShowRelativeDateMarkersOff", "when": "viewItem == gitlens:date-marker && config.gitlens.views.showRelativeDateMarkers", "group": "1_gitlens@0" + }, + { + "command": "gitlens.ghpr.views.createWorktree", + "when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive|description/", + "group": "pullrequest@2" } ], "webview/context": [ diff --git a/src/commands.ts b/src/commands.ts index d8a3976..096250e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -16,6 +16,7 @@ export * from './commands/diffWithRevision'; export * from './commands/diffWithRevisionFrom'; export * from './commands/diffWithWorking'; export * from './commands/externalDiff'; +export * from './commands/ghpr/createWorktree'; export * from './commands/gitCommands'; export * from './commands/inviteToLiveShare'; export * from './commands/logging'; diff --git a/src/commands/ghpr/createWorktree.ts b/src/commands/ghpr/createWorktree.ts new file mode 100644 index 0000000..e1a087b --- /dev/null +++ b/src/commands/ghpr/createWorktree.ts @@ -0,0 +1,103 @@ +import type { Uri } from 'vscode'; +import { window } from 'vscode'; +import { Commands } from '../../constants'; +import type { Container } from '../../container'; +import { GitReference } from '../../git/models/reference'; +import type { GitRemote } from '../../git/models/remote'; +import { Logger } from '../../logger'; +import { command } from '../../system/command'; +import { waitUntilNextTick } from '../../system/promise'; +import { Command } from '../base'; +import { GitActions } from '../gitCommands.actions'; + +interface PullRequestNode { + readonly pullRequestModel: PullRequest; +} + +interface PullRequest { + readonly githubRepository: { + readonly rootUri: Uri; + }; + readonly head: { + readonly ref: string; + readonly sha: string; + readonly repositoryCloneUrl: { + readonly owner: string; + readonly url: Uri; + }; + }; + + readonly item: { + readonly number: number; + }; +} + +@command() +export class CreateWorktreeCommand extends Command { + constructor(private readonly container: Container) { + super(Commands.CreateWorktreeForGHPR); + } + + async execute(...args: [PullRequestNode | PullRequest, ...unknown[]]) { + const [arg] = args; + let pr; + if ('pullRequestModel' in arg) { + pr = arg.pullRequestModel; + } else { + pr = arg; + } + + const { + githubRepository: { rootUri }, + head: { + repositoryCloneUrl: { url: remoteUri, owner: remoteOwner }, + ref, + }, + } = pr; + + let repo = this.container.git.getRepository(rootUri); + if (repo == null) { + void window.showWarningMessage( + `Unable to find repository(${rootUri.toString()}) for PR #${pr.item.number}`, + ); + return; + } + + repo = await repo.getMainRepository(); + if (repo == null) { + void window.showWarningMessage( + `Unable to find main repository(${rootUri.toString()}) for PR #${pr.item.number}`, + ); + return; + } + + const remoteUrl = remoteUri.toString(); + + let remote: GitRemote | undefined; + [remote] = await repo.getRemotes({ filter: r => r.url === remoteUrl }); + if (remote == null) { + await GitActions.Remote.add(repo, remoteOwner, remoteUrl, { fetch: true }); + [remote] = await repo.getRemotes({ filter: r => r.url === remoteUrl }); + if (remote == null) return; + } else { + await this.container.git.fetch(repo.path, { remote: remote.name }); + } + + await waitUntilNextTick(); + + try { + await GitActions.Worktree.create( + repo, + undefined, + GitReference.create(`${remote.name}/${ref}`, repo.path, { + refType: 'branch', + name: `${remote.name}/${ref}`, + remote: true, + }), + ); + } catch (ex) { + Logger.error(ex, 'CreateWorktreeCommand', 'Unable to create worktree'); + void window.showErrorMessage(`Unable to create worktree for ${ref}`); + } + } +} diff --git a/src/constants.ts b/src/constants.ts index d09a538..672d99d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -229,6 +229,8 @@ export const enum Commands { ViewsOpenDirectoryDiff = 'gitlens.views.openDirectoryDiff', ViewsOpenDirectoryDiffWithWorking = 'gitlens.views.openDirectoryDiffWithWorking', + CreateWorktreeForGHPR = 'gitlens.ghpr.views.createWorktree', + Deprecated_DiffHeadWith = 'gitlens.diffHeadWith', Deprecated_DiffWorkingWith = 'gitlens.diffWorkingWith', Deprecated_OpenBranchesInRemote = 'gitlens.openBranchesInRemote', diff --git a/src/system/promise.ts b/src/system/promise.ts index c984a41..120ce61 100644 --- a/src/system/promise.ts +++ b/src/system/promise.ts @@ -250,7 +250,11 @@ export async function raceAll( } export async function wait(ms: number): Promise { - await new Promise(resolve => setTimeout(resolve, ms)); + await new Promise(resolve => setTimeout(resolve, ms)); +} + +export async function waitUntilNextTick(): Promise { + await new Promise(resolve => queueMicrotask(resolve)); } export class AggregateError extends Error {