forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
149 lines (133 loc) · 5.71 KB
/
index.ts
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
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {types as graphQLTypes} from 'typed-graphqlify';
import {URL} from 'url';
import {getConfig, NgDevConfig} from '../../utils/config';
import {error, info, promptConfirm} from '../../utils/console';
import {GitClient} from '../../utils/git';
import {getPr} from '../../utils/github';
/* GraphQL schema for the response body for each pending PR. */
const PR_SCHEMA = {
state: graphQLTypes.string,
maintainerCanModify: graphQLTypes.boolean,
viewerDidAuthor: graphQLTypes.boolean,
headRefOid: graphQLTypes.string,
headRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
baseRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
};
/**
* Rebase the provided PR onto its merge target branch, and push up the resulting
* commit to the PRs repository.
*/
export async function rebasePr(
prNumber: number, githubToken: string, config: Pick<NgDevConfig, 'github'> = getConfig()) {
const git = new GitClient(githubToken);
// TODO: Rely on a common assertNoLocalChanges function.
if (git.hasLocalChanges()) {
error('Cannot perform rebase of PR with local changes.');
process.exit(1);
}
/**
* The branch originally checked out before this method performs any Git
* operations that may change the working branch.
*/
const originalBranch = git.getCurrentBranch();
/* Get the PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
const headRefName = pr.headRef.name;
const baseRefName = pr.baseRef.name;
const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
const fullBaseRef = `${pr.baseRef.repository.nameWithOwner}:${baseRefName}`;
const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken);
const baseRefUrl = addAuthenticationToUrl(pr.baseRef.repository.url, githubToken);
// Note: Since we use a detached head for rebasing the PR and therefore do not have
// remote-tracking branches configured, we need to set our expected ref and SHA. This
// allows us to use `--force-with-lease` for the detached head while ensuring that we
// never accidentally override upstream changes that have been pushed in the meanwhile.
// See:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
// be pushed up.
if (!pr.maintainerCanModify && !pr.viewerDidAuthor) {
error(
`Cannot rebase as you did not author the PR and the PR does not allow maintainers` +
`to modify the PR`);
process.exit(1);
}
try {
// Fetch the branch at the commit of the PR, and check it out in a detached state.
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
git.run(['fetch', headRefUrl, headRefName]);
git.run(['checkout', '--detach', 'FETCH_HEAD']);
// Fetch the PRs target branch and rebase onto it.
info(`Fetching ${fullBaseRef} to rebase #${prNumber} on`);
git.run(['fetch', baseRefUrl, baseRefName]);
info(`Attempting to rebase PR #${prNumber} on ${fullBaseRef}`);
const rebaseResult = git.runGraceful(['rebase', 'FETCH_HEAD']);
// If the rebase was clean, push the rebased PR up to the authors fork.
if (rebaseResult.status === 0) {
info(`Rebase was able to complete automatically without conflicts`);
info(`Pushing rebased PR #${prNumber} to ${fullHeadRef}`);
git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
info(`Rebased and updated PR #${prNumber}`);
cleanUpGitState();
process.exit(0);
}
} catch (err) {
error(err.message);
cleanUpGitState();
process.exit(1);
}
// On automatic rebase failures, prompt to choose if the rebase should be continued
// manually or aborted now.
info(`Rebase was unable to complete automatically without conflicts.`);
// If the command is run in a non-CI environment, prompt to format the files immediately.
const continueRebase =
process.env['CI'] === undefined && await promptConfirm('Manually complete rebase?');
if (continueRebase) {
info(`After manually completing rebase, run the following command to update PR #${prNumber}:`);
info(` $ git push ${pr.headRef.repository.url} HEAD:${headRefName} ${forceWithLeaseFlag}`);
info();
info(`To abort the rebase and return to the state of the repository before this command`);
info(`run the following command:`);
info(` $ git rebase --abort && git reset --hard && git checkout ${originalBranch}`);
process.exit(1);
} else {
info(`Cleaning up git state, and restoring previous state.`);
}
cleanUpGitState();
process.exit(1);
/** Reset git back to the original branch. */
function cleanUpGitState() {
// Ensure that any outstanding rebases are aborted.
git.runGraceful(['rebase', '--abort'], {stdio: 'ignore'});
// Ensure that any changes in the current repo state are cleared.
git.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
// Checkout the original branch from before the run began.
git.runGraceful(['checkout', originalBranch], {stdio: 'ignore'});
}
}
/** Adds the provided token as username to the provided url. */
function addAuthenticationToUrl(urlString: string, token: string) {
const url = new URL(urlString);
url.username = token;
return url.toString();
}