1#!/usr/bin/env node --redirect-warnings=/dev/null
  2
  3const { execFileSync } = require("child_process");
  4
  5main();
  6
  7async function main() {
  8  let version = process.argv[2];
  9  let channel = process.argv[3];
 10  let parts = version.split(".");
 11
 12  if (
 13    process.argv.length != 4 ||
 14    parts.length != 3 ||
 15    parts.find((part) => isNaN(part)) != null ||
 16    (channel != "stable" && channel != "preview")
 17  ) {
 18    console.log("Usage: draft-release-notes <version> {stable|preview}");
 19    process.exit(1);
 20  }
 21
 22  // currently we can only draft notes for patch releases.
 23  if (parts[2] == 0) {
 24    process.exit(0);
 25  }
 26
 27  let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
 28  let suffix = channel == "preview" ? "-pre" : "";
 29  let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
 30
 31  try {
 32    execFileSync("rm", ["-rf", "target/shallow_clone"]);
 33    execFileSync("git", [
 34      "clone",
 35      "https://github.com/zed-industries/zed",
 36      "target/shallow_clone",
 37      "--filter=tree:0",
 38      "--no-checkout",
 39      "--branch",
 40      tag,
 41      "--depth",
 42      100,
 43    ]);
 44    execFileSync("git", [
 45      "-C",
 46      "target/shallow_clone",
 47      "rev-parse",
 48      "--verify",
 49      tag,
 50    ]);
 51    execFileSync("git", [
 52      "-C",
 53      "target/shallow_clone",
 54      "rev-parse",
 55      "--verify",
 56      priorTag,
 57    ]);
 58  } catch (e) {
 59    console.error(e.stderr.toString());
 60    process.exit(1);
 61  }
 62
 63  const newCommits = getCommits(priorTag, tag);
 64
 65  let releaseNotes = [];
 66  let missing = [];
 67  let skipped = [];
 68
 69  for (const commit of newCommits) {
 70    let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
 71    let notes = commit.releaseNotes;
 72    if (commit.pr == "") {
 73      link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
 74    } else if (!notes.includes("zed-industries/zed/issues")) {
 75      notes = notes + " ([#" + commit.pr + "](" + link + "))";
 76    }
 77
 78    if (commit.releaseNotes == "") {
 79      missing.push("- MISSING " + commit.firstLine + " " + link);
 80    } else if (commit.releaseNotes.startsWith("- N/A")) {
 81      skipped.push("- N/A " + commit.firstLine + " " + link);
 82    } else {
 83      releaseNotes.push(notes);
 84    }
 85  }
 86
 87  console.log(releaseNotes.join("\n") + "\n");
 88}
 89
 90function getCommits(oldTag, newTag) {
 91  const pullRequestNumbers = execFileSync(
 92    "git",
 93    [
 94      "-C",
 95      "target/shallow_clone",
 96      "log",
 97      `${oldTag}..${newTag}`,
 98      "--format=DIVIDER\n%H|||%B",
 99    ],
100    { encoding: "utf8" },
101  )
102    .replace(/\r\n/g, "\n")
103    .split("DIVIDER\n")
104    .filter((commit) => commit.length > 0)
105    .map((commit) => {
106      let [hash, firstLine] = commit.split("\n")[0].split("|||");
107      let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
108      let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
109      let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
110        .split("\n\n")[0]
111        .trim()
112        .replace(/\n(?![\n-])/g, " ");
113
114      if (releaseNotes.includes("<public_issue_number_if_exists>")) {
115        releaseNotes = "";
116      }
117
118      return {
119        hash,
120        pr,
121        cherryPick,
122        releaseNotes,
123        firstLine,
124      };
125    });
126
127  return pullRequestNumbers;
128}