b39a5b71fe
Trop annotations are in the form of "(Also in 7.3, 8, 9)" with links to
the sibling branches.
Previously seen in b43e601b83
but is now
free of optional chaining and nullish coalescing, to run on Node < 14 :)
183 lines
7.3 KiB
TypeScript
183 lines
7.3 KiB
TypeScript
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 if (args.length === 6 &&
|
|
args[0] === 'branch' &&
|
|
args[1] === '--all' &&
|
|
args[2] === '--contains' &&
|
|
args[3].endsWith('-x-y')) {
|
|
// "what branch is this tag in?"
|
|
// git branch --all --contains ${ref} --sort version:refname
|
|
stdout = args[3];
|
|
} 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('trop annotations', () => {
|
|
it('shows sibling branches', 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(1);
|
|
console.log(results.fix);
|
|
expect(results.fix[0].trops).to.have.keys('8-x-y', '9-x-y');
|
|
});
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
});
|