Skip to content

Instantly share code, notes, and snippets.

@benjifisher
Created November 6, 2013 14:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benjifisher/7337143 to your computer and use it in GitHub Desktop.
Save benjifisher/7337143 to your computer and use it in GitHub Desktop.
This php command-line script will do a binary search to find the first git commit that breaks your patch. It will give simple usage instructions if you invoke it without arguments. I wrote this script to help in re-rolling patches for Drupal. I posted the original version as a comment on https://drupal.org/patch/reroll . The script makes a reaso…
#!/usr/bin/env php
<?php
// Get the arguments. If there are not two, then print usage info.
if (count($argv) != 3) {
$scriptname = basename($argv[0]);
print("Usage: $scriptname <date> <patchfile>" . PHP_EOL);
print('where <date> is any date understood by git' . PHP_EOL);
print('and <patchfile> applied cleanly on <date>, not today.' . PHP_EOL);
print("Example: $scriptname 2013/05/17 foo.patch" . PHP_EOL);
return;
}
list($scriptpath, $date, $patchfile) = $argv;
// Check that there are no modified files.
exec('git status --porcelain', $git_status);
$modified = preg_filter('/^ M\s*/', '', $git_status);
if ($modified) {
print('Please commit, discard, or stash your changes before running this command.' . PHP_EOL);
print('modified: ' . implode(', ', $modified) . PHP_EOL);
return;
}
// Get a list of commits from $date to present.
exec("git log --reverse --format=format:%H --since=$date", $commits);
$num_commits = count($commits);
print("There are $num_commits commits since $date.\n");
// Before doing any checkouts, figure out how to restore the current state.
exec('git status --short --branch', $status);
if (preg_match('/no branch/', $status[0])) {
exec('git show --format=format:%H', $show);
$git_restore = $show[0];
}
else {
$git_restore = preg_replace('/## /', '', $status[0]);
}
print("git restore: $git_restore\n");
// Check that the patch applies to the oldest commit but not to the newest one.
if (_find_commit_apply_check($commits[$num_commits - 1], $patchfile, $git_restore)) {
print('The patch applies cleanly to the latest commit.' . PHP_EOL);
return;
}
if (!_find_commit_apply_check($commits[0], $patchfile, $git_restore)) {
print("The patch does not apply to the first commit after $date.\n");
print('Perhaps try one day earlier.' . PHP_EOL);
return;
}
// Do a binary search for the commit that breaks the patch.
$clean_commit = 0;
$dirty_commit = $num_commits - 1;
while ($dirty_commit - $clean_commit > 1) {
$pivot = (int) (($clean_commit + $dirty_commit) / 2);
echo "testing commit {$commits[$pivot]}, $pivot of $num_commits.\n";
if (_find_commit_apply_check($commits[$pivot], $patchfile, $git_restore)) {
$clean_commit = $pivot;
}
else {
$dirty_commit = $pivot;
}
}
// Output the result.
echo <<<EOS
The patch $patchfile applies cleanly to commmit {$commits[$clean_commit]}
but not to the following commit {$commits[$dirty_commit]}
EOS;
/**
* Check out $commit, see whether $patch applies, and restore the branch or
* commit $restore. Return TRUE if the patch applied cleanly.
*/
function _find_commit_apply_check($commit, $patch, $restore) {
// Some git commands seem to write to stderr, so redirect it with 2>&1.
// Does this work on Windows? Maybe use proc_open() instead.
exec("git checkout $commit 2>&1");
exec("git apply --check $patch 2>&1", $check);
$errors = preg_filter('/^error:/', '', $check);
exec("git checkout $restore 2>&1");
return empty($errors);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment