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));