protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $grep = $request->getValue('grep'); $repository = $drequest->getRepository(); $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); $results = array(); $future = $repository->getLocalCommandFuture('grep --rev %s --print0 --line-number %s %s', hgsprintf('ancestors(%s)', $drequest->getStableCommit()), $grep, $path); $lines = id(new LinesOfALargeExecFuture($future))->setDelimiter(""); $parts = array(); foreach ($lines as $line) { $parts[] = $line; if (count($parts) == 4) { list($path, $char_offset, $line, $string) = $parts; $results[] = array($path, $line, $string); if (count($results) >= $offset + $limit) { break; } $parts = array(); } } unset($lines); return $results; }
public function testhgsprintf() { $this->assertEqual("'version-1'", hgsprintf('%s', 'version-1')); $this->assertEqual("'single\\'quote'", hgsprintf('%s', "single'quote")); $this->assertEqual("'back\\\\slash'", hgsprintf('%s', 'back\\slash')); $this->assertEqual("'33%'", hgsprintf('%R', hgsprintf('%s', '33%'))); }
public function __construct(PhabricatorRepository $repository, $commit) { $this->repository = $repository; $future = $repository->getLocalCommandFuture('log --template %s --rev %s', '{rev}\\1{node}\\1{date}\\1{parents}\\2', hgsprintf('reverse(ancestors(%s))', $commit)); $this->iterator = new LinesOfALargeExecFuture($future); $this->iterator->setDelimiter(""); $this->iterator->rewind(); }
protected function executeQuery() { $repository = $this->getRepository(); $path = $this->path; $commit = $this->commit; $match_against = trim($path, '/'); $prefix = trim('./' . $match_against, '/'); list($entire_manifest) = $repository->execxLocalCommand('locate --print0 --rev %s -I %s', hgsprintf('%s', $commit), $prefix); return explode("", $entire_manifest); }
private function loadMercurialCommitRef() { $repository = $this->getRepository(); list($stdout) = $repository->execxLocalCommand('log --template %s --rev %s', '{author}\\n{desc}', hgsprintf('%s', $this->identifier)); list($author, $message) = explode("\n", $stdout, 2); $author = phutil_utf8ize($author); $message = phutil_utf8ize($message); list($author_name, $author_email) = $this->splitUserIdentifier($author); $hashes = array(id(new DiffusionCommitHash())->setHashType(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT)->setHashValue($this->identifier)); return id(new DiffusionCommitRef())->setAuthorName($author_name)->setAuthorEmail($author_email)->setMessage($message)->setHashes($hashes); }
protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $paths = $request->getValue('paths'); $results = $this->loadCommitsFromCache($paths); foreach ($paths as $path => $commit) { if (array_key_exists($path, $results)) { continue; } list($hash) = $repository->execxLocalCommand('log --template %s --limit 1 --removed --rev %s -- %s', '{node}', hgsprintf('reverse(ancestors(%s))', $commit), nonempty(ltrim($path, '/'), '.')); $results[$path] = trim($hash); } return $results; }
protected function executeQuery() { $repository = $this->getRepository(); $path = $this->path; $commit = $this->commit; $hg_paths_command = 'locate --print0 --rev %s -I %s'; $hg_version = PhabricatorRepositoryVersion::getMercurialVersion(); if (PhabricatorRepositoryVersion::isMercurialFilesCommandAvailable($hg_version)) { $hg_paths_command = 'files --print0 --rev %s -I %s'; } $match_against = trim($path, '/'); $prefix = trim('./' . $match_against, '/'); list($entire_manifest) = $repository->execxLocalCommand($hg_paths_command, hgsprintf('%s', $commit), $prefix); return explode("", $entire_manifest); }
protected function executeQuery() { $repository = $this->getRepository(); if ($this->contains !== null) { $spec = hgsprintf('(descendants(%s) and head())', $this->contains); } else { $spec = hgsprintf('head()'); } list($stdout) = $repository->execxLocalCommand('log --template %s --rev %s', '{node}\\1{branch}\\2', $spec); $branches = array(); $lines = explode("", $stdout); $lines = array_filter($lines); foreach ($lines as $line) { list($node, $branch) = explode("", $line); $branches[] = id(new DiffusionRepositoryRef())->setShortName($branch)->setCommitIdentifier($node); } return $branches; }
protected function executeQuery() { $repository = $this->getRepository(); $specs = array(); if ($this->contains !== null) { $specs['all'] = hgsprintf('(descendants(%s) and head())', $this->contains); $specs['open'] = hgsprintf('(descendants(%s) and head() and not closed())', $this->contains); } else { $specs['all'] = hgsprintf('head()'); $specs['open'] = hgsprintf('head() and not closed()'); } $futures = array(); foreach ($specs as $key => $spec) { $futures[$key] = $repository->getLocalCommandFuture('log --template %s --rev %s', '{node}\\1{branch}\\2', $spec); } $branches = array(); $open = array(); foreach (new FutureIterator($futures) as $key => $future) { list($stdout) = $future->resolvex(); $lines = explode("", $stdout); $lines = array_filter($lines); foreach ($lines as $line) { list($node, $branch) = explode("", $line); $id = $node . '/' . $branch; if (empty($branches[$id])) { $branches[$id] = id(new DiffusionRepositoryRef())->setShortName($branch)->setCommitIdentifier($node); } if ($key == 'open') { $open[$id] = true; } } } foreach ($branches as $id => $branch) { $branch->setRawFields(array('closed' => empty($open[$id]))); } return array_values($branches); }
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 cleanupBranch() { $repository_api = $this->getRepositoryAPI(); echo "Cleaning up feature {$this->branchType}...\n"; if ($this->isGit) { list($ref) = $repository_api->execxLocal('rev-parse --verify %s', $this->branch); $ref = trim($ref); $recovery_command = csprintf('git checkout -b %s %s', $this->branch, $ref); echo "(Use `{$recovery_command}` if you want it back.)\n"; $repository_api->execxLocal('branch -D %s', $this->branch); } else { if ($this->isHg) { $common_ancestor = $repository_api->getCanonicalRevisionName(hgsprintf("ancestor(%s,%s)", $this->onto, $this->branch)); $branch_root = $repository_api->getCanonicalRevisionName(hgsprintf("first((%s::%s)-%s)", $common_ancestor, $this->branch, $common_ancestor)); $repository_api->execxLocal('--config extensions.mq= strip -r %s', $branch_root); if ($repository_api->isBookmark($this->branch)) { $repository_api->execxLocal('bookmark -d %s', $this->branch); } } } if ($this->getArgument('delete-remote')) { if ($this->isGit) { list($err, $ref) = $repository_api->execManualLocal('rev-parse --verify %s/%s', $this->remote, $this->branch); if ($err) { echo "No remote feature {$this->branchType} to clean up.\n"; } else { // NOTE: In Git, you delete a remote branch by pushing it with a // colon in front of its name: // // git push <remote> :<branch> echo "Cleaning up remote feature branch...\n"; $repository_api->execxLocal('push %s :%s', $this->remote, $this->branch); } } else { if ($this->isHg) { // named branches were closed as part of the earlier commit // so only worry about bookmarks if ($repository_api->isBookmark($this->branch)) { $repository_api->execxLocal('push -B %s %s', $this->branch, $this->remote); } } } } }
/** * Find all ancestors of a new closing branch head which are not ancestors * of any old closing branch head. */ private function loadNewCommitIdentifiers($new_head, array $all_closing_heads) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($all_closing_heads) { $escheads = array(); foreach ($all_closing_heads as $head) { $escheads[] = hgsprintf('%s', $head); } $escheads = implode(' or ', $escheads); list($stdout) = $this->getRepository()->execxLocalCommand('log --template %s --rev %s', '{node}\\n', hgsprintf('%s', $new_head) . ' - (' . $escheads . ')'); } else { list($stdout) = $this->getRepository()->execxLocalCommand('log --template %s --rev %s', '{node}\\n', hgsprintf('%s', $new_head)); } $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } return phutil_split_lines($stdout, $retain_newlines = false); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($all_closing_heads) { list($stdout) = $this->getRepository()->execxLocalCommand('log --format=%s %s --not %Ls', '%H', $new_head, $all_closing_heads); } else { list($stdout) = $this->getRepository()->execxLocalCommand('log --format=%s %s', '%H', $new_head); } $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } return phutil_split_lines($stdout, $retain_newlines = false); default: throw new Exception(pht('Unsupported VCS "%s"!', $vcs)); } }
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; }
private function resolveMercurialRefs() { $repository = $this->getRepository(); $futures = array(); foreach ($this->refs as $ref) { $futures[$ref] = $repository->getLocalCommandFuture('log --template=%s --rev %s', '{node}', hgsprintf('%s', $ref)); } $results = array(); foreach (Futures($futures) as $ref => $future) { try { list($stdout) = $future->resolvex(); } catch (CommandException $ex) { if (preg_match('/ambiguous identifier/', $ex->getStdErr())) { // This indicates that the ref ambiguously matched several things. // Eventually, it would be nice to return all of them, but it is // unclear how to best do that. For now, treat it as a miss instead. continue; } throw $ex; } // It doesn't look like we can figure out the type (commit/branch/rev) // from this output very easily. For now, just call everything a commit. $type = 'commit'; $results[$ref][] = array('type' => $type, 'identifier' => trim($stdout)); } return $results; }
private function findMercurialPushKeyRefUpdates() { $key_namespace = getenv('HG_NAMESPACE'); if ($key_namespace === 'phases') { // Mercurial changes commit phases as part of normal push operations. We // just ignore these, as they don't seem to represent anything // interesting. return array(); } $key_name = getenv('HG_KEY'); $key_old = getenv('HG_OLD'); if (!strlen($key_old)) { $key_old = null; } $key_new = getenv('HG_NEW'); if (!strlen($key_new)) { $key_new = null; } if ($key_namespace !== 'bookmarks') { throw new Exception(pht("Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). " . "Rejecting push.", $key_namespace, $key_name, coalesce($key_old, pht('null')), coalesce($key_new, pht('null')))); } if ($key_old === $key_new) { // We get a callback when the bookmark doesn't change. Just ignore this, // as it's a no-op. return array(); } $ref_flags = 0; $merge_base = null; if ($key_old === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else { if ($key_new === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else { list($merge_base_raw) = $this->getRepository()->execxLocalCommand('log --template %s --rev %s', '{node}', hgsprintf('ancestor(%s, %s)', $key_old, $key_new)); if (strlen(trim($merge_base_raw))) { $merge_base = trim($merge_base_raw); } if ($merge_base && $merge_base === $key_old) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; } } } $ref_update = $this->newPushLog()->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK)->setRefName($key_name)->setRefOld(coalesce($key_old, self::EMPTY_HASH))->setRefNew(coalesce($key_new, self::EMPTY_HASH))->setChangeFlags($ref_flags); return array($ref_update); }
public function run() { $console = PhutilConsole::getConsole(); if (!$this->getArgument('show-base')) { $this->printRepositorySection(); $console->writeOut("\n"); } $repository_api = $this->getRepositoryAPI(); $arg_commit = $this->getArgument('commit'); if (count($arg_commit)) { $this->parseBaseCommitArgument($arg_commit); } $arg = $arg_commit ? ' ' . head($arg_commit) : ''; $repository_api->setBaseCommitArgumentRules($this->getArgument('base', '')); $supports_ranges = $repository_api->supportsCommitRanges(); $head_commit = $this->getArgument('head'); if ($head_commit !== null) { $arg .= csprintf(' --head %R', $head_commit); $repository_api->setHeadCommit($head_commit); } if ($supports_ranges) { $relative = $repository_api->getBaseCommit(); if ($this->getArgument('show-base')) { echo $relative . "\n"; return 0; } $info = $repository_api->getLocalCommitInformation(); if ($info) { $commits = array(); foreach ($info as $commit) { $hash = substr($commit['commit'], 0, 16); $summary = $commit['summary']; $commits[] = " {$hash} {$summary}"; } $commits = implode("\n", $commits); } else { $commits = ' ' . pht('(No commits.)'); } $explanation = $repository_api->getBaseCommitExplanation(); $relative_summary = $repository_api->getCommitSummary($relative); $relative = substr($relative, 0, 16); if ($repository_api instanceof ArcanistGitAPI) { $head = $this->getArgument('head', 'HEAD'); $command = csprintf('git diff %R', "{$relative}..{$head}"); } else { if ($repository_api instanceof ArcanistMercurialAPI) { $command = csprintf('hg diff --rev %R', hgsprintf('%s', $relative)); } else { throw new Exception(pht('Unknown VCS!')); } } echo phutil_console_wrap(phutil_console_format("**%s**\n%s\n\n %s %s\n\n", pht('COMMIT RANGE'), pht("If you run '%s', changes between the commit:", "arc diff{$arg}"), $relative, $relative_summary)); if ($head_commit === null) { $will_be_sent = pht('...and the current working copy state will be sent to ' . 'Differential, because %s', $explanation); } else { $will_be_sent = pht('...and "%s" will be sent to Differential, because %s', $head_commit, $explanation); } echo phutil_console_wrap(phutil_console_format("%s\n\n%s\n\n \$ %s\n\n%s\n\n", $will_be_sent, pht('You can see the exact changes that will be sent by running ' . 'this command:'), $command, pht('These commits will be included in the diff:'))); echo $commits . "\n\n\n"; } $any_status = $this->getArgument('any-status'); $query = array('status' => $any_status ? 'status-any' : 'status-open'); $revisions = $repository_api->loadWorkingCopyDifferentialRevisions($this->getConduit(), $query); echo phutil_console_wrap(phutil_console_format("**%s**\n%s\n\n", pht('MATCHING REVISIONS'), pht('These Differential revisions match the changes in this working ' . 'copy:'))); if (empty($revisions)) { echo " " . pht('(No revisions match.)') . "\n"; echo "\n"; echo phutil_console_wrap(phutil_console_format(pht("Since there are no revisions in Differential which match this " . "working copy, a new revision will be **created** if you run " . "'%s'.\n\n", "arc diff{$arg}"))); } else { $other_author_phids = array(); foreach ($revisions as $revision) { if ($revision['authorPHID'] != $this->getUserPHID()) { $other_author_phids[] = $revision['authorPHID']; } } $other_authors = array(); if ($other_author_phids) { $other_authors = $this->getConduit()->callMethodSynchronous('user.query', array('phids' => $other_author_phids)); $other_authors = ipull($other_authors, 'userName', 'phid'); } foreach ($revisions as $revision) { $title = $revision['title']; $monogram = 'D' . $revision['id']; if ($revision['authorPHID'] != $this->getUserPHID()) { $author = $other_authors[$revision['authorPHID']]; echo pht(" %s (%s) %s\n", $monogram, $author, $title); } else { echo pht(" %s %s\n", $monogram, $title); } echo ' ' . pht('Reason') . ': ' . $revision['why'] . "\n"; echo "\n"; } if (count($revisions) == 1) { echo phutil_console_wrap(phutil_console_format(pht("Since exactly one revision in Differential matches this " . "working copy, it will be **updated** if you run '%s'.", "arc diff{$arg}"))); } else { echo phutil_console_wrap(pht("Since more than one revision in Differential matches this " . "working copy, you will be asked which revision you want to " . "update if you run '%s'.", "arc diff {$arg}")); } echo "\n\n"; } return 0; }
private function resolveMercurialRefs() { $repository = $this->getRepository(); // First, pull all of the branch heads in the repository. Doing this in // bulk is much faster than querying each individual head if we're // checking even a small number of refs. $branches = id(new DiffusionLowLevelMercurialBranchesQuery())->setRepository($repository)->executeQuery(); $branches = mgroup($branches, 'getShortName'); $results = array(); $unresolved = $this->refs; foreach ($unresolved as $key => $ref) { if (empty($branches[$ref])) { continue; } foreach ($branches[$ref] as $branch) { $fields = $branch->getRawFields(); $results[$ref][] = array('type' => 'branch', 'identifier' => $branch->getCommitIdentifier(), 'closed' => idx($fields, 'closed', false)); } unset($unresolved[$key]); } if (!$unresolved) { return $results; } // If we still have unresolved refs (which might be things like "tip"), // try to resolve them individually. $futures = array(); foreach ($unresolved as $ref) { $futures[$ref] = $repository->getLocalCommandFuture('log --template=%s --rev %s', '{node}', hgsprintf('%s', $ref)); } foreach (new FutureIterator($futures) as $ref => $future) { try { list($stdout) = $future->resolvex(); } catch (CommandException $ex) { if (preg_match('/ambiguous identifier/', $ex->getStdErr())) { // This indicates that the ref ambiguously matched several things. // Eventually, it would be nice to return all of them, but it is // unclear how to best do that. For now, treat it as a miss instead. continue; } if (preg_match('/unknown revision/', $ex->getStdErr())) { // No matches for this ref. continue; } throw $ex; } // It doesn't look like we can figure out the type (commit/branch/rev) // from this output very easily. For now, just call everything a commit. $type = 'commit'; $results[$ref][] = array('type' => $type, 'identifier' => trim($stdout)); } return $results; }
protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commit_hash = $request->getValue('commit'); $path = $request->getValue('path'); $offset = $request->getValue('offset'); $limit = $request->getValue('limit'); $path = DiffusionPathIDQuery::normalizePath($path); $path = ltrim($path, '/'); // NOTE: Older versions of Mercurial give different results for these // commands (see T1268): // // $ hg log -- '' // $ hg log // // All versions of Mercurial give different results for these commands // (merge commits are excluded with the "." version): // // $ hg log -- . // $ hg log // // If we don't have a path component in the query, omit it from the command // entirely to avoid these inconsistencies. // NOTE: When viewing the history of a file, we don't use "-b", because // Mercurial stops history at the branchpoint but we're interested in all // ancestors. When viewing history of a branch, we do use "-b", and thus // stop history (this is more consistent with the Mercurial worldview of // branches). if (strlen($path)) { $path_arg = csprintf('-- %s', $path); $branch_arg = ''; } else { $path_arg = ''; // NOTE: --branch used to be called --only-branch; use -b for // compatibility. $branch_arg = csprintf('-b %s', $drequest->getBranch()); } list($stdout) = $repository->execxLocalCommand('log --debug --template %s --limit %d %C --rev %s %C', '{node};{parents}\\n', $offset + $limit, $branch_arg, hgsprintf('reverse(ancestors(%s))', $commit_hash), $path_arg); $stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout); $lines = explode("\n", trim($stdout)); $lines = array_slice($lines, $offset); $hash_list = array(); $parent_map = array(); $last = null; foreach (array_reverse($lines) as $line) { list($hash, $parents) = explode(';', $line); $parents = trim($parents); if (!$parents) { if ($last === null) { $parent_map[$hash] = array('...'); } else { $parent_map[$hash] = array($last); } } else { $parents = preg_split('/\\s+/', $parents); foreach ($parents as $parent) { list($plocal, $phash) = explode(':', $parent); if (!preg_match('/^0+$/', $phash)) { $parent_map[$hash][] = $phash; } } // This may happen for the zeroth commit in repository, both hashes // are "000000000...". if (empty($parent_map[$hash])) { $parent_map[$hash] = array('...'); } } // The rendering code expects the first commit to be "mainline", like // Git. Flip the order so it does the right thing. $parent_map[$hash] = array_reverse($parent_map[$hash]); $hash_list[] = $hash; $last = $hash; } $hash_list = array_reverse($hash_list); $this->parents = $parent_map; return DiffusionQuery::loadHistoryForCommitIdentifiers($hash_list, $drequest); }
/** * Find all ancestors of a new closing branch head which are not ancestors * of any old closing branch head. */ private function loadNewCommitIdentifiers($new_head, array $all_closing_heads) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: if ($all_closing_heads) { $parts = array(); foreach ($all_closing_heads as $head) { $parts[] = hgsprintf('%s', $head); } // See T5896. Mercurial can not parse an "X or Y or ..." rev list // with more than about 300 items, because it exceeds the maximum // allowed recursion depth. Split all the heads into chunks of // 256, and build a query like this: // // ((1 or 2 or ... or 255) or (256 or 257 or ... 511)) // // If we have more than 65535 heads, we'll do that again: // // (((1 or ...) or ...) or ((65536 or ...) or ...)) $chunk_size = 256; while (count($parts) > $chunk_size) { $chunks = array_chunk($parts, $chunk_size); foreach ($chunks as $key => $chunk) { $chunks[$key] = '(' . implode(' or ', $chunk) . ')'; } $parts = array_values($chunks); } $parts = '(' . implode(' or ', $parts) . ')'; list($stdout) = $this->getRepository()->execxLocalCommand('log --template %s --rev %s', '{node}\\n', hgsprintf('%s', $new_head) . ' - ' . $parts); } else { list($stdout) = $this->getRepository()->execxLocalCommand('log --template %s --rev %s', '{node}\\n', hgsprintf('%s', $new_head)); } $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } return phutil_split_lines($stdout, $retain_newlines = false); case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: if ($all_closing_heads) { list($stdout) = $this->getRepository()->execxLocalCommand('log --format=%s %s --not %Ls', '%H', $new_head, $all_closing_heads); } else { list($stdout) = $this->getRepository()->execxLocalCommand('log --format=%s %s', '%H', $new_head); } $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } return phutil_split_lines($stdout, $retain_newlines = false); default: throw new Exception(pht('Unsupported VCS "%s"!', $vcs)); } }
public function run() { $source = $this->getSource(); $param = $this->getSourceParam(); try { switch ($source) { case self::SOURCE_PATCH: if ($param == '-') { $patch = @file_get_contents('php://stdin'); if (!strlen($patch)) { throw new ArcanistUsageException(pht('Failed to read patch from stdin!')); } } else { $patch = Filesystem::readFile($param); } $bundle = ArcanistBundle::newFromDiff($patch); break; case self::SOURCE_BUNDLE: $path = $this->getArgument('arcbundle'); $bundle = ArcanistBundle::newFromArcBundle($path); break; case self::SOURCE_REVISION: $bundle = $this->loadRevisionBundleFromConduit($this->getConduit(), $param); break; case self::SOURCE_DIFF: $bundle = $this->loadDiffBundleFromConduit($this->getConduit(), $param); break; } } catch (ConduitClientException $ex) { if ($ex->getErrorCode() == 'ERR-INVALID-SESSION') { // Phabricator is not configured to allow anonymous access to // Differential. $this->authenticateConduit(); return $this->run(); } else { throw $ex; } } $try_encoding = nonempty($this->getArgument('encoding'), null); if (!$try_encoding) { if ($this->requiresConduit()) { try { $try_encoding = $this->getRepositoryEncoding(); } catch (ConduitClientException $e) { $try_encoding = null; } } } if ($try_encoding) { $bundle->setEncoding($try_encoding); } $sanity_check = !$this->getArgument('force', false); // we should update the working copy before we do ANYTHING else to // the working copy if ($this->shouldUpdateWorkingCopy()) { $this->updateWorkingCopy(); } if ($sanity_check) { $this->requireCleanWorkingCopy(); } $repository_api = $this->getRepositoryAPI(); $has_base_revision = $repository_api->hasLocalCommit($bundle->getBaseRevision()); if ($this->canBranch() && ($this->shouldBranch() || $this->shouldCommit() && $has_base_revision)) { if ($repository_api instanceof ArcanistGitAPI) { $original_branch = $repository_api->getBranchName(); } else { if ($repository_api instanceof ArcanistMercurialAPI) { $original_branch = $repository_api->getActiveBookmark(); } } // If we weren't on a branch, then record the ref we'll return to // instead. if ($original_branch === null) { if ($repository_api instanceof ArcanistGitAPI) { $original_branch = $repository_api->getCanonicalRevisionName('HEAD'); } else { if ($repository_api instanceof ArcanistMercurialAPI) { $original_branch = $repository_api->getCanonicalRevisionName('.'); } } } $new_branch = $this->createBranch($bundle, $has_base_revision); } if (!$has_base_revision && $this->shouldApplyDependencies()) { $this->applyDependencies($bundle); } if ($sanity_check) { $this->sanityCheck($bundle); } if ($repository_api instanceof ArcanistSubversionAPI) { $patch_err = 0; $copies = array(); $deletes = array(); $patches = array(); $propset = array(); $adds = array(); $symlinks = array(); $changes = $bundle->getChanges(); foreach ($changes as $change) { $type = $change->getType(); $should_patch = true; $filetype = $change->getFileType(); switch ($filetype) { case ArcanistDiffChangeType::FILE_SYMLINK: $should_patch = false; $symlinks[] = $change; break; } switch ($type) { case ArcanistDiffChangeType::TYPE_MOVE_AWAY: case ArcanistDiffChangeType::TYPE_MULTICOPY: case ArcanistDiffChangeType::TYPE_DELETE: $path = $change->getCurrentPath(); $fpath = $repository_api->getPath($path); if (!@file_exists($fpath)) { $ok = phutil_console_confirm(pht("Patch deletes file '%s', but the file does not exist in " . "the working copy. Continue anyway?", $path)); if (!$ok) { throw new ArcanistUserAbortException(); } } else { $deletes[] = $change->getCurrentPath(); } $should_patch = false; break; case ArcanistDiffChangeType::TYPE_COPY_HERE: case ArcanistDiffChangeType::TYPE_MOVE_HERE: $path = $change->getOldPath(); $fpath = $repository_api->getPath($path); if (!@file_exists($fpath)) { $cpath = $change->getCurrentPath(); if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) { $verbs = pht('copies'); } else { $verbs = pht('moves'); } $ok = phutil_console_confirm(pht("Patch %s '%s' to '%s', but source path does not exist " . "in the working copy. Continue anyway?", $verbs, $path, $cpath)); if (!$ok) { throw new ArcanistUserAbortException(); } } else { $copies[] = array($change->getOldPath(), $change->getCurrentPath()); } break; case ArcanistDiffChangeType::TYPE_ADD: $adds[] = $change->getCurrentPath(); break; } if ($should_patch) { $cbundle = ArcanistBundle::newFromChanges(array($change)); $patches[$change->getCurrentPath()] = $cbundle->toUnifiedDiff(); $prop_old = $change->getOldProperties(); $prop_new = $change->getNewProperties(); $props = $prop_old + $prop_new; foreach ($props as $key => $ignored) { if (idx($prop_old, $key) !== idx($prop_new, $key)) { $propset[$change->getCurrentPath()][$key] = idx($prop_new, $key); } } } } // Before we start doing anything, create all the directories we're going // to add files to if they don't already exist. foreach ($copies as $copy) { list($src, $dst) = $copy; $this->createParentDirectoryOf($dst); } foreach ($patches as $path => $patch) { $this->createParentDirectoryOf($path); } foreach ($adds as $add) { $this->createParentDirectoryOf($add); } // TODO: The SVN patch workflow likely does not work on windows because // of the (cd ...) stuff. foreach ($copies as $copy) { list($src, $dst) = $copy; passthru(csprintf('(cd %s; svn cp %s %s)', $repository_api->getPath(), ArcanistSubversionAPI::escapeFileNameForSVN($src), ArcanistSubversionAPI::escapeFileNameForSVN($dst))); } foreach ($deletes as $delete) { passthru(csprintf('(cd %s; svn rm %s)', $repository_api->getPath(), ArcanistSubversionAPI::escapeFileNameForSVN($delete))); } foreach ($symlinks as $symlink) { $link_target = $symlink->getSymlinkTarget(); $link_path = $symlink->getCurrentPath(); switch ($symlink->getType()) { case ArcanistDiffChangeType::TYPE_ADD: case ArcanistDiffChangeType::TYPE_CHANGE: case ArcanistDiffChangeType::TYPE_MOVE_HERE: case ArcanistDiffChangeType::TYPE_COPY_HERE: execx('(cd %s && ln -sf %s %s)', $repository_api->getPath(), $link_target, $link_path); break; } } foreach ($patches as $path => $patch) { $err = null; if ($patch) { $tmp = new TempFile(); Filesystem::writeFile($tmp, $patch); passthru(csprintf('(cd %s; patch -p0 < %s)', $repository_api->getPath(), $tmp), $err); } else { passthru(csprintf('(cd %s; touch %s)', $repository_api->getPath(), $path), $err); } if ($err) { $patch_err = max($patch_err, $err); } } foreach ($adds as $add) { passthru(csprintf('(cd %s; svn add %s)', $repository_api->getPath(), ArcanistSubversionAPI::escapeFileNameForSVN($add))); } foreach ($propset as $path => $changes) { foreach ($changes as $prop => $value) { if ($prop == 'unix:filemode') { // Setting this property also changes the file mode. $prop = 'svn:executable'; $value = octdec($value) & 0111 ? 'on' : null; } if ($value === null) { passthru(csprintf('(cd %s; svn propdel %s %s)', $repository_api->getPath(), $prop, ArcanistSubversionAPI::escapeFileNameForSVN($path))); } else { passthru(csprintf('(cd %s; svn propset %s %s %s)', $repository_api->getPath(), $prop, $value, ArcanistSubversionAPI::escapeFileNameForSVN($path))); } } } if ($patch_err == 0) { echo phutil_console_format("<bg:green>** %s **</bg> %s\n", pht('OKAY'), pht('Successfully applied patch to the working copy.')); } else { echo phutil_console_format("\n\n<bg:yellow>** %s **</bg> %s\n", pht('WARNING'), pht("Some hunks could not be applied cleanly by the unix '%s' " . "utility. Your working copy may be different from the revision's " . "base, or you may be in the wrong subdirectory. You can export " . "the raw patch file using '%s', and then try to apply it by " . "fiddling with options to '%s' (particularly, %s), or manually. " . "The output above, from '%s', may be helpful in " . "figuring out what went wrong.", 'patch', 'arc export --unified', 'patch', '-p', 'patch')); } return $patch_err; } else { if ($repository_api instanceof ArcanistGitAPI) { $patchfile = new TempFile(); Filesystem::writeFile($patchfile, $bundle->toGitPatch()); $passthru = new PhutilExecPassthru('git apply --index --reject -- %s', $patchfile); $passthru->setCWD($repository_api->getPath()); $err = $passthru->execute(); if ($err) { echo phutil_console_format("\n<bg:red>** %s **</bg>\n", pht('Patch Failed!')); // NOTE: Git patches may fail if they change the case of a filename // (for instance, from 'example.c' to 'Example.c'). As of now, Git // can not apply these patches on case-insensitive filesystems and // there is no way to build a patch which works. throw new ArcanistUsageException(pht('Unable to apply patch!')); } // in case there were any submodule changes involved $repository_api->execpassthru('submodule update --init --recursive'); if ($this->shouldCommit()) { if ($bundle->getFullAuthor()) { $author_cmd = csprintf('--author=%s', $bundle->getFullAuthor()); } else { $author_cmd = ''; } $commit_message = $this->getCommitMessage($bundle); $future = $repository_api->execFutureLocal('commit -a %C -F - --no-verify', $author_cmd); $future->write($commit_message); $future->resolvex(); $verb = pht('committed'); } else { $verb = pht('applied'); } if ($this->canBranch() && !$this->shouldBranch() && $this->shouldCommit() && $has_base_revision) { $repository_api->execxLocal('checkout %s', $original_branch); $ex = null; try { $repository_api->execxLocal('cherry-pick %s', $new_branch); } catch (Exception $ex) { // do nothing } $repository_api->execxLocal('branch -D %s', $new_branch); if ($ex) { echo phutil_console_format("\n<bg:red>** %s**</bg>\n", pht('Cherry Pick Failed!')); throw $ex; } } echo phutil_console_format("<bg:green>** %s **</bg> %s\n", pht('OKAY'), pht('Successfully %s patch.', $verb)); } else { if ($repository_api instanceof ArcanistMercurialAPI) { $future = $repository_api->execFutureLocal('import --no-commit -'); $future->write($bundle->toGitPatch()); try { $future->resolvex(); } catch (CommandException $ex) { echo phutil_console_format("\n<bg:red>** %s **</bg>\n", pht('Patch Failed!')); $stderr = $ex->getStdErr(); if (preg_match('/case-folding collision/', $stderr)) { echo phutil_console_wrap(phutil_console_format("\n<bg:yellow>** %s **</bg> %s\n", pht('WARNING'), pht("This patch may have failed because it attempts to change " . "the case of a filename (for instance, from '%s' to '%s'). " . "Mercurial cannot apply patches like this on case-insensitive " . "filesystems. You must apply this patch manually.", 'example.c', 'Example.c'))); } throw $ex; } if ($this->shouldCommit()) { $author = coalesce($bundle->getFullAuthor(), $bundle->getAuthorName()); if ($author !== null) { $author_cmd = csprintf('-u %s', $author); } else { $author_cmd = ''; } $commit_message = $this->getCommitMessage($bundle); $future = $repository_api->execFutureLocal('commit %C -l -', $author_cmd); $future->write($commit_message); $future->resolvex(); if (!$this->shouldBranch() && $has_base_revision) { $original_rev = $repository_api->getCanonicalRevisionName($original_branch); $current_parent = $repository_api->getCanonicalRevisionName(hgsprintf('%s^', $new_branch)); $err = 0; if ($original_rev != $current_parent) { list($err) = $repository_api->execManualLocal('rebase --dest %s --rev %s', hgsprintf('%s', $original_branch), hgsprintf('%s', $new_branch)); } $repository_api->execxLocal('bookmark --delete %s', $new_branch); if ($err) { $repository_api->execManualLocal('rebase --abort'); throw new ArcanistUsageException(phutil_console_format("\n<bg:red>** %s**</bg>\n", pht('Rebase onto %s failed!', $original_branch))); } } $verb = pht('committed'); } else { $verb = pht('applied'); } echo phutil_console_format("<bg:green>** %s **</bg> %s\n", pht('OKAY'), pht('Successfully %s patch.', $verb)); } else { throw new Exception(pht('Unknown version control system.')); } } } return 0; }
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; }