Unverified Commit fa3a37c7 authored by Nate Bosch's avatar Nate Bosch Committed by GitHub

Use merge-base in find_commit.dart (#138033)

Closes #97595

The prior approach of manually diffing the entire log chain is less
efficient, and only found the original branch point ignoring subsequent
merges. The limitation forced PR workflows into rebasing and force
pushing new history to get the branch point far enough for CI to pass.

Use `git merge-base` to find the latest common commit with the main
branch.
Add an `allowFailure` argument to the `git` utility to use a more
specific failure in the case of no shared history when this command will
fail with a generic error.

Use `^branch` with the `git log` commands to exclude shared history and
more easily count the unique commits on each branch.

Drop the `Commit` abstraction. Parse directly to timestamp or line counts.
parent ed928289
...@@ -18,23 +18,9 @@ void log(String message) { ...@@ -18,23 +18,9 @@ void log(String message) {
} }
} }
class Commit { const String _commitTimestampFormat = '--format=%cI';
Commit(this.hash, this.timestamp); DateTime _parseTimestamp(String line) => DateTime.parse(line.trim());
int _countLines(String output) => output.trim().split('/n').where((String line) => line.isNotEmpty).length;
final String hash;
final DateTime timestamp;
static String formatArgument = '--format=%H %cI';
static Commit parse(String line) {
final int space = line.indexOf(' ');
return Commit(line.substring(0, space), DateTime.parse(line.substring(space+1, line.length).trimRight()));
}
static List<Commit> parseList(String lines) {
return lines.split('\n').where((String line) => line.isNotEmpty).map(parse).toList().reversed.toList();
}
}
String findCommit({ String findCommit({
required String primaryRepoDirectory, required String primaryRepoDirectory,
...@@ -43,41 +29,39 @@ String findCommit({ ...@@ -43,41 +29,39 @@ String findCommit({
required String secondaryRepoDirectory, required String secondaryRepoDirectory,
required String secondaryBranch, required String secondaryBranch,
}) { }) {
final Commit anchor; final DateTime anchor;
if (primaryBranch == primaryTrunk) { if (primaryBranch == primaryTrunk) {
log('on $primaryTrunk, using last commit as anchor'); log('on $primaryTrunk, using last commit time');
anchor = Commit.parse(git(primaryRepoDirectory, <String>['log', Commit.formatArgument, '--max-count=1', primaryBranch, '--'])); anchor = _parseTimestamp(git(primaryRepoDirectory, <String>['log', _commitTimestampFormat, '--max-count=1', primaryBranch, '--']));
} else { } else {
final List<Commit> branchCommits = Commit.parseList(git(primaryRepoDirectory, <String>['log', Commit.formatArgument, primaryBranch, '--'])); final String mergeBase = git(primaryRepoDirectory, <String>['merge-base', primaryBranch, primaryTrunk], allowFailure: true).trim();
final List<Commit> trunkCommits = Commit.parseList(git(primaryRepoDirectory, <String>['log', Commit.formatArgument, primaryTrunk, '--'])); if (mergeBase.isEmpty) {
if (branchCommits.isEmpty || trunkCommits.isEmpty || branchCommits.first.hash != trunkCommits.first.hash) {
throw StateError('Branch $primaryBranch does not seem to have a common history with trunk $primaryTrunk.'); throw StateError('Branch $primaryBranch does not seem to have a common history with trunk $primaryTrunk.');
} }
if (branchCommits.last.hash == trunkCommits.last.hash) { anchor = _parseTimestamp(git(primaryRepoDirectory, <String>['log', _commitTimestampFormat, '--max-count=1', mergeBase, '--']));
log('$primaryBranch is even with $primaryTrunk, using last commit as anchor'); if (debugLogging) {
anchor = trunkCommits.last; final int missingTrunkCommits = _countLines(git(primaryRepoDirectory, <String>['rev-list', primaryTrunk, '^$primaryBranch', '--']));
} else { final int extraCommits = _countLines(git(primaryRepoDirectory, <String>['rev-list', primaryBranch, '^$primaryTrunk', '--']));
int index = 0; if (missingTrunkCommits == 0 && extraCommits == 0) {
while (branchCommits.length > index && trunkCommits.length > index && branchCommits[index].hash == trunkCommits[index].hash) { log('$primaryBranch is even with $primaryTrunk at $mergeBase');
index += 1; } else {
log('$primaryBranch branched from $primaryTrunk $missingTrunkCommits commits ago, trunk has advanced by $extraCommits commits since then.');
} }
log('$primaryBranch branched from $primaryTrunk ${branchCommits.length - index} commits ago, trunk has advanced by ${trunkCommits.length - index} commits since then.');
anchor = trunkCommits[index - 1];
} }
} }
return git(secondaryRepoDirectory, <String>[ return git(secondaryRepoDirectory, <String>[
'log', 'log',
'--format=%H', '--format=%H',
'--until=${anchor.timestamp.toIso8601String()}', '--until=${anchor.toIso8601String()}',
'--max-count=1', '--max-count=1',
secondaryBranch, secondaryBranch,
'--', '--',
]); ]);
} }
String git(String workingDirectory, List<String> arguments) { String git(String workingDirectory, List<String> arguments, {bool allowFailure = false}) {
final ProcessResult result = Process.runSync('git', arguments, workingDirectory: workingDirectory); final ProcessResult result = Process.runSync('git', arguments, workingDirectory: workingDirectory);
if (result.exitCode != 0 || '${result.stderr}'.isNotEmpty) { if (!allowFailure && result.exitCode != 0 || '${result.stderr}'.isNotEmpty) {
throw ProcessException('git', arguments, '${result.stdout}${result.stderr}', result.exitCode); throw ProcessException('git', arguments, '${result.stdout}${result.stderr}', result.exitCode);
} }
return '${result.stdout}'; return '${result.stdout}';
...@@ -89,7 +73,7 @@ void main(List<String> arguments) { ...@@ -89,7 +73,7 @@ void main(List<String> arguments) {
'Usage: dart find_commit.dart [<path-to-primary-repo>] <path-to-secondary-repo>\n' 'Usage: dart find_commit.dart [<path-to-primary-repo>] <path-to-secondary-repo>\n'
'This script will find the commit in the secondary repo that was contemporary\n' 'This script will find the commit in the secondary repo that was contemporary\n'
'when the commit in the primary repo was created. If that commit is on a\n' 'when the commit in the primary repo was created. If that commit is on a\n'
"branch, then the date of the branch's creation is used instead.\n" "branch, then the date of the branch's last merge is used instead.\n"
'If <path-to-primary-repo> is omitted, the current directory is used for the\n' 'If <path-to-primary-repo> is omitted, the current directory is used for the\n'
'primary repo.' 'primary repo.'
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment