diff --git a/scripts/report-unmerged-branches.mjs b/scripts/report-unmerged-branches.mjs index 241d08f..aac4057 100644 --- a/scripts/report-unmerged-branches.mjs +++ b/scripts/report-unmerged-branches.mjs @@ -42,6 +42,15 @@ export function inspectRepository(repository, { fetch = false } = {}) { const output = git(repository.path, 'for-each-ref', '--format=%(refname)%00%(objectname)%00%(symref)', `refs/remotes/${remote}/`); const cache = new Map(); const knownEquivalent = new Set(); + const mergeBases = new Map(); + const isolatedEquivalence = new Map(); + const mergeBase = sha => { + if (!mergeBases.has(sha)) { + try { mergeBases.set(sha, git(repository.path, 'merge-base', result.targetSha, sha)); } + catch (error) { if (error.status === 1 && !String(error.stderr || '').trim()) mergeBases.set(sha, null); else throw error; } + } + return mergeBases.get(sha); + }; for (const line of output.split('\n').filter(Boolean)) { const [ref, sha, symbolicRef] = line.split('\0'); if (symbolicRef || ref === targetRef) continue; @@ -49,20 +58,24 @@ export function inspectRepository(repository, { fetch = false } = {}) { try { let comparison = cache.get(sha); if (!comparison) { - const range = `${targetRef}..${ref}`; + // Pin the inventory SHAs so another fetch cannot change this comparison midway. + const range = `${result.targetSha}..${sha}`; const ahead = Number(git(repository.path, 'rev-list', '--count', range)); comparison = { ahead, unmergedCommits: [], equivalentCommits: [], mergeCommits: [] }; if (ahead > 0) { - const cherry = git(repository.path, 'cherry', targetRef, ref); + const cherry = git(repository.path, 'cherry', result.targetSha, sha); const equivalence = new Map(cherry.split('\n').filter(Boolean).map(line => [line.slice(2), line[0]])); comparison.mergeCommits = commits(repository.path, range, '--merges'); for (const commit of commits(repository.path, range, '--no-merges')) { if (equivalence.get(commit.sha) === '-') knownEquivalent.add(commit.sha); // If the branch merged the target, Git's merge base can hide older // upstream patches. Compare the individual incoming commit too. - if (comparison.mergeCommits.length && !knownEquivalent.has(commit.sha)) { - const isolated = git(repository.path, 'cherry', targetRef, commit.sha); - for (const line of isolated.split('\n')) if (line.startsWith('- ')) knownEquivalent.add(line.slice(2)); + if (comparison.mergeCommits.length && !knownEquivalent.has(commit.sha) && mergeBase(sha) !== mergeBase(commit.sha)) { + if (!isolatedEquivalence.has(commit.sha)) { + const isolated = git(repository.path, 'cherry', result.targetSha, commit.sha); + isolatedEquivalence.set(commit.sha, isolated.split('\n').includes('- ' + commit.sha)); + } + if (isolatedEquivalence.get(commit.sha)) knownEquivalent.add(commit.sha); } // A changed SHA alone does not mean the change is absent from the target branch. const list = knownEquivalent.has(commit.sha) ? comparison.equivalentCommits : comparison.unmergedCommits;