Skip to content

Commit

Permalink
Enable updating branch for non-checkout PRs (#5836)
Browse files Browse the repository at this point in the history
and for vscode.dev
Part of #5802
  • Loading branch information
alexr00 authored Mar 15, 2024
1 parent 14219bd commit 9cd8816
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2139,6 +2139,11 @@ export class FolderRepositoryManager implements vscode.Disposable {
return;
}

if (!pullRequest.isActive ||
((pullRequest.item.mergeable !== PullRequestMergeability.Conflict) && (vscode.env.appHost === 'vscode.dev' || vscode.env.appHost === 'github.dev'))) {
return pullRequest.updateBranch();
}

if (this.repository.state.workingTreeChanges.length > 0 || this.repository.state.indexChanges.length > 0) {
await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when the there changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true });
return;
Expand Down
13 changes: 13 additions & 0 deletions src/github/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ export interface IRawFileChange {
patch: string | undefined;
}

export interface IRawFileContent {
type: string;
size: number;
name: string;
path: string;
content?: string | undefined;
sha: string;
url: string;
git_url: string | null;
html_url: string | null;
download_url: string | null;
}

export interface IPullRequestsPagingOptions {
fetchNextPage: boolean;
fetchOnePagePerRepo?: boolean;
Expand Down
103 changes: 102 additions & 1 deletion src/github/pullRequestModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import gql from 'graphql-tag';
import * as vscode from 'vscode';
import { Repository } from '../api/api';
import { DiffSide, IComment, IReviewThread, SubjectType, ViewedState } from '../common/comment';
import { parseDiff } from '../common/diffHunk';
import { getModifiedContentFromDiffHunk, parseDiff } from '../common/diffHunk';
import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file';
import { GitHubRef } from '../common/githubRef';
import Logger from '../common/logger';
Expand Down Expand Up @@ -53,6 +53,7 @@ import {
GithubItemStateEnum,
IAccount,
IRawFileChange,
IRawFileContent,
ISuggestedReviewer,
ITeam,
MergeMethod,
Expand Down Expand Up @@ -785,6 +786,106 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
}
}

private async getUpdateBranchFiles(head: GitHubRef, base: GitHubRef, baseCommitSha: string): Promise<IRawFileContent[]> {
const { octokit, remote } = await this.githubRepository.ensure();

// Get the files that would change as part of the merge
const compareData = await restPaginate<typeof octokit.api.repos.compareCommits, OctokitCommon.ReposCompareCommitsResponseData>(octokit.api.repos.compareCommits, {
repo: remote.repositoryName,
owner: head.owner,
base: `${head.owner}:${head.ref}`, // flip base and head because we are comparing for a merge to update the PR
head: `${base.owner}:${baseCommitSha}`,
});

const files: IRawFileContent[] = (await Promise.all(compareData.map(data => data.files).flat().map(async (file) => {
if (!file) {
return;
}
const { data: baseData }: { data: IRawFileContent } = await octokit.call(octokit.api.repos.getContent, {
owner: base.owner,
repo: remote.repositoryName,
path: file?.filename,
ref: baseCommitSha
}) as { data: IRawFileContent };
if ((!file.previous_filename || !this._fileChanges.has(file.previous_filename)) && !this._fileChanges.has(file.filename)) {
// File is not part of the PR, so we can use the content from the base branch
return baseData;
} else {
// File is part of the PR. We have to apply the patch of the base to the head content.
// NOTE THAT CONFLICTS ARE NOT YET HANDLED!!
const { data: headData }: { data: IRawFileContent } = await octokit.call(octokit.api.repos.getContent, {
owner: head.owner,
repo: remote.repositoryName,
path: file?.previous_filename ?? file.filename,
ref: head.ref
}) as { data: IRawFileContent };

if (file.status === 'modified' && file.patch && headData.content) {
const buff = buffer.Buffer.from(headData.content, 'base64');
const asString = new TextDecoder().decode(buff);
const modifiedContent = getModifiedContentFromDiffHunk(asString, file.patch);
const asBuff = new TextEncoder().encode(modifiedContent);
baseData.content = asBuff.toString();
} else {
// What would cause a modified file to not have a patch?
Logger.error(`File ${file.filename} has status ${file.status} and no patch`, GitHubRepository.ID);
// We don't want to commit something that's going to break, so throw
throw new Error(`File ${file.filename} has status ${file.status} and no patch`);
}
return baseData;
}
}))).filter<IRawFileContent>((file): file is IRawFileContent => file !== undefined);
return files;
}

