From bb1ac81bb1ddab587e0a4c80c882b308268f7fc0 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 23 Apr 2019 13:52:23 -0700 Subject: [PATCH] Experimental management scripts (#31067) * Add configure-experimental * Add script for synchronizing branches with master and creating an experimental branch with the result of merging those --- scripts/open-user-pr.ts | 11 +-- scripts/run-sequence.js | 19 +++++ scripts/update-experimental-branches.js | 97 +++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 scripts/run-sequence.js create mode 100644 scripts/update-experimental-branches.js diff --git a/scripts/open-user-pr.ts b/scripts/open-user-pr.ts index 9c582ad9895..0a636c267b8 100644 --- a/scripts/open-user-pr.ts +++ b/scripts/open-user-pr.ts @@ -1,16 +1,7 @@ /// // Must reference esnext.asynciterable lib, since octokit uses AsyncIterable internally -import cp = require("child_process"); import Octokit = require("@octokit/rest"); - -const opts = { timeout: 100_000, shell: true, stdio: "inherit" } -function runSequence(tasks: [string, string[]][]) { - for (const task of tasks) { - console.log(`${task[0]} ${task[1].join(" ")}`); - const result = cp.spawnSync(task[0], task[1], opts); - if (result.status !== 0) throw new Error(`${task[0]} ${task[1].join(" ")} failed: ${result.stderr && result.stderr.toString()}`); - } -} +import {runSequence} from "./run-sequence"; function padNum(number: number) { const str = "" + number; diff --git a/scripts/run-sequence.js b/scripts/run-sequence.js new file mode 100644 index 00000000000..ef7a384af4c --- /dev/null +++ b/scripts/run-sequence.js @@ -0,0 +1,19 @@ +// @ts-check +const cp = require("child_process"); +/** + * + * @param {[string, string[]][]} tasks + * @param {cp.SpawnSyncOptions} opts + */ +function runSequence(tasks, opts = { timeout: 100000, shell: true, stdio: "inherit" }) { + let lastResult; + for (const task of tasks) { + console.log(`${task[0]} ${task[1].join(" ")}`); + const result = cp.spawnSync(task[0], task[1], opts); + if (result.status !== 0) throw new Error(`${task[0]} ${task[1].join(" ")} failed: ${result.stderr && result.stderr.toString()}`); + lastResult = result; + } + return lastResult && lastResult.stdout && lastResult.stdout.toString(); +} + +exports.runSequence = runSequence; \ No newline at end of file diff --git a/scripts/update-experimental-branches.js b/scripts/update-experimental-branches.js new file mode 100644 index 00000000000..c112cf1a367 --- /dev/null +++ b/scripts/update-experimental-branches.js @@ -0,0 +1,97 @@ +// @ts-check +/// +const Octokit = require("@octokit/rest"); +const {runSequence} = require("./run-sequence"); + +/** + * This program should be invoked as `node ./scripts/update-experimental-branches [Branch2] [...]` + */ +async function main() { + const branchesRaw = process.argv[3]; + const branches = process.argv.slice(3); + if (!branches.length) { + throw new Error(`No experimental branches, aborting...`); + } + console.log(`Performing experimental branch updating and merging for branches ${branchesRaw}`); + + const gh = new Octokit(); + gh.authenticate({ + type: "token", + token: process.argv[2] + }); + + // Fetch all relevant refs + runSequence([ + ["git", ["fetch", "origin", "master:master", ...branches.map(b => `${b}:${b}`)]] + ]) + + // Forcibly cleanup workspace + runSequence([ + ["git", ["clean", "-fdx"]], + ["git", ["checkout", "."]], + ["git", ["checkout", "master"]], + ]); + + // Update branches + for (const branch of branches) { + // Checkout, then get the merge base + const mergeBase = runSequence([ + ["git", ["checkout", branch]], + ["git", ["merge-base", branch, "master"]], + ]); + // Simulate the merge and abort if there are conflicts + const mergeTree = runSequence([ + ["git", ["merge-tree", mergeBase, branch, "master"]] + ]); + if (mergeTree.indexOf(`===${"="}===`)) { // 7 equals is the center of the merge conflict marker + const res = await gh.pulls.list({owner: "Microsoft", repo: "TypeScript", base: branch}); + if (res && res.data && res.data[0]) { + const pr = res.data[0]; + await gh.issues.createComment({ + owner: "Microsoft", + repo: "TypeScript", + number: pr.number, + body: `This PR is configured as an experiment, and currently has merge conflicts with master - please rebase onto master and fix the conflicts.` + }); + } + throw new Error(`Merge conflict detected on branch ${branch} with master`); + } + // Merge is good - apply a rebase and (force) push + runSequence([ + ["git", ["rebase", "master"]], + ["git", ["push", "-f", "-u", "origin", branch]], + ]); + } + + // Return to `master` and make a new `experimental` branch + runSequence([ + ["git", ["checkout", "master"]], + ["git", ["branch", "-D", "experimental"]], + ["git", ["checkout", "-b", "experimental"]], + ]); + + // Merge each branch into `experimental` (which, if there is a conflict, we now know is from inter-experiment conflict) + for (const branch of branches) { + // Find the merge base + const mergeBase = runSequence([ + ["git", ["merge-base", branch, "experimental"]], + ]); + // Simulate the merge and abort if there are conflicts + const mergeTree = runSequence([ + ["git", ["merge-tree", mergeBase, branch, "experimental"]] + ]); + if (mergeTree.indexOf(`===${"="}===`)) { // 7 equals is the center of the merge conflict marker + throw new Error(`Merge conflict detected on branch ${branch} with other experiment`); + } + // Merge (always producing a merge commit) + runSequence([ + ["git", ["merge", branch, "--no-ff"]], + ]); + } + // Every branch merged OK, force push the replacement `experimental` branch + runSequence([ + ["git", ["push", "-f", "-u", "origin", "experimental"]], + ]); +} + +main().catch(e => (console.error(e), process.exitCode = 2));