Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/api/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface RepositoryState {
readonly mergeChanges: Change[];
readonly indexChanges: Change[];
readonly workingTreeChanges: Change[];
readonly untrackedChanges?: Change[];

readonly onDidChange: Event<void>;
}
Expand Down Expand Up @@ -211,6 +212,8 @@ export interface Repository {
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise<void>;
pushRefWithLease?(remote: string, branch: string, newSha: string, expectedSha: string): Promise<void>;
getRemoteRefs?(remote: string, opts?: { heads?: boolean; tags?: boolean }): Promise<Ref[]>;

blame(path: string): Promise<string>;
log(options?: LogOptions): Promise<Commit[]>;
Expand All @@ -219,6 +222,10 @@ export interface Repository {
add(paths: string[]): Promise<void>;
merge(ref: string): Promise<void>;
mergeAbort(): Promise<void>;
rebase?(upstream: string, options?: { onto?: string; rebaseMerges?: boolean }): Promise<void>;
rebaseAbort?(): Promise<void>;
updateRef?(ref: string, newSha: string, oldSha: string): Promise<void>;
resetKeep?(ref: string): Promise<void>;

createWorktree?(options?: { path?: string; commitish?: string; branch?: string }): Promise<string>;
deleteWorktree?(path: string, options?: { force?: boolean }): Promise<void>;
Expand Down Expand Up @@ -253,6 +260,7 @@ export interface IGit {

registerPostCommitCommandsProvider?(provider: PostCommitCommandsProvider): Disposable;
getRepositoryWorkspace?(uri: Uri): Promise<Uri[] | null>;
openWorktreeRepository?(uri: Uri): Promise<Repository | null>;
clone?(uri: Uri, options?: CloneOptions): Promise<Uri | null>;
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export function registerCommands(
) {
const logId = 'RegisterCommands';

PullRequestOverviewPanel.registerGlobalCommands(context, telemetry);
PullRequestOverviewPanel.registerGlobalCommands(context, telemetry, reposManager);

context.subscriptions.push(
vscode.commands.registerCommand(
Expand Down
4 changes: 4 additions & 0 deletions src/gitProviders/builtinGit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class BuiltinGitProvider extends Disposable implements IGit {
return this._gitAPI.getRepositoryWorkspace(uri);
}

openWorktreeRepository(uri: vscode.Uri): Promise<Repository | null> {
return this._gitAPI.openRepository(uri);
}

clone(uri: vscode.Uri, options?: CloneOptions): Promise<vscode.Uri | null> {
return this._gitAPI.clone(uri, options);
}
Expand Down
8 changes: 8 additions & 0 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,14 @@ export class FolderRepositoryManager extends Disposable {
return this._repository;
}

async openWorktreeRepository(uri: vscode.Uri): Promise<Repository | null> {
const provider = this._git.getGitProvider(this._repository.rootUri);
if (!provider?.openWorktreeRepository) {
throw new Error(vscode.l10n.t('The Git provider cannot open a temporary worktree.'));
}
return provider.openWorktreeRepository(uri);
}

set repository(repository: Repository) {
this._repository = repository;
}
Expand Down
4 changes: 4 additions & 0 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,9 +832,13 @@ export interface PullRequestStackResponse {
url: string;
state: GithubItemStateEnum;
isDraft: boolean;
baseRefName: string;
baseRepository: { owner: { login: string } } | null;
headRefName: string;
headRepository: { owner: { login: string } } | null;
mergeable: 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN';
mergeStateStatus: 'BEHIND' | 'BLOCKED' | 'CLEAN' | 'DIRTY' | 'HAS_HOOKS' | 'UNKNOWN' | 'UNSTABLE';
mergeQueueEntry?: { state: string } | null;
};
}[];
pageInfo: { hasNextPage: boolean; endCursor: string | null };
Expand Down
20 changes: 20 additions & 0 deletions src/github/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface PullRequestStack {
position: number;
size: number;
base: string;
needsUpdate?: boolean;
pullRequests: {
position: number;
number: number;
Expand All @@ -35,6 +36,7 @@ export interface PullRequestStack {
head: string;
state: GithubItemStateEnum;
isDraft: boolean;
isQueued?: boolean;
mergeable: PullRequestMergeability;
}[];
}
Expand All @@ -47,6 +49,24 @@ export enum PullRequestMergeability {
Behind,
}

export function getUpdatableStackEntries(stack: PullRequestStack): PullRequestStack['pullRequests'] | undefined {
if (stack.pullRequests.length !== stack.size) {
return;
}
const firstClosed = stack.pullRequests.findIndex(entry => entry.state === GithubItemStateEnum.Closed);
const entries = firstClosed < 0 ? stack.pullRequests : stack.pullRequests.slice(0, firstClosed);
if (!entries.length || entries.some(entry => entry.state !== GithubItemStateEnum.Open || entry.isQueued
|| entry.mergeable === PullRequestMergeability.Conflict || entry.mergeable === PullRequestMergeability.Unknown)
|| stack.pullRequests.slice(entries.length).some(entry => entry.state !== GithubItemStateEnum.Closed)) {
return;
}
return entries;
}

export function isStackUpdatable(stack: PullRequestStack): boolean {
return !!stack.needsUpdate && !!getUpdatableStackEntries(stack);
}

export function isStackMergeable(stack: PullRequestStack, number: number): boolean {
const current = stack.pullRequests.find(entry => entry.number === number);
if (!current || current.position !== stack.position || current.state !== GithubItemStateEnum.Open
Expand Down
49 changes: 39 additions & 10 deletions src/github/pullRequestModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,7 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
const { query, remote, schema } = await this.githubRepository.ensure();
let stack: PullRequestStack | undefined;
let after: string | null = null;
const comparisons = new Map<number, { base: string; head: string }>();

while (true) {
let data: PullRequestStackResponse;
Expand Down Expand Up @@ -1757,22 +1758,50 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
position: pullRequest.stackEntry.position,
size: pullRequest.stack.size,
base: pullRequest.stack.baseRefName,
needsUpdate: false,
pullRequests: [],
};
stack.pullRequests.push(...pullRequest.stack.entries.nodes.map(entry => ({
position: entry.position,
number: entry.pullRequest.number,
title: entry.pullRequest.title,
url: entry.pullRequest.url,
head: entry.pullRequest.headRefName,
state: entry.pullRequest.state,
isDraft: entry.pullRequest.isDraft,
mergeable: parseMergeability(entry.pullRequest.mergeable, entry.pullRequest.mergeStateStatus),
})));
stack.pullRequests.push(...pullRequest.stack.entries.nodes.map(entry => {
const pr = entry.pullRequest;
if (pr.state === GithubItemStateEnum.Open) {
if (!pr.baseRepository || !pr.headRepository || !pr.baseRefName || !pr.headRefName) {
throw new Error(`Missing branch information for pull request #${pr.number} in this stack.`);
}
comparisons.set(pr.number, {
base: `${pr.baseRepository.owner.login}:${pr.baseRefName}`,
head: `${pr.headRepository.owner.login}:${pr.headRefName}`,
});
}
return {
position: entry.position,
number: pr.number,
title: pr.title,
url: pr.url,
head: pr.headRefName,
state: pr.state,
isDraft: pr.isDraft,
isQueued: !!pr.mergeQueueEntry,
mergeable: parseMergeability(pr.mergeable, pr.mergeStateStatus),
};
}));

const pageInfo = pullRequest.stack.entries.pageInfo;
if (!pageInfo.hasNextPage) {
stack.pullRequests.sort((a, b) => a.position - b.position);
const outdated = await Promise.all(stack.pullRequests.filter(entry =>
entry.state === GithubItemStateEnum.Open && entry.mergeable !== PullRequestMergeability.Conflict,
).map(async entry => {
const refs = comparisons.get(entry.number)!;
const comparison = await this.githubRepository.compareCommits(refs.base, refs.head);
if (comparison?.behind_by === undefined) {
throw new Error(`Unable to check whether pull request #${entry.number} is behind its stack base.`);
}
if (comparison.behind_by > 0) {
entry.mergeable = PullRequestMergeability.Behind;
}
return comparison.behind_by > 0;
}));
stack.needsUpdate = outdated.some(Boolean);
return stack;
}
if (!pageInfo.endCursor || pageInfo.endCursor === after) {
Expand Down
Loading
Loading