/**
* Should only be called if there are no conflicts!!
*/
async updateBranch(): Promise<void> {
if (this.item.mergeable === PullRequestMergeability.Conflict) {
Logger.error('Cannot update a branch with conflicts', GitHubRepository.ID);
throw new Error('Cannot update a branch with conflicts');
}

const head = this.head;
const base = this.base;
if (!head) {
return;
}
Logger.debug(`Updating branch ${head.ref} to ${base.ref} - enter`, GitHubRepository.ID);
try {
const { octokit, remote } = await this.githubRepository.ensure();
const baseCommitSha = (await octokit.call(octokit.api.repos.getBranch, { owner: remote.owner, repo: remote.repositoryName, branch: base.ref })).data.commit.sha;

const lastCommitSha = (await octokit.call(octokit.api.repos.getBranch, { owner: head.owner, repo: remote.repositoryName, branch: head.ref })).data.commit.sha;
const lastTreeSha = (await octokit.call(octokit.api.repos.getCommit, { owner: head.owner, repo: remote.repositoryName, ref: lastCommitSha })).data.commit.tree.sha;

const files: IRawFileContent[] = await this.getUpdateBranchFiles(head, base, baseCommitSha);

const treeItems: { path: string, mode: '100644', content: string }[] = [];
for (const file of files) {
if (file.content) {
const buff = buffer.Buffer.from(file.content, 'base64');
treeItems.push({ path: file.path, mode: '100644', content: buff.toString() });
}
}

const newTreeSha = (await octokit.call(octokit.api.git.createTree, { owner: head.owner, repo: remote.repositoryName, base_tree: lastTreeSha, tree: treeItems })).data.sha;
let message: string;
if (base.owner === head.owner) {
message = `Merge branch \`${base.ref}\` into ${head.ref}`;
} else {
message = `Merge branch \`${base.owner}:${base.ref}\` into ${head.ref}`;
}
const newCommitSha = (await octokit.call(octokit.api.git.createCommit, { owner: head.owner, repo: remote.repositoryName, message, tree: newTreeSha, parents: [lastCommitSha, baseCommitSha] })).data.sha;
await octokit.call(octokit.api.git.updateRef, { owner: head.owner, repo: remote.repositoryName, ref: `heads/${head.ref}`, sha: newCommitSha });

} catch (e) {
Logger.error(`Updating branch ${head.ref} to ${base.ref} failed: ${e}`, GitHubRepository.ID);
}
Logger.debug(`Updating branch ${head.ref} to ${base.ref} - done`, GitHubRepository.ID);
}

/**
* Get existing requests to review.
*/
Expand Down
3 changes: 2 additions & 1 deletion webviews/components/merge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ export const OfferToUpdate = ({ mergeable, isSimple, isCurrentlyCheckedOut, canU
setBusy(true);
updateBranch().finally(() => setBusy(false));
};
if (!canUpdateBranch || !isCurrentlyCheckedOut || isSimple || mergeable === PullRequestMergeability.Behind || mergeable === PullRequestMergeability.Conflict || mergeable === PullRequestMergeability.Unknown) {
const isNotCheckedoutWithConflicts = !isCurrentlyCheckedOut && mergeable === PullRequestMergeability.Conflict;
if (!canUpdateBranch || isNotCheckedoutWithConflicts || isSimple || mergeable === PullRequestMergeability.Behind || mergeable === PullRequestMergeability.Conflict || mergeable === PullRequestMergeability.Unknown) {
return null;
}
return (
Expand Down

0 comments on commit 9cd8816

Please sign in to comment.