From 632e1ceda61bd461835f7a0240107f852b2d34cd Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 3 Dec 2013 10:27:45 -0800 Subject: [PATCH] Do the heavy lifting for git commit hooks Summary: Ref T4189. This doesn't add any rules yet, but does all the heavy lifting to figure out what's changed and put it in a consuamble (if somewhat ad-hoc) datastructure, which lists all the ref and tag modifications and all the new commits in a consistent way. From here, it should be fairly straightforward to add top-level rules (e.g., ff pushes only). Test Plan: Output is huge, see comments. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T4189 Differential Revision: https://secure.phabricator.com/D7687 --- .../engine/DiffusionCommitHookEngine.php | 136 +++++++++++++++--- 1 file changed, 115 insertions(+), 21 deletions(-) diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index 7a6855de46..b4a5c1e5fd 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -61,14 +61,128 @@ final class DiffusionCommitHookEngine extends Phobject { return $err; } + /** + * @task git + */ private function executeGitHook() { $updates = $this->parseGitUpdates($this->getStdin()); - // TODO: Do useful things. + // TODO: Do cheap checks: non-ff commits, mutating refs without access, + // creating or deleting things you can't touch. We can do all non-content + // checks here. + + $updates = $this->findGitNewCommits($updates); + + // TODO: Now, do content checks. return 0; } + + /** + * @task git + */ + private function parseGitUpdates($stdin) { + $updates = array(); + + $lines = phutil_split_lines($stdin, $retain_endings = false); + foreach ($lines as $line) { + $parts = explode(' ', $line, 3); + if (count($parts) != 3) { + throw new Exception(pht('Expected "old new ref", got "%s".', $line)); + } + $update = array( + 'old' => $parts[0], + 'old.short' => substr($parts[0], 0, 8), + 'new' => $parts[1], + 'new.short' => substr($parts[1], 0, 8), + 'ref' => $parts[2], + ); + + if (preg_match('(^refs/heads/)', $update['ref'])) { + $update['type'] = 'branch'; + } else if (preg_match('(^refs/tags/)', $update['ref'])) { + $update['type'] = 'tag'; + } else { + $update['type'] = 'unknown'; + } + + $updates[] = $update; + } + + $updates = $this->findGitMergeBases($updates); + + return $updates; + } + + + /** + * @task git + */ + private function findGitMergeBases(array $updates) { + $empty = str_repeat('0', 40); + + $futures = array(); + foreach ($updates as $key => $update) { + // Updates are in the form: + // + // + // + // If the old hash is "00000...", the ref is being created (either a new + // branch, or a new tag). If the new hash is "00000...", the ref is being + // deleted. If both are nonempty, the ref is being updated. For updates, + // we'll figure out the `merge-base` of the old and new objects here. This + // lets us reject non-FF changes cheaply; later, we'll figure out exactly + // which commits are new. + + if ($update['old'] == $empty) { + $updates[$key]['operation'] = 'create'; + } else if ($update['new'] == $empty) { + $updates[$key]['operation'] = 'delete'; + } else { + $updates[$key]['operation'] = 'update'; + $futures[$key] = $this->getRepository()->getLocalCommandFuture( + 'merge-base %s %s', + $update['old'], + $update['new']); + } + } + + foreach (Futures($futures)->limit(8) as $key => $future) { + list($stdout) = $future->resolvex(); + $updates[$key]['merge-base'] = rtrim($stdout, "\n"); + } + + return $updates; + } + + private function findGitNewCommits(array $updates) { + $futures = array(); + foreach ($updates as $key => $update) { + if ($update['type'] == 'delete') { + // Deleting a branch or tag can never create any new commits. + continue; + } + + // NOTE: This piece of magic finds all new commits, by walking backward + // from the new value to the value of *any* existing ref in the + // repository. Particularly, this will cover the cases of a new branch, a + // completely moved tag, etc. + $futures[$key] = $this->getRepository()->getLocalCommandFuture( + 'log --format=%s %s --not --all', + '%H', + $update['new']); + } + + foreach (Futures($futures)->limit(8) as $key => $future) { + list($stdout) = $future->resolvex(); + $commits = phutil_split_lines($stdout, $retain_newlines = false); + $updates[$key]['commits'] = $commits; + } + + return $updates; + } + private function executeSubversionHook() { // TODO: Do useful things here, too. @@ -82,24 +196,4 @@ final class DiffusionCommitHookEngine extends Phobject { return 0; } - - private function parseGitUpdates($stdin) { - $updates = array(); - - $lines = phutil_split_lines($stdin, $retain_endings = false); - foreach ($lines as $line) { - $parts = explode(' ', $line, 3); - if (count($parts) != 3) { - throw new Exception(pht('Expected "old new ref", got "%s".', $line)); - } - $updates[] = array( - 'old' => $parts[0], - 'new' => $parts[1], - 'ref' => $parts[2], - ); - } - - return $updates; - } - }