public static function newFromRawCorpus($corpus) { $obj = new ArcanistDifferentialCommitMessage(); $obj->rawCorpus = $corpus; $obj->revisionID = $obj->parseRevisionIDFromRawCorpus($corpus); $pattern = '/^git-svn-id:\\s*([^@]+)@(\\d+)\\s+(.*)$/m'; $match = null; if (preg_match($pattern, $corpus, $match)) { $obj->gitSVNBaseRevision = $match[1] . '@' . $match[2]; $obj->gitSVNBasePath = $match[1]; $obj->gitSVNUUID = $match[3]; } return $obj; }
public function run() { $working_copy = $this->getWorkingCopy(); if (!$working_copy->getProjectID()) { throw new ArcanistUsageException("You have installed a git pre-receive hook in a remote without an " . ".arcconfig."); } // Git repositories have special rules in pre-receive hooks. We need to // construct the API against the .git directory instead of the project // root or commands don't work properly. $repository_api = ArcanistGitAPI::newHookAPI($_SERVER['PWD']); $root = $working_copy->getProjectRoot(); $parser = new ArcanistDiffParser(); $mark_revisions = array(); $stdin = file_get_contents('php://stdin'); $commits = array_filter(explode("\n", $stdin)); foreach ($commits as $commit) { list($old_ref, $new_ref, $refname) = explode(' ', $commit); list($log) = execx('(cd %s && git log -n1 %s)', $repository_api->getPath(), $new_ref); $message_log = reset($parser->parseDiff($log)); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($message_log->getMetadata('message')); $revision_id = $message->getRevisionID(); if ($revision_id) { $mark_revisions[] = $revision_id; } // TODO: Do commit message junk. $info = $repository_api->getPreReceiveHookStatus($old_ref, $new_ref); $paths = ipull($info, 'mask'); $frefs = ipull($info, 'ref'); $data = array(); foreach ($paths as $path => $mask) { list($stdout) = execx('(cd %s && git cat-file blob %s)', $repository_api->getPath(), $frefs[$path]); $data[$path] = $stdout; } // TODO: Do commit content junk. $commit_name = $new_ref; if ($revision_id) { $commit_name = 'D' . $revision_id . ' (' . $commit_name . ')'; } echo "[arc pre-receive] {$commit_name} OK...\n"; } $conduit = $this->getConduit(); $futures = array(); foreach ($mark_revisions as $revision_id) { $futures[] = $conduit->callMethod('differential.close', array('revisionID' => $revision_id)); } Futures($futures)->resolveAll(); return 0; }
/** * Based on the 'git show' output extracts the commit date, author, * subject nad Differential revision . * 'Differential Revision:' * * @param string message output of git show -s --format="format:%ct%n%cn%n%b" */ public function parseCommitMessage($message) { $message_lines = explode("\n", trim($message)); $this->commitTime = $message_lines[0]; $this->commitAuthor = $message_lines[1]; $this->commitSubject = trim($message_lines[2]); $this->revisionID = ArcanistDifferentialCommitMessage::newFromRawCorpus($message)->getRevisionID(); }
public function resolveBaseCommitRule($rule, $source) { list($type, $name) = explode(':', $rule, 2); switch ($type) { case 'git': $matches = null; if (preg_match('/^merge-base\\((.+)\\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal('merge-base %s HEAD', $matches[1]); if (!$err) { $this->setBaseCommitExplanation("it is the merge-base of '{$matches[1]}' and HEAD, as " . "specified by '{$rule}' in your {$source} 'base' " . "configuration."); return trim($merge_base); } } else { if (preg_match('/^branch-unique\\((.+)\\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal('merge-base %s HEAD', $matches[1]); if ($err) { return null; } $merge_base = trim($merge_base); list($commits) = $this->execxLocal('log --format=%C %s..HEAD --', '%H', $merge_base); $commits = array_filter(explode("\n", $commits)); if (!$commits) { return null; } $commits[] = $merge_base; $head_branch_count = null; foreach ($commits as $commit) { list($branches) = $this->execxLocal('branch --contains %s', $commit); $branches = array_filter(explode("\n", $branches)); if ($head_branch_count === null) { // If this is the first commit, it's HEAD. Count how many // branches it is on; we want to include commits on the same // number of branches. This covers a case where this branch // has sub-branches and we're running "arc diff" here again // for whatever reason. $head_branch_count = count($branches); } else { if (count($branches) > $head_branch_count) { foreach ($branches as $key => $branch) { $branches[$key] = trim($branch, ' *'); } $branches = implode(', ', $branches); $this->setBaseCommitExplanation("it is the first commit between '{$merge_base}' (the " . "merge-base of '{$matches[1]}' and HEAD) which is also " . "contained by another branch ({$branches})."); return $commit; } } } } else { list($err) = $this->execManualLocal('cat-file -t %s', $name); if (!$err) { $this->setBaseCommitExplanation("it is specified by '{$rule}' in your {$source} 'base' " . "configuration."); return $name; } } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation("you specified '{$rule}' in your {$source} 'base' " . "configuration."); return self::GIT_MAGIC_ROOT_COMMIT; case 'amended': $text = $this->getCommitMessage('HEAD'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation("HEAD has been amended with 'Differential Revision:', " . "as specified by '{$rule}' in your {$source} 'base' " . "configuration."); return 'HEAD^'; } break; case 'upstream': list($err, $upstream) = $this->execManualLocal('rev-parse --abbrev-ref --symbolic-full-name %s', '@{upstream}'); if (!$err) { $upstream = rtrim($upstream); list($upstream_merge_base) = $this->execxLocal('merge-base %s HEAD', $upstream); $upstream_merge_base = rtrim($upstream_merge_base); $this->setBaseCommitExplanation("it is the merge-base of the upstream of the current branch " . "and HEAD, and matched the rule '{$rule}' in your {$source} " . "'base' configuration."); return $upstream_merge_base; } break; case 'this': $this->setBaseCommitExplanation("you specified '{$rule}' in your {$source} 'base' " . "configuration."); return 'HEAD^'; } default: return null; } return null; }
/** * Retrieve the git messages between HEAD and the last update. * * @task message */ private function getGitUpdateMessage() { $repository_api = $this->getRepositoryAPI(); $parser = $this->newDiffParser(); $commit_messages = $repository_api->getGitCommitLog(); $commit_messages = $parser->parseDiff($commit_messages); if (count($commit_messages) == 1) { // If there's only one message, assume this is an amend-based workflow and // that using it to prefill doesn't make sense. return null; } // We have more than one message, so figure out which ones are new. We // do this by pulling the current diff and comparing commit hashes in the // working copy with attached commit hashes. It's not super important that // we always get this 100% right, we're just trying to do something // reasonable. $local = $this->loadActiveLocalCommitInfo(); $hashes = ipull($local, null, 'commit'); $usable = array(); foreach ($commit_messages as $message) { $text = $message->getMetadata('message'); $parsed = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); if ($parsed->getRevisionID()) { // If this is an amended commit message with a revision ID, it's // certainly not new. Stop marking commits as usable and break out. break; } if (isset($hashes[$message->getCommitHash()])) { // If this commit is currently part of the diff, stop using commit // messages, since anything older than this isn't new. break; } // Otherwise, this looks new, so it's a usable commit message. $usable[] = $text; } if (!$usable) { // No new commit messages, so we don't have anywhere to start from. return null; } return $this->formatUsableLogs($usable); }
private function loadCommitInfo(array $branches, ArcanistRepositoryAPI $repository_api) { $futures = array(); foreach ($branches as $branch) { // NOTE: "-s" is an option deep in git's diff argument parser that doesn't // seem to have much documentation and has no long form. It suppresses any // diff output. $futures[$branch['name']] = $repository_api->execFutureLocal('show -s --format=%C %s', '%H%x01%ct%x01%T%x01%s%x01%b', $branch['name']); } $branches = ipull($branches, null, 'name'); $commit_map = array(); foreach (Futures($futures) as $name => $future) { list($info) = $future->resolvex(); list($hash, $epoch, $tree, $desc, $text) = explode("", trim($info), 5); $branch = $branches[$name]; $branch['hash'] = $hash; $branch['desc'] = $desc; try { $text = $desc . "\n" . $text; $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); $id = $message->getRevisionID(); $branch += array('epoch' => (int) $epoch, 'tree' => $tree, 'revisionID' => $id); } catch (ArcanistUsageException $ex) { // In case of invalid commit message which fails the parsing, // do nothing. } $commit_map[$hash] = $branch; } return $commit_map; }
public function resolveBaseCommitRule($rule, $source) { list($type, $name) = explode(':', $rule, 2); switch ($type) { case 'git': $matches = null; if (preg_match('/^merge-base\\((.+)\\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal('merge-base %s HEAD', $matches[1]); if (!$err) { $this->setBaseCommitExplanation(pht("it is the merge-base of '%s' and HEAD, as specified by " . "'%s' in your %s 'base' configuration.", $matches[1], $rule, $source)); return trim($merge_base); } } else { if (preg_match('/^branch-unique\\((.+)\\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal('merge-base %s HEAD', $matches[1]); if ($err) { return null; } $merge_base = trim($merge_base); list($commits) = $this->execxLocal('log --format=%C %s..HEAD --', '%H', $merge_base); $commits = array_filter(explode("\n", $commits)); if (!$commits) { return null; } $commits[] = $merge_base; $head_branch_count = null; $all_branch_names = ipull($this->getAllBranches(), 'name'); foreach ($commits as $commit) { // Ideally, we would use something like "for-each-ref --contains" // to get a filtered list of branches ready for script consumption. // Instead, try to get predictable output from "branch --contains". list($branches) = $this->execxLocal('-c column.ui=never -c color.ui=never branch --contains %s', $commit); $branches = array_filter(explode("\n", $branches)); // Filter the list, removing the "current" marker (*) and ignoring // anything other than known branch names (mainly, any possible // "detached HEAD" or "no branch" line). foreach ($branches as $key => $branch) { $branch = trim($branch, ' *'); if (in_array($branch, $all_branch_names)) { $branches[$key] = $branch; } else { unset($branches[$key]); } } if ($head_branch_count === null) { // If this is the first commit, it's HEAD. Count how many // branches it is on; we want to include commits on the same // number of branches. This covers a case where this branch // has sub-branches and we're running "arc diff" here again // for whatever reason. $head_branch_count = count($branches); } else { if (count($branches) > $head_branch_count) { $branches = implode(', ', $branches); $this->setBaseCommitExplanation(pht("it is the first commit between '%s' (the merge-base of " . "'%s' and HEAD) which is also contained by another branch " . "(%s).", $merge_base, $matches[1], $branches)); return $commit; } } } } else { list($err) = $this->execManualLocal('cat-file -t %s', $name); if (!$err) { $this->setBaseCommitExplanation(pht("it is specified by '%s' in your %s 'base' configuration.", $rule, $source)); return $name; } } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation(pht("you specified '%s' in your %s 'base' configuration.", $rule, $source)); return self::GIT_MAGIC_ROOT_COMMIT; case 'amended': $text = $this->getCommitMessage('HEAD'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation(pht("HEAD has been amended with 'Differential Revision:', " . "as specified by '%s' in your %s 'base' configuration.", $rule, $source)); return 'HEAD^'; } break; case 'upstream': list($err, $upstream) = $this->execManualLocal('rev-parse --abbrev-ref --symbolic-full-name %s', '@{upstream}'); if (!$err) { $upstream = rtrim($upstream); list($upstream_merge_base) = $this->execxLocal('merge-base %s HEAD', $upstream); $upstream_merge_base = rtrim($upstream_merge_base); $this->setBaseCommitExplanation(pht("it is the merge-base of the upstream of the current branch " . "and HEAD, and matched the rule '%s' in your %s " . "'base' configuration.", $rule, $source)); return $upstream_merge_base; } break; case 'this': $this->setBaseCommitExplanation(pht("you specified '%s' in your %s 'base' configuration.", $rule, $source)); return 'HEAD^'; } default: return null; } return null; }
private function shouldAmend() { $api = $this->getRepositoryAPI(); if ($this->isHistoryImmutable() || !$api->supportsAmend()) { return false; } $commits = $api->getLocalCommitInformation(); if (!$commits) { return false; } $commit = reset($commits); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($commit['message']); if ($message->getGitSVNBaseRevision()) { return false; } if ($api->getAuthor() != $commit['author']) { return false; } if ($message->getRevisionID() && $this->getArgument('create')) { return false; } // TODO: Check commits since tracking branch. If empty then return false. $repository = $this->loadProjectRepository(); if ($repository) { $callsign = $repository['callsign']; $known_commits = $this->getConduit()->callMethodSynchronous('diffusion.getcommits', array('commits' => array('r' . $callsign . $commit['commit']))); if (ifilter($known_commits, 'error', $negate = true)) { return false; } } if (!$message->getRevisionID()) { return true; } $in_working_copy = $api->loadWorkingCopyDifferentialRevisions($this->getConduit(), array('authors' => array($this->getUserPHID()), 'status' => 'status-open')); if ($in_working_copy) { return true; } return false; }
private function loadCommitInfo(array $branches) { $repository_api = $this->getRepositoryAPI(); $futures = array(); foreach ($branches as $branch) { if ($repository_api instanceof ArcanistMercurialAPI) { $futures[$branch['name']] = $repository_api->execFutureLocal('log -l 1 --template %s -r %s', "{node}{date|hgdate}{p1node}{desc|firstline}{desc}", hgsprintf('%s', $branch['name'])); } else { // NOTE: "-s" is an option deep in git's diff argument parser that // doesn't seem to have much documentation and has no long form. It // suppresses any diff output. $futures[$branch['name']] = $repository_api->execFutureLocal('show -s --format=%C %s --', '%H%x01%ct%x01%T%x01%s%x01%s%n%n%b', $branch['name']); } } $branches = ipull($branches, null, 'name'); $futures = id(new FutureIterator($futures))->limit(16); foreach ($futures as $name => $future) { list($info) = $future->resolvex(); list($hash, $epoch, $tree, $desc, $text) = explode("", trim($info), 5); $branch = $branches[$name] + array('hash' => $hash, 'desc' => $desc, 'tree' => $tree, 'epoch' => (int) $epoch); try { $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); $id = $message->getRevisionID(); $branch['revisionID'] = $id; } catch (ArcanistUsageException $ex) { // In case of invalid commit message which fails the parsing, // do nothing. $branch['revisionID'] = null; } $branches[$name] = $branch; } return $branches; }
private function calculateShouldAmend() { $api = $this->getRepositoryAPI(); if ($this->isHistoryImmutable() || !$api->supportsAmend()) { return false; } $commits = $api->getLocalCommitInformation(); if (!$commits) { return false; } $commit = reset($commits); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($commit['message']); if ($message->getGitSVNBaseRevision()) { return false; } if ($api->getAuthor() != $commit['author']) { return false; } if ($message->getRevisionID() && $this->getArgument('create')) { return false; } // TODO: Check commits since tracking branch. If empty then return false. // Don't amend the current commit if it has already been published. $repository = $this->loadProjectRepository(); if ($repository) { $callsign = $repository['callsign']; $commit_name = 'r' . $callsign . $commit['commit']; $result = $this->getConduit()->callMethodSynchronous('diffusion.querycommits', array('names' => array($commit_name))); $known_commit = idx($result['identifierMap'], $commit_name); if ($known_commit) { return false; } } if (!$message->getRevisionID()) { return true; } $in_working_copy = $api->loadWorkingCopyDifferentialRevisions($this->getConduit(), array('authors' => array($this->getUserPHID()), 'status' => 'status-open')); if ($in_working_copy) { return true; } return false; }
public function resolveBaseCommitRule($rule, $source) { list($type, $name) = explode(':', $rule, 2); // NOTE: This function MUST return node hashes or symbolic commits (like // branch names or the word "tip"), not revsets. This includes ".^" and // similar, which a revset, not a symbolic commit identifier. If you return // a revset it will be escaped later and looked up literally. switch ($type) { case 'hg': $matches = null; if (preg_match('/^gca\\((.+)\\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal('log --template={node} --rev %s', sprintf('ancestor(., %s)', $matches[1])); if (!$err) { $this->setBaseCommitExplanation(pht("it is the greatest common ancestor of '%s' and %s, as " . "specified by '%s' in your %s 'base' configuration.", $matches[1], '.', $rule, $source)); return trim($merge_base); } } else { list($err, $commit) = $this->execManualLocal('log --template {node} --rev %s', hgsprintf('%s', $name)); if ($err) { list($err, $commit) = $this->execManualLocal('log --template {node} --rev %s', $name); } if (!$err) { $this->setBaseCommitExplanation(pht("it is specified by '%s' in your %s 'base' configuration.", $rule, $source)); return trim($commit); } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation(pht("you specified '%s' in your %s 'base' configuration.", $rule, $source)); return 'null'; case 'outgoing': list($err, $outgoing_base) = $this->execManualLocal('log --template={node} --rev %s', 'limit(reverse(ancestors(.) - outgoing()), 1)'); if (!$err) { $this->setBaseCommitExplanation(pht("it is the first ancestor of the working copy that is not " . "outgoing, and it matched the rule %s in your %s " . "'base' configuration.", $rule, $source)); return trim($outgoing_base); } case 'amended': $text = $this->getCommitMessage('.'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation(pht("'%s' has been amended with 'Differential Revision:', " . "as specified by '%s' in your %s 'base' configuration.", '.' . $rule, $source)); // NOTE: This should be safe because Mercurial doesn't support // amend until 2.2. return $this->getCanonicalRevisionName('.^'); } break; case 'bookmark': $revset = 'limit(' . ' sort(' . ' (ancestors(.) and bookmark() - .) or' . ' (ancestors(.) - outgoing()), ' . ' -rev),' . '1)'; list($err, $bookmark_base) = $this->execManualLocal('log --template={node} --rev %s', $revset); if (!$err) { $this->setBaseCommitExplanation(pht("it is the first ancestor of %s that either has a bookmark, " . "or is already in the remote and it matched the rule %s in " . "your %s 'base' configuration", '.', $rule, $source)); return trim($bookmark_base); } break; case 'this': $this->setBaseCommitExplanation(pht("you specified '%s' in your %s 'base' configuration.", $rule, $source)); return $this->getCanonicalRevisionName('.^'); default: if (preg_match('/^nodiff\\((.+)\\)$/', $name, $matches)) { list($results) = $this->execxLocal('log --template %s --rev %s', "{node}{desc}", sprintf('ancestor(.,%s)::.^', $matches[1])); $results = array_reverse(explode("", trim($results))); foreach ($results as $result) { if (empty($result)) { continue; } list($node, $desc) = explode("", $result, 2); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($desc); if ($message->getRevisionID()) { $this->setBaseCommitExplanation(pht("it is the first ancestor of %s that has a diff and is " . "the gca or a descendant of the gca with '%s', " . "specified by '%s' in your %s 'base' configuration.", '.', $matches[1], $rule, $source)); return $node; } } } break; } break; default: return null; } return null; }
public function resolveBaseCommitRule($rule, $source) { list($type, $name) = explode(':', $rule, 2); switch ($type) { case 'hg': $matches = null; if (preg_match('/^gca\\((.+)\\)$/', $name, $matches)) { list($err, $merge_base) = $this->execManualLocal('log --template={node} --rev %s', sprintf('ancestor(., %s)', $matches[1])); if (!$err) { $this->setBaseCommitExplanation("it is the greatest common ancestor of '{$matches[1]}' and ., as" . "specified by '{$rule}' in your {$source} 'base' " . "configuration."); return trim($merge_base); } } else { list($err) = $this->execManualLocal('id -r %s', $name); if (!$err) { $this->setBaseCommitExplanation("it is specified by '{$rule}' in your {$source} 'base' " . "configuration."); return $name; } } break; case 'arc': switch ($name) { case 'empty': $this->setBaseCommitExplanation("you specified '{$rule}' in your {$source} 'base' " . "configuration."); return 'null'; case 'outgoing': list($err, $outgoing_base) = $this->execManualLocal('log --template={node} --rev %s', 'limit(reverse(ancestors(.) - outgoing()), 1)'); if (!$err) { $this->setBaseCommitExplanation("it is the first ancestor of the working copy that is not " . "outgoing, and it matched the rule {$rule} in your {$source} " . "'base' configuration."); return trim($outgoing_base); } case 'amended': $text = $this->getCommitMessage('.'); $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); if ($message->getRevisionID()) { $this->setBaseCommitExplanation("'.' has been amended with 'Differential Revision:', " . "as specified by '{$rule}' in your {$source} 'base' " . "configuration."); // NOTE: This should be safe because Mercurial doesn't support // amend until 2.2. return '.^'; } break; case 'bookmark': $revset = 'limit(' . ' sort(' . ' (ancestors(.) and bookmark() - .) or' . ' (ancestors(.) - outgoing()), ' . ' -rev),' . '1)'; list($err, $bookmark_base) = $this->execManualLocal('log --template={node} --rev %s', $revset); if (!$err) { $this->setBaseCommitExplanation("it is the first ancestor of . that either has a bookmark, or " . "is already in the remote and it matched the rule {$rule} in " . "your {$source} 'base' configuration"); return trim($bookmark_base); } } break; default: return null; } return null; }
public function loadWorkingCopyDifferentialRevisions(ConduitClient $conduit, array $query) { $messages = $this->getGitCommitLog(); if (!strlen($messages)) { return array(); } $parser = new ArcanistDiffParser(); $messages = $parser->parseDiff($messages); // First, try to find revisions by explicit revision IDs in commit messages. $revision_ids = array(); foreach ($messages as $message) { $object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message->getMetadata('message')); if ($object->getRevisionID()) { $revision_ids[] = $object->getRevisionID(); } } if ($revision_ids) { $results = $conduit->callMethodSynchronous('differential.query', $query + array('ids' => $revision_ids)); return $results; } // If we didn't succeed, try to find revisions by hash. $hashes = array(); foreach ($this->getLocalCommitInformation() as $commit) { $hashes[] = array('gtcm', $commit['commit']); $hashes[] = array('gttr', $commit['tree']); } $results = $conduit->callMethodSynchronous('differential.query', $query + array('commitHashes' => $hashes)); return $results; }
private function loadCommitInfo(array $branches) { $repository_api = $this->getRepositoryAPI(); $branches = ipull($branches, null, 'name'); if ($repository_api instanceof ArcanistMercurialAPI) { $futures = array(); foreach ($branches as $branch) { $futures[$branch['name']] = $repository_api->execFutureLocal('log -l 1 --template %s -r %s', "{node}{date|hgdate}{p1node}{desc|firstline}{desc}", hgsprintf('%s', $branch['name'])); } $futures = id(new FutureIterator($futures))->limit(16); foreach ($futures as $name => $future) { list($info) = $future->resolvex(); $fields = explode("", trim($info), 5); list($hash, $epoch, $tree, $desc, $text) = $fields; $branches[$name] += array('hash' => $hash, 'desc' => $desc, 'tree' => $tree, 'epoch' => (int) $epoch, 'text' => $text); } } foreach ($branches as $name => $branch) { $text = $branch['text']; try { $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); $id = $message->getRevisionID(); $branch['revisionID'] = $id; } catch (ArcanistUsageException $ex) { // In case of invalid commit message which fails the parsing, // do nothing. $branch['revisionID'] = null; } $branches[$name] = $branch; } return $branches; }