Browse Source

chore: ensure release notes always come from Clerk (#23777)

* chore: ensure release notes always come from Clerk

Now with tests!

* chore: move sinon devDependency into `spec-main`

* refactor: tweak note-spec variable for readability
Charles Kerr 4 years ago
parent
commit
980e592271
25 changed files with 351 additions and 72 deletions
  1. 48 72
      script/release/notes/notes.js
  2. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-0600420bac25439fc2067d51c6aaa4ee11770577
  3. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-2955c67c4ea712fa22773ac9113709fc952bfd49
  4. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98
  5. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-467409458e716c68b35fa935d556050ca6bed1c4
  6. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-89eb309d0b22bd4aec058ffaf983e81e56a5c378
  7. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-8bc0c92137f4a77dc831ca644a86a3e48b51a11e
  8. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-commit-a6ff42c190cb5caf8f3e217748e49183a951491b
  9. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-issue-20214-comments
  10. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-issue-21891-comments
  11. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-issue-21946-comments
  12. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-issue-22750-comments
  13. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-issue-22828-comments
  14. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-20214
  15. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-20620
  16. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-21497
  17. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-21591
  18. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-21891
  19. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-21946
  20. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-22750
  21. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-22770
  22. 0 0
      spec-main/fixtures/release-notes/cache/electron-electron-pull-22828
  23. 2 0
      spec-main/package.json
  24. 185 0
      spec-main/release-notes-spec.ts
  25. 116 0
      spec-main/yarn.lock

+ 48 - 72
script/release/notes/notes.js

@@ -18,7 +18,6 @@ const { ELECTRON_VERSION, SRC_DIR } = require('../../lib/utils');
 const MAX_FAIL_COUNT = 3;
 const CHECK_INTERVAL = 5000;
 
-const CACHE_DIR = path.resolve(__dirname, '.cache');
 const NO_NOTES = 'No notes';
 const FOLLOW_REPOS = ['electron/electron', 'electron/node'];
 
@@ -28,6 +27,8 @@ const fixTypes = new Set(['fix']);
 const otherTypes = new Set(['spec', 'build', 'test', 'chore', 'deps', 'refactor', 'tools', 'vendor', 'perf', 'style', 'ci']);
 const knownTypes = new Set([...docTypes.keys(), ...featTypes.keys(), ...fixTypes.keys(), ...otherTypes.keys()]);
 
+const getCacheDir = () => process.env.NOTES_CACHE_PATH || path.resolve(__dirname, '.cache');
+
 /**
 ***
 **/
@@ -53,9 +54,7 @@ class Commit {
     this.owner = owner; // string
     this.repo = repo; // string
 
-    this.body = null; // string
     this.isBreakingChange = false;
-    this.issueNumber = null; // number
     this.note = null; // string
     this.prKeys = new Set(); // GHKey
     this.revertHash = null; // string
@@ -129,41 +128,11 @@ const OMIT_FROM_RELEASE_NOTES_KEYS = [
   'blank'
 ];
 
-const getNoteFromBody = body => {
-  if (!body) {
-    return null;
-  }
-
-  const NOTE_PREFIX = 'Notes: ';
-  const NOTE_HEADER = '#### Release Notes';
-
-  let note = body
-    .split(/\r?\n\r?\n/) // split into paragraphs
-    .map(paragraph => paragraph.trim())
-    .map(paragraph => paragraph.startsWith(NOTE_HEADER) ? paragraph.slice(NOTE_HEADER.length).trim() : paragraph)
-    .find(paragraph => paragraph.startsWith(NOTE_PREFIX));
-
-  if (note) {
-    note = note
-      .slice(NOTE_PREFIX.length)
-      .replace(/<!--.*-->/, '') // '<!-- change summary here-->'
-      .replace(/\r?\n/, ' ') // remove newlines
-      .trim();
-  }
-
-  if (note && OMIT_FROM_RELEASE_NOTES_KEYS.includes(note.toLowerCase())) {
-    return NO_NOTES;
-  }
-
-  return note;
-};
-
 /**
  * Looks for our project's conventions in the commit message:
  *
  * 'semantic: some description' -- sets semanticType, subject
  * 'some description (#99999)' -- sets subject, pr
- * 'Fixes #3333' -- sets issueNumber
  * 'Merge pull request #99999 from ${branchname}' -- sets pr
  * 'This reverts commit ${sha}' -- sets revertHash
  * line starting with 'BREAKING CHANGE' in body -- sets isBreakingChange
@@ -181,13 +150,6 @@ const parseCommitMessage = (commitMessage, commit) => {
     subject = subject.slice(0, pos).trim();
   }
 
-  if (body) {
-    commit.body = body;
-
-    const note = getNoteFromBody(body);
-    if (note) { commit.note = note; }
-  }
-
   // if the subject ends in ' (#dddd)', treat it as a pull request id
   let match;
   if ((match = subject.match(/^(.*)\s\(#(\d+)\)$/))) {
@@ -219,7 +181,6 @@ const parseCommitMessage = (commitMessage, commit) => {
 
   // https://help.github.com/articles/closing-issues-using-keywords/
   if ((match = body.match(/\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved|for)\s#(\d+)\b/i))) {
-    commit.issueNumber = parseInt(match[1]);
     commit.semanticType = commit.semanticType || 'fix';
   }
 
@@ -243,32 +204,32 @@ const parseCommitMessage = (commitMessage, commit) => {
 const parsePullText = (pull, commit) => parseCommitMessage(`${pull.data.title}\n\n${pull.data.body}`, commit);
 
 const getLocalCommitHashes = async (dir, ref) => {
-  const args = ['log', '-z', '--format=%H', ref];
-  return (await runGit(dir, args)).split('\0').map(hash => hash.trim());
+  const args = ['log', '--format=%H', ref];
+  return (await runGit(dir, args)).split(/[\r\n]+/).map(hash => hash.trim());
 };
 
 // return an array of Commits
 const getLocalCommits = async (module, point1, point2) => {
   const { owner, repo, dir } = module;
 
-  const fieldSep = '||';
-  const format = ['%H', '%B'].join(fieldSep);
-  const args = ['log', '-z', '--cherry-pick', '--right-only', '--first-parent', `--format=${format}`, `${point1}..${point2}`];
-  const logs = (await runGit(dir, args)).split('\0').map(field => field.trim());
+  const fieldSep = ',';
+  const format = ['%H', '%s'].join(fieldSep);
+  const args = ['log', '--cherry-pick', '--right-only', '--first-parent', `--format=${format}`, `${point1}..${point2}`];
+  const logs = (await runGit(dir, args)).split(/[\r\n]+/).map(field => field.trim());
 
   const commits = [];
   for (const log of logs) {
     if (!log) {
       continue;
     }
-    const [hash, message] = log.split(fieldSep, 2).map(field => field.trim());
-    commits.push(parseCommitMessage(message, new Commit(hash, owner, repo)));
+    const [hash, subject] = log.split(fieldSep, 2).map(field => field.trim());
+    commits.push(parseCommitMessage(subject, new Commit(hash, owner, repo)));
   }
   return commits;
 };
 
 const checkCache = async (name, operation) => {
-  const filename = path.resolve(CACHE_DIR, name);
+  const filename = path.resolve(getCacheDir(), name);
   if (fs.existsSync(filename)) {
     return JSON.parse(fs.readFileSync(filename, 'utf8'));
   }
@@ -292,7 +253,8 @@ async function runRetryable (fn, maxRetries) {
     }
   }
   // Silently eat 404s.
-  if (lastError.status !== 404) throw lastError;
+  // Silently eat 422s, which come from "No commit found for SHA"
+  if (lastError.status !== 404 && lastError.status !== 422) throw lastError;
 }
 
 const getPullCacheFilename = ghKey => `${ghKey.owner}-${ghKey.repo}-pull-${ghKey.number}`;
@@ -300,7 +262,7 @@ const getPullCacheFilename = ghKey => `${ghKey.owner}-${ghKey.repo}-pull-${ghKey
 const getCommitPulls = async (owner, repo, hash) => {
   const name = `${owner}-${repo}-commit-${hash}`;
   const retryableFunc = () => octokit.repos.listPullRequestsAssociatedWithCommit({ owner, repo, commit_sha: hash });
-  const ret = await checkCache(name, () => runRetryable(retryableFunc, MAX_FAIL_COUNT));
+  let ret = await checkCache(name, () => runRetryable(retryableFunc, MAX_FAIL_COUNT));
 
   // only merged pulls belong in release notes
   if (ret && ret.data) {
@@ -316,6 +278,11 @@ const getCommitPulls = async (owner, repo, hash) => {
     }
   }
 
+  // ensure the return value has the expected structure, even on failure
+  if (!ret || !ret.data) {
+    ret = { data: [] };
+  }
+
   return ret;
 };
 
@@ -447,19 +414,25 @@ function getOldestMajorBranchOfCommit (commit, pool) {
     .shift();
 }
 
+function commitExistsBeforeMajor (commit, pool, major) {
+  const firstAppearance = getOldestMajorBranchOfCommit(commit, pool);
+  return firstAppearance && (firstAppearance < major);
+}
+
 /***
 ****  Main
 ***/
 
 const getNotes = async (fromRef, toRef, newVersion) => {
-  if (!fs.existsSync(CACHE_DIR)) {
-    fs.mkdirSync(CACHE_DIR);
+  const cacheDir = getCacheDir();
+  if (!fs.existsSync(cacheDir)) {
+    fs.mkdirSync(cacheDir);
   }
 
   const pool = new Pool();
 
   // get the electron/electron commits
-  const electron = { owner: 'electron', repo: 'electron', dir: ELECTRON_VERSION };
+  const electron = { owner: 'electron', repo: 'electron', dir: path.resolve(SRC_DIR, 'electron') };
   await addRepoToPool(pool, electron, fromRef, toRef);
 
   // Don't include submodules if comparing across major versions;
@@ -496,27 +469,24 @@ const getNotes = async (fromRef, toRef, newVersion) => {
   // ensure the commit has a note
   for (const commit of pool.commits) {
     for (const prKey of commit.prKeys.values()) {
-      commit.note = commit.note || await getNoteFromClerk(prKey);
       if (commit.note) {
         break;
       }
+      commit.note = await getNoteFromClerk(prKey);
     }
-    // use a fallback note in case someone missed a 'Notes' comment
-    commit.note = commit.note || commit.subject;
   }
 
   // remove non-user-facing commits
   pool.commits = pool.commits
-    .filter(commit => commit.note !== NO_NOTES)
+    .filter(commit => commit.note && (commit.note !== NO_NOTES))
     .filter(commit => !((commit.note || commit.subject).match(/^[Bb]ump v\d+\.\d+\.\d+/)));
 
   if (!shouldIncludeMultibranchChanges(newVersion)) {
-    const currentMajor = semver.parse(newVersion).major;
-    pool.commits = pool.commits
-      .filter(commit => getOldestMajorBranchOfCommit(commit, pool) >= currentMajor);
+    const { major } = semver.parse(newVersion);
+    pool.commits = pool.commits.filter(commit => !commitExistsBeforeMajor(commit, pool, major));
   }
 
-  pool.commits = removeSupercededChromiumUpdates(pool.commits);
+  pool.commits = removeSupercededStackUpdates(pool.commits);
 
   const notes = {
     breaking: [],
@@ -550,18 +520,24 @@ const getNotes = async (fromRef, toRef, newVersion) => {
   return notes;
 };
 
-const removeSupercededChromiumUpdates = (commits) => {
-  const chromiumRegex = /^Updated Chromium to \d+\.\d+\.\d+\.\d+/;
-  const updates = commits.filter(commit => (commit.note || commit.subject).match(chromiumRegex));
-  const keepers = commits.filter(commit => !updates.includes(commit));
+const removeSupercededStackUpdates = (commits) => {
+  const updateRegex = /^Updated ([a-zA-Z.]+) to v?([\d.]+)/;
+  const notupdates = [];
 
-  // keep the newest update.
-  if (updates.length) {
-    const compare = (a, b) => (a.note || a.subject).localeCompare(b.note || b.subject);
-    keepers.push(updates.sort(compare).pop());
+  const newest = {};
+  for (const commit of commits) {
+    const match = (commit.note || commit.subject).match(updateRegex);
+    if (!match) {
+      notupdates.push(commit);
+      continue;
+    }
+    const [ , dep, version ] = match;
+    if (!newest[dep] || newest[dep].version < version) {
+      newest[dep] = { commit, version };
+    }
   }
 
-  return keepers;
+  return [ ...notupdates, ...Object.values(newest).map(o => o.commit) ];
 };
 
 /***

File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-0600420bac25439fc2067d51c6aaa4ee11770577


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-2955c67c4ea712fa22773ac9113709fc952bfd49


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-467409458e716c68b35fa935d556050ca6bed1c4


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-89eb309d0b22bd4aec058ffaf983e81e56a5c378


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-8bc0c92137f4a77dc831ca644a86a3e48b51a11e


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-commit-a6ff42c190cb5caf8f3e217748e49183a951491b


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-issue-20214-comments


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-issue-21891-comments


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-issue-21946-comments


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-issue-22750-comments


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-issue-22828-comments


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-20214


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-20620


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-21497


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-21591


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-21891


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-21946


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-22750


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-22770


File diff suppressed because it is too large
+ 0 - 0
spec-main/fixtures/release-notes/cache/electron-electron-pull-22828


+ 2 - 0
spec-main/package.json

@@ -4,10 +4,12 @@
   "main": "index.js",
   "version": "0.1.0",
   "devDependencies": {
+    "@types/sinon": "^9.0.4",
     "@types/ws": "^7.2.0",
     "busboy": "^0.3.1",
     "echo": "file:fixtures/native-addon/echo",
     "q": "^1.5.1",
+    "sinon": "^9.0.1",
     "ws": "^7.2.1"
   },
   "dependencies": {

+ 185 - 0
spec-main/release-notes-spec.ts

@@ -0,0 +1,185 @@
+import { GitProcess, IGitExecutionOptions, IGitResult } from 'dugite';
+import { expect } from 'chai';
+import * as notes from '../script/release/notes/notes.js';
+import * as path from 'path';
+import * as sinon from 'sinon';
+
+/* Fake a Dugite GitProcess that only returns the specific
+   commits that we want to test */
+
+class Commit {
+  sha1: string;
+  subject: string;
+  constructor (sha1: string, subject: string) {
+    this.sha1 = sha1;
+    this.subject = subject;
+  }
+}
+
+class GitFake {
+  branches: {
+    [key: string]: Commit[],
+  };
+
+  constructor () {
+    this.branches = {};
+  }
+
+  setBranch (name: string, commits: Array<Commit>): void {
+    this.branches[name] = commits;
+  }
+
+  // find the newest shared commit between branches a and b
+  mergeBase (a: string, b:string): string {
+    for (const commit of [ ...this.branches[a].reverse() ]) {
+      if (this.branches[b].map((commit: Commit) => commit.sha1).includes(commit.sha1)) {
+        return commit.sha1;
+      }
+    }
+    console.error('test error: branches not related');
+    return '';
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  exec (args: string[], path: string, options?: IGitExecutionOptions | undefined): Promise<IGitResult> {
+    let stdout = '';
+    const stderr = '';
+    const exitCode = 0;
+
+    if (args.length === 3 && args[0] === 'merge-base') {
+      // expected form: `git merge-base branchName1 branchName2`
+      const a: string = args[1]!;
+      const b: string = args[2]!;
+      stdout = this.mergeBase(a, b);
+    } else if (args.length === 3 && args[0] === 'log' && args[1] === '--format=%H') {
+      // exepcted form: `git log --format=%H branchName
+      const branch: string = args[2]!;
+      stdout = this.branches[branch].map((commit: Commit) => commit.sha1).join('\n');
+    } else if (args.length > 1 && args[0] === 'log' && args.includes('--format=%H,%s')) {
+      // expected form: `git log --format=%H,%s sha1..branchName
+      const [ start, branch ] = args[args.length - 1].split('..');
+      const lines : string[] = [];
+      let started = false;
+      for (const commit of this.branches[branch]) {
+        started = started || commit.sha1 === start;
+        if (started) {
+          lines.push(`${commit.sha1},${commit.subject}` /* %H,%s */);
+        }
+      }
+      stdout = lines.join('\n');
+    } else {
+      console.error('unhandled GitProcess.exec():', args);
+    }
+
+    return Promise.resolve({ exitCode, stdout, stderr });
+  }
+}
+
+describe('release notes', () => {
+  const sandbox = sinon.createSandbox();
+  const gitFake = new GitFake();
+
+  const oldBranch = '8-x-y';
+  const newBranch = '9-x-y';
+
+  // commits shared by both oldBranch and newBranch
+  const sharedHistory = [
+    new Commit('2abea22b4bffa1626a521711bacec7cd51425818', "fix: explicitly cancel redirects when mode is 'error' (#20686)"),
+    new Commit('467409458e716c68b35fa935d556050ca6bed1c4', 'build: add support for automated minor releases (#20620)') // merge-base
+  ];
+
+  // these commits came after newBranch was created
+  const newBreaking = new Commit('2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98', 'refactor: use v8 serialization for ipc (#20214)');
+  const newFeat = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: allow GUID parameter to avoid systray demotion on Windows  (#21891)');
+  const newFix = new Commit('0600420bac25439fc2067d51c6aaa4ee11770577', "fix: don't allow window to go behind menu bar on mac (#22828)");
+  const oldFix = new Commit('f77bd19a70ac2d708d17ddbe4dc12745ca3a8577', 'fix: prevent menu gc during popup (#20785)');
+
+  // a bug that's fixed in both branches by separate PRs
+  const newTropFix = new Commit('a6ff42c190cb5caf8f3e217748e49183a951491b', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22750)');
+  const oldTropFix = new Commit('8751f485c5a6c8c78990bfd55a4350700f81f8cd', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22749)');
+
+  before(() => {
+    // location of relase-notes' octokit reply cache
+    const fixtureDir = path.resolve(__dirname, 'fixtures', 'release-notes');
+    process.env.NOTES_CACHE_PATH = path.resolve(fixtureDir, 'cache');
+  });
+
+  beforeEach(() => {
+    const wrapper = (args: string[], path: string, options?: IGitExecutionOptions | undefined) => gitFake.exec(args, path, options);
+    sandbox.replace(GitProcess, 'exec', wrapper);
+
+    gitFake.setBranch(oldBranch, [ ...sharedHistory, oldFix ]);
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  describe('changes that exist in older branches', () => {
+    // use case: this fix is NOT news because it was already fixed
+    // while oldBranch was the latest stable release
+    it('are skipped if the target version is a new major line (x.0.0)', async function () {
+      const version = 'v9.0.0';
+      gitFake.setBranch(oldBranch, [ ...sharedHistory, oldTropFix ]);
+      gitFake.setBranch(newBranch, [ ...sharedHistory, newTropFix ]);
+      const results: any = await notes.get(oldBranch, newBranch, version);
+      expect(results.fix).to.have.lengthOf(0);
+    });
+
+    // use case: this fix IS news because it's being fixed in
+    // multiple stable branches at once, including newBranch.
+    it('are included if the target version is a minor or patch bump', async function () {
+      const version = 'v9.0.1';
+      gitFake.setBranch(oldBranch, [ ...sharedHistory, oldTropFix ]);
+      gitFake.setBranch(newBranch, [ ...sharedHistory, newTropFix ]);
+      const results: any = await notes.get(oldBranch, newBranch, version);
+      expect(results.fix).to.have.lengthOf(1);
+    });
+  });
+
+  // use case: A malicious contributor could edit the text of their 'Notes:'
+  // in the PR body after a PR's been merged and the maintainers have moved on.
+  // So instead always use the release-clerk PR comment
+  it('uses the release-clerk text', async function () {
+    // realText source: ${fixtureDir}/electron-electron-issue-21891-comments
+    const realText = 'Added GUID parameter to Tray API to avoid system tray icon demotion on Windows';
+    const testCommit = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: lole u got troled hard (#21891)');
+    const version = 'v9.0.0';
+
+    gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
+    const results: any = await notes.get(oldBranch, newBranch, version);
+    expect(results.feat).to.have.lengthOf(1);
+    expect(results.feat[0].hash).to.equal(testCommit.sha1);
+    expect(results.feat[0].note).to.equal(realText);
+  });
+
+  // test that when you feed in different semantic commit types,
+  // the parser returns them in the results' correct category
+  describe('semantic commit', () => {
+    const version = 'v9.0.0';
+
+    it("honors 'feat' type", async function () {
+      const testCommit = newFeat;
+      gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
+      const results: any = await notes.get(oldBranch, newBranch, version);
+      expect(results.feat).to.have.lengthOf(1);
+      expect(results.feat[0].hash).to.equal(testCommit.sha1);
+    });
+
+    it("honors 'fix' type", async function () {
+      const testCommit = newFix;
+      gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
+      const results: any = await notes.get(oldBranch, newBranch, version);
+      expect(results.fix).to.have.lengthOf(1);
+      expect(results.fix[0].hash).to.equal(testCommit.sha1);
+    });
+
+    it("honors 'BREAKING CHANGE' message", async function () {
+      const testCommit = newBreaking;
+      gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
+      const results: any = await notes.get(oldBranch, newBranch, version);
+      expect(results.breaking).to.have.lengthOf(1);
+      expect(results.breaking[0].hash).to.equal(testCommit.sha1);
+    });
+  });
+});

+ 116 - 0
spec-main/yarn.lock

@@ -2,11 +2,59 @@
 # yarn lockfile v1
 
 
+"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
+  integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==
+  dependencies:
+    type-detect "4.0.8"
+
+"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
+  integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
+"@sinonjs/formatio@^5.0.1":
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089"
+  integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==
+  dependencies:
+    "@sinonjs/commons" "^1"
+    "@sinonjs/samsam" "^5.0.2"
+
+"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938"
+  integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==
+  dependencies:
+    "@sinonjs/commons" "^1.6.0"
+    lodash.get "^4.4.2"
+    type-detect "^4.0.8"
+
+"@sinonjs/text-encoding@^0.7.1":
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
+  integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
+
 "@types/node@*":
   version "13.7.0"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4"
   integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==
 
+"@types/sinon@^9.0.4":
+  version "9.0.4"
+  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1"
+  integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw==
+  dependencies:
+    "@types/sinonjs__fake-timers" "*"
+
+"@types/sinonjs__fake-timers@*":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
+  integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
+
 "@types/ws@^7.2.0":
   version "7.2.1"
   resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.1.tgz#b800f2b8aee694e2b581113643e20d79dd3b8556"
@@ -60,6 +108,11 @@ [email protected]:
   dependencies:
     streamsearch "0.1.2"
 
+diff@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
 dirty-chai@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/dirty-chai/-/dirty-chai-2.0.1.tgz#6b2162ef17f7943589da840abc96e75bda01aff3"
@@ -83,6 +136,16 @@ fast-json-stable-stringify@^2.0.0:
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
[email protected]:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -95,6 +158,11 @@ json5@^1.0.1:
   dependencies:
     minimist "^1.2.0"
 
+just-extend@^4.0.2:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
+  integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
+
 loader-utils@^1.0.0:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
@@ -104,16 +172,39 @@ loader-utils@^1.0.0:
     emojis-list "^2.0.0"
     json5 "^1.0.1"
 
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
+
 minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
+nise@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913"
+  integrity sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+    "@sinonjs/fake-timers" "^6.0.0"
+    "@sinonjs/text-encoding" "^0.7.1"
+    just-extend "^4.0.2"
+    path-to-regexp "^1.7.0"
+
 node-ensure@^0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
   integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=
 
+path-to-regexp@^1.7.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+  integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+  dependencies:
+    isarray "0.0.1"
+
 pdfjs-dist@^2.2.228:
   version "2.2.228"
   resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.2.228.tgz#777b068a0a16c96418433303807c183058b47aaa"
@@ -140,11 +231,36 @@ schema-utils@^0.4.0:
     ajv "^6.1.0"
     ajv-keywords "^3.1.0"
 
+sinon@^9.0.1:
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d"
+  integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==
+  dependencies:
+    "@sinonjs/commons" "^1.7.2"
+    "@sinonjs/fake-timers" "^6.0.1"
+    "@sinonjs/formatio" "^5.0.1"
+    "@sinonjs/samsam" "^5.0.3"
+    diff "^4.0.2"
+    nise "^4.0.1"
+    supports-color "^7.1.0"
+
 [email protected]:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
   integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
 
+supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  dependencies:
+    has-flag "^4.0.0"
+
[email protected], type-detect@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
 uri-js@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"

Some files were not shown because too many files changed in this diff