fix(dev-infra): support running scripts from within a detached head (#37737)
Scripts provided in the `ng-dev` command might use local `git` commands. For such scripts, we keep track of the branch that has been checked out before the command has been invoked. We do this so that we can later (upon command completion) restore back to the original branch. We do not want to leave the Git repository in a dirty state. It looks like this logic currently only deals with branches but does not work properly when a command is invoked from a detached head. We can make it work by just checking out the previous revision (if no branch is checked out). PR Close #37737
This commit is contained in:
parent
eee2fd22e0
commit
dbc2364d16
|
@ -63,8 +63,8 @@ export async function discoverNewConflictsForPr(
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The active github branch when the run began. */
|
/** The active github branch or revision before we performed any Git commands. */
|
||||||
const originalBranch = git.getCurrentBranch();
|
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||||
/* Progress bar to indicate progress. */
|
/* Progress bar to indicate progress. */
|
||||||
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
|
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
|
||||||
/* PRs which were found to be conflicting. */
|
/* PRs which were found to be conflicting. */
|
||||||
|
@ -103,7 +103,7 @@ export async function discoverNewConflictsForPr(
|
||||||
const result = exec(`git rebase FETCH_HEAD`);
|
const result = exec(`git rebase FETCH_HEAD`);
|
||||||
if (result.code) {
|
if (result.code) {
|
||||||
error('The requested PR currently has conflicts');
|
error('The requested PR currently has conflicts');
|
||||||
cleanUpGitState(originalBranch);
|
cleanUpGitState(previousBranchOrRevision);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ export async function discoverNewConflictsForPr(
|
||||||
info();
|
info();
|
||||||
info(`Result:`);
|
info(`Result:`);
|
||||||
|
|
||||||
cleanUpGitState(originalBranch);
|
cleanUpGitState(previousBranchOrRevision);
|
||||||
|
|
||||||
// If no conflicts are found, exit successfully.
|
// If no conflicts are found, exit successfully.
|
||||||
if (conflicts.length === 0) {
|
if (conflicts.length === 0) {
|
||||||
|
@ -147,14 +147,14 @@ export async function discoverNewConflictsForPr(
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset git back to the provided branch. */
|
/** Reset git back to the provided branch or revision. */
|
||||||
export function cleanUpGitState(branch: string) {
|
export function cleanUpGitState(previousBranchOrRevision: string) {
|
||||||
// Ensure that any outstanding rebases are aborted.
|
// Ensure that any outstanding rebases are aborted.
|
||||||
exec(`git rebase --abort`);
|
exec(`git rebase --abort`);
|
||||||
// Ensure that any changes in the current repo state are cleared.
|
// Ensure that any changes in the current repo state are cleared.
|
||||||
exec(`git reset --hard`);
|
exec(`git reset --hard`);
|
||||||
// Checkout the original branch from before the run began.
|
// Checkout the original branch from before the run began.
|
||||||
exec(`git checkout ${branch}`);
|
exec(`git checkout ${previousBranchOrRevision}`);
|
||||||
// Delete the generated branch.
|
// Delete the generated branch.
|
||||||
exec(`git branch -D ${tempWorkingBranch}`);
|
exec(`git branch -D ${tempWorkingBranch}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class AutosquashMergeStrategy extends MergeStrategy {
|
||||||
// is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
|
// is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
|
||||||
// the rebase seems interactive to Git, while it's not interactive to the user.
|
// the rebase seems interactive to Git, while it's not interactive to the user.
|
||||||
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
|
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
|
||||||
const branchBeforeRebase = this.git.getCurrentBranch();
|
const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision();
|
||||||
const rebaseEnv =
|
const rebaseEnv =
|
||||||
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
|
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
|
||||||
this.git.run(
|
this.git.run(
|
||||||
|
@ -69,9 +69,9 @@ export class AutosquashMergeStrategy extends MergeStrategy {
|
||||||
// Update pull requests commits to reference the pull request. This matches what
|
// Update pull requests commits to reference the pull request. This matches what
|
||||||
// Github does when pull requests are merged through the Web UI. The motivation is
|
// Github does when pull requests are merged through the Web UI. The motivation is
|
||||||
// that it should be easy to determine which pull request contained a given commit.
|
// that it should be easy to determine which pull request contained a given commit.
|
||||||
// **Note**: The filter-branch command relies on the working tree, so we want to make
|
// Note: The filter-branch command relies on the working tree, so we want to make sure
|
||||||
// sure that we are on the initial branch where the merge script has been run.
|
// that we are on the initial branch or revision where the merge script has been invoked.
|
||||||
this.git.run(['checkout', '-f', branchBeforeRebase]);
|
this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]);
|
||||||
this.git.run(
|
this.git.run(
|
||||||
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]);
|
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]);
|
||||||
|
|
||||||
|
|
|
@ -87,14 +87,14 @@ export class PullRequestMergeTask {
|
||||||
new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) :
|
new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) :
|
||||||
new AutosquashMergeStrategy(this.git);
|
new AutosquashMergeStrategy(this.git);
|
||||||
|
|
||||||
// Branch that is currently checked out so that we can switch back to it once
|
// Branch or revision that is currently checked out so that we can switch back to
|
||||||
// the pull request has been merged.
|
// it once the pull request has been merged.
|
||||||
let previousBranch: null|string = null;
|
let previousBranchOrRevision: null|string = null;
|
||||||
|
|
||||||
// The following block runs Git commands as child processes. These Git commands can fail.
|
// The following block runs Git commands as child processes. These Git commands can fail.
|
||||||
// We want to capture these command errors and return an appropriate merge request status.
|
// We want to capture these command errors and return an appropriate merge request status.
|
||||||
try {
|
try {
|
||||||
previousBranch = this.git.getCurrentBranch();
|
previousBranchOrRevision = this.git.getCurrentBranchOrRevision();
|
||||||
|
|
||||||
// Run preparations for the merge (e.g. fetching branches).
|
// Run preparations for the merge (e.g. fetching branches).
|
||||||
await strategy.prepare(pullRequest);
|
await strategy.prepare(pullRequest);
|
||||||
|
@ -107,7 +107,7 @@ export class PullRequestMergeTask {
|
||||||
|
|
||||||
// Switch back to the previous branch. We need to do this before deleting the temporary
|
// Switch back to the previous branch. We need to do this before deleting the temporary
|
||||||
// branches because we cannot delete branches which are currently checked out.
|
// branches because we cannot delete branches which are currently checked out.
|
||||||
this.git.run(['checkout', '-f', previousBranch]);
|
this.git.run(['checkout', '-f', previousBranchOrRevision]);
|
||||||
|
|
||||||
await strategy.cleanup(pullRequest);
|
await strategy.cleanup(pullRequest);
|
||||||
|
|
||||||
|
@ -123,8 +123,8 @@ export class PullRequestMergeTask {
|
||||||
} finally {
|
} finally {
|
||||||
// Always try to restore the branch if possible. We don't want to leave
|
// Always try to restore the branch if possible. We don't want to leave
|
||||||
// the repository in a different state than before.
|
// the repository in a different state than before.
|
||||||
if (previousBranch !== null) {
|
if (previousBranchOrRevision !== null) {
|
||||||
this.git.runGraceful(['checkout', '-f', previousBranch]);
|
this.git.runGraceful(['checkout', '-f', previousBranchOrRevision]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,10 +50,10 @@ export async function rebasePr(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The branch originally checked out before this method performs any Git
|
* The branch or revision originally checked out before this method performed
|
||||||
* operations that may change the working branch.
|
* any Git operations that may change the working branch.
|
||||||
*/
|
*/
|
||||||
const originalBranch = git.getCurrentBranch();
|
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||||
/* Get the PR information from Github. */
|
/* Get the PR information from Github. */
|
||||||
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
|
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ export async function rebasePr(
|
||||||
info();
|
info();
|
||||||
info(`To abort the rebase and return to the state of the repository before this command`);
|
info(`To abort the rebase and return to the state of the repository before this command`);
|
||||||
info(`run the following command:`);
|
info(`run the following command:`);
|
||||||
info(` $ git rebase --abort && git reset --hard && git checkout ${originalBranch}`);
|
info(` $ git rebase --abort && git reset --hard && git checkout ${previousBranchOrRevision}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
info(`Cleaning up git state, and restoring previous state.`);
|
info(`Cleaning up git state, and restoring previous state.`);
|
||||||
|
@ -137,7 +137,7 @@ export async function rebasePr(
|
||||||
// Ensure that any changes in the current repo state are cleared.
|
// Ensure that any changes in the current repo state are cleared.
|
||||||
git.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
|
git.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
|
||||||
// Checkout the original branch from before the run began.
|
// Checkout the original branch from before the run began.
|
||||||
git.runGraceful(['checkout', originalBranch], {stdio: 'ignore'});
|
git.runGraceful(['checkout', previousBranchOrRevision], {stdio: 'ignore'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,9 +119,16 @@ export class GitClient {
|
||||||
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the currently checked out branch. */
|
/** Gets the currently checked out branch or revision. */
|
||||||
getCurrentBranch(): string {
|
getCurrentBranchOrRevision(): string {
|
||||||
return this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
const branchName = this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
||||||
|
// If no branch name could be resolved. i.e. `HEAD` has been returned, then Git
|
||||||
|
// is currently in a detached state. In those cases, we just want to return the
|
||||||
|
// currently checked out revision/SHA.
|
||||||
|
if (branchName === 'HEAD') {
|
||||||
|
return this.run(['rev-parse', 'HEAD']).stdout.trim();
|
||||||
|
}
|
||||||
|
return branchName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets whether the current Git repository has uncommitted changes. */
|
/** Gets whether the current Git repository has uncommitted changes. */
|
||||||
|
|
Loading…
Reference in New Issue