Skip to content

Commit

Permalink
Merge pull request chromaui#174 from chromaui/1132-support-squash-merges
Browse files Browse the repository at this point in the history
Support squash merges natively
  • Loading branch information
tmeasday authored Sep 8, 2020
2 parents 4abbae9 + 189079f commit a7140b7
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 83 deletions.
5 changes: 1 addition & 4 deletions bin/git/getCommitAndBranch.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import setupDebug from 'debug';
import envCi from 'env-ci';
import dedent from 'ts-dedent';

import { getBranch, getCommit } from './git';

export const debug = setupDebug('chromatic-cli:tester');

const notHead = b => {
if (!b || b === 'HEAD') {
return false;
Expand Down Expand Up @@ -101,7 +98,7 @@ export async function getCommitAndBranch({ patchBaseRef, inputFromCI, log } = {}
!!process.env.CI ||
!!process.env.REPOSITORY_URL ||
!!process.env.GITHUB_REPOSITORY;
debug(
log.debug(
`git info: ${JSON.stringify({
commit,
committedAt,
Expand Down
98 changes: 69 additions & 29 deletions bin/git/git.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import setupDebug from 'debug';
import execa from 'execa';
import gql from 'fake-tag';
import { EOL } from 'os';
import dedent from 'ts-dedent';

const debug = setupDebug('chromatic-cli:git');

async function execGitCommand(command) {
try {
const { all } = await execa.command(command, {
Expand Down Expand Up @@ -50,7 +47,7 @@ async function execGitCommand(command) {
export const FETCH_N_INITIAL_BUILD_COMMITS = 20;

const TesterFirstCommittedAtQuery = gql`
query TesterFirstCommittedAtQuery($branch: String!) {
query TesterFirstCommittedAtQuery($commit: String!, $branch: String!) {
app {
firstBuild(sortByCommittedAt: true) {
committedAt
Expand All @@ -59,6 +56,11 @@ const TesterFirstCommittedAtQuery = gql`
commit
committedAt
}
pullRequest(mergeInfo: { commit: $commit, baseRefName: $branch }) {
lastHeadBuild {
commit
}
}
}
}
`;
Expand Down Expand Up @@ -124,6 +126,7 @@ function commitsForCLI(commits) {
// `commitsWithBuilds`.
//
async function nextCommits(
{ log },
limit,
{ firstCommittedAtSeconds, commitsWithBuilds, commitsWithoutBuilds }
) {
Expand All @@ -133,9 +136,9 @@ async function nextCommits(
const command = `git rev-list HEAD \
${firstCommittedAtSeconds ? `--since ${firstCommittedAtSeconds}` : ''} \
-n ${limit + commitsWithoutBuilds.length} --not ${commitsForCLI(commitsWithBuilds)}`;
debug(`running ${command}`);
log.debug(`running ${command}`);
const commits = (await execGitCommand(command)).split('\n').filter(c => !!c);
debug(`command output: ${commits}`);
log.debug(`command output: ${commits}`);

return (
commits
Expand All @@ -148,7 +151,7 @@ async function nextCommits(

// Which of the listed commits are "maximally descendent":
// ie c in commits such that there are no descendents of c in commits.
async function maximallyDescendentCommits(commits) {
async function maximallyDescendentCommits({ log }, commits) {
if (commits.length === 0) {
return commits;
}
Expand All @@ -158,34 +161,34 @@ async function maximallyDescendentCommits(commits) {
// List the tree from <commits> not including the tree from <parentCommits>
// This just filters any commits that are ancestors of other commits
const command = `git rev-list ${commitsForCLI(commits)} --not ${commitsForCLI(parentCommits)}`;
debug(`running ${command}`);
log.debug(`running ${command}`);
const maxCommits = (await execGitCommand(command)).split('\n').filter(c => !!c);
debug(`command output: ${maxCommits}`);
log.debug(`command output: ${maxCommits}`);

return maxCommits;
}

// Exponentially iterate `limit` up to infinity to find a "covering" set of commits with builds
async function step(
client,
{ client, log },
limit,
{ firstCommittedAtSeconds, commitsWithBuilds, commitsWithoutBuilds }
) {
debug(`step: checking ${limit} up to ${firstCommittedAtSeconds}`);
debug(`step: commitsWithBuilds: ${commitsWithBuilds}`);
debug(`step: commitsWithoutBuilds: ${commitsWithoutBuilds}`);
log.debug(`step: checking ${limit} up to ${firstCommittedAtSeconds}`);
log.debug(`step: commitsWithBuilds: ${commitsWithBuilds}`);
log.debug(`step: commitsWithoutBuilds: ${commitsWithoutBuilds}`);

const candidateCommits = await nextCommits(limit, {
const candidateCommits = await nextCommits({ log }, limit, {
firstCommittedAtSeconds,
commitsWithBuilds,
commitsWithoutBuilds,
});

debug(`step: candidateCommits: ${candidateCommits}`);
log.debug(`step: candidateCommits: ${candidateCommits}`);

// No more commits uncovered commitsWithBuilds!
if (candidateCommits.length === 0) {
debug('step: no candidateCommits; we are done');
log.debug('step: no candidateCommits; we are done');
return commitsWithBuilds;
}

Expand All @@ -194,69 +197,106 @@ async function step(
} = await client.runQuery(TesterHasBuildsWithCommitsQuery, {
commits: candidateCommits,
});
debug(`step: newCommitsWithBuilds: ${newCommitsWithBuilds}`);
log.debug(`step: newCommitsWithBuilds: ${newCommitsWithBuilds}`);

const newCommitsWithoutBuilds = candidateCommits.filter(
commit => !newCommitsWithBuilds.find(c => c === commit)
);

return step(client, limit * 2, {
return step({ client, log }, limit * 2, {
firstCommittedAtSeconds,
commitsWithBuilds: [...commitsWithBuilds, ...newCommitsWithBuilds],
commitsWithoutBuilds: [...commitsWithoutBuilds, ...newCommitsWithoutBuilds],
});
}

export async function getBaselineCommits(client, { branch, ignoreLastBuildOnBranch = false } = {}) {
const { committedAt } = await getCommit();
export async function getBaselineCommits(
{ client, log },
{ branch, ignoreLastBuildOnBranch = false } = {}
) {
const { commit, committedAt } = await getCommit();

// Include the latest build from this branch as an ancestor of the current build
const {
app: { firstBuild, lastBuild },
app: { firstBuild, lastBuild, pullRequest },
} = await client.runQuery(TesterFirstCommittedAtQuery, {
branch,
commit,
});
debug(`App firstBuild: ${firstBuild}, lastBuild: ${lastBuild}`);
log.debug(
`App firstBuild: %o, lastBuild: %o, pullRequest: %o`,
firstBuild,
lastBuild,
pullRequest
);

if (!firstBuild) {
debug('App has no builds, returning []');
log.debug('App has no builds, returning []');
return [];
}

const initialCommitsWithBuilds = [];
const extraBaselineCommits = [];

// Don't do any special branching logic for builds on `HEAD`, this is fairly meaningless
// (CI systems that have been pushed tags can not set a branch)
// Add the most recent build on the branch as a (potential) baseline build, unless:
// - the user opts out with `--ignore-last-build-on-branch`
// - the commit is newer than the build we are running, in which case we doing this build out
// of order and that could lead to problems.
// - the current branch is `HEAD`; this is fairly meaningless
// (CI systems that have been pushed tags can not set a branch)
// @see https://www.chromatic.com/docs/branching-and-baselines#rebasing
if (
branch !== 'HEAD' &&
!ignoreLastBuildOnBranch &&
lastBuild &&
lastBuild.committedAt <= committedAt
) {
if (await commitExists(lastBuild.commit)) {
log.debug(`Adding last branch build commit ${lastBuild.commit} to commits with builds`);
initialCommitsWithBuilds.push(lastBuild.commit);
} else {
debug(`Last build commit not in index, blindly appending to baselines`);
log.debug(
`Last branch build commit ${lastBuild.commit} not in index, blindly appending to baselines`
);
extraBaselineCommits.push(lastBuild.commit);
}
}

// Add the most recent build on a (merged) branch if as a (potential) baseline if we think
// this commit was the commit that merged the PR.
// @see https://www.chromatic.com/docs/branching-and-baselines#squash-and-rebase-merging
if (pullRequest && pullRequest.lastHeadBuild) {
if (await commitExists(pullRequest.lastHeadBuild.commit)) {
log.debug(
`Adding merged PR build commit ${pullRequest.lastHeadBuild.commit} to commits with builds`
);
initialCommitsWithBuilds.push(pullRequest.lastHeadBuild.commit);
} else {
log.debug(
`Merged PR build commit ${pullRequest.lastHeadBuild.commit} not in index, blindly appending to baselines`
);
extraBaselineCommits.push(pullRequest.lastHeadBuild.commit);
}
}

// Get a "covering" set of commits that have builds. This is a set of commits
// such that any ancestor of HEAD is either:
// - in commitsWithBuilds
// - an ancestor of a commit in commitsWithBuilds
// - has no build
const commitsWithBuilds = await step(client, FETCH_N_INITIAL_BUILD_COMMITS, {
const commitsWithBuilds = await step({ client, log }, FETCH_N_INITIAL_BUILD_COMMITS, {
firstCommittedAtSeconds: firstBuild.committedAt && firstBuild.committedAt / 1000,
commitsWithBuilds: initialCommitsWithBuilds,
commitsWithoutBuilds: [],
});

debug(`Final commitsWithBuilds: ${commitsWithBuilds}`);
log.debug(`Final commitsWithBuilds: ${commitsWithBuilds}`);

// For any pair A,B of builds, there is no point in using B if it is an ancestor of A.
return [...extraBaselineCommits, ...(await maximallyDescendentCommits(commitsWithBuilds))];
return [
...extraBaselineCommits,
...(await maximallyDescendentCommits({ log }, commitsWithBuilds)),
];
}

/**
Expand Down
Loading

0 comments on commit a7140b7

Please sign in to comment.