diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php index c580ab56..bc196c59 100644 --- a/src/repository/marker/ArcanistMarkerRef.php +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -1,186 +1,196 @@ getMarkerType()) { case self::TYPE_BRANCH: return pht('Branch "%s"', $this->getName()); case self::TYPE_BOOKMARK: return pht('Bookmark "%s"', $this->getName()); default: return pht('Marker "%s"', $this->getName()); } } protected function newHardpoints() { return array( $this->newHardpoint(self::HARDPOINT_COMMITREF), $this->newHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF), $this->newHardpoint(self::HARDPOINT_REMOTEREF), ); } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setMarkerType($marker_type) { $this->markerType = $marker_type; return $this; } public function getMarkerType() { return $this->markerType; } public function setEpoch($epoch) { $this->epoch = $epoch; return $this; } public function getEpoch() { return $this->epoch; } public function setMarkerHash($marker_hash) { $this->markerHash = $marker_hash; return $this; } public function getMarkerHash() { return $this->markerHash; } public function setDisplayHash($display_hash) { $this->displayHash = $display_hash; return $this; } public function getDisplayHash() { return $this->displayHash; } public function setCommitHash($commit_hash) { $this->commitHash = $commit_hash; return $this; } public function getCommitHash() { return $this->commitHash; } public function setTreeHash($tree_hash) { $this->treeHash = $tree_hash; return $this; } public function getTreeHash() { return $this->treeHash; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { return $this->summary; } public function setMessage($message) { $this->message = $message; return $this; } public function getMessage() { return $this->message; } public function setIsActive($is_active) { $this->isActive = $is_active; return $this; } public function getIsActive() { return $this->isActive; } public function setRemoteName($remote_name) { $this->remoteName = $remote_name; return $this; } public function getRemoteName() { return $this->remoteName; } public function isBookmark() { return ($this->getMarkerType() === self::TYPE_BOOKMARK); } public function isBranch() { return ($this->getMarkerType() === self::TYPE_BRANCH); } + public function isCommitState() { + return ($this->getMarkerType() === self::TYPE_COMMIT_STATE); + } + + public function isBranchState() { + return ($this->getMarkerType() === self::TYPE_BRANCH_STATE); + } + public function attachCommitRef(ArcanistCommitRef $ref) { return $this->attachHardpoint(self::HARDPOINT_COMMITREF, $ref); } public function getCommitRef() { return $this->getHardpoint(self::HARDPOINT_COMMITREF); } public function attachWorkingCopyStateRef(ArcanistWorkingCopyStateRef $ref) { return $this->attachHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF, $ref); } public function getWorkingCopyStateRef() { return $this->getHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF); } public function attachRemoteRef(ArcanistRemoteRef $ref = null) { return $this->attachHardpoint(self::HARDPOINT_REMOTEREF, $ref); } public function getRemoteRef() { return $this->getHardpoint(self::HARDPOINT_REMOTEREF); } protected function buildRefView(ArcanistRefView $view) { $title = pht( '%s %s', $this->getDisplayHash(), $this->getSummary()); $view ->setObjectName($this->getRefDisplayName()) ->setTitle($title); } } diff --git a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php index 0cca7d76..fd12e9aa 100644 --- a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php @@ -1,128 +1,122 @@ newMarkers(); } protected function newRemoteRefMarkers(ArcanistRemoteRef $remote = null) { return $this->newMarkers($remote); } private function newMarkers(ArcanistRemoteRef $remote = null) { $api = $this->getRepositoryAPI(); // In native Mercurial it is difficult to identify remote markers, and // complicated to identify local markers efficiently. We use an extension // to provide a command which works like "git for-each-ref" locally and // "git ls-remote" when given a remote. $argv = array(); foreach ($api->getMercurialExtensionArguments() as $arg) { $argv[] = $arg; } $argv[] = 'arc-ls-markers'; // NOTE: In remote mode, we're using passthru and a tempfile on this // because it's a remote command and may prompt the user to provide // credentials interactively. In local mode, we can just read stdout. if ($remote !== null) { $tmpfile = new TempFile(); Filesystem::remove($tmpfile); $argv[] = '--output'; $argv[] = phutil_string_cast($tmpfile); } $argv[] = '--'; if ($remote !== null) { $argv[] = $remote->getRemoteName(); } if ($remote !== null) { $passthru = $api->newPassthru('%Ls', $argv); $err = $passthru->execute(); if ($err) { throw new Exception( pht( 'Call to "hg arc-ls-markers" failed with error "%s".', $err)); } $raw_data = Filesystem::readFile($tmpfile); unset($tmpfile); } else { $future = $api->newFuture('%Ls', $argv); list($raw_data) = $future->resolve(); } $items = phutil_json_decode($raw_data); $markers = array(); foreach ($items as $item) { if (!empty($item['isClosed'])) { // NOTE: For now, we ignore closed branch heads. continue; } $node = $item['node']; - if (!$node) { - // NOTE: For now, we ignore the virtual "current branch" marker. - continue; - } switch ($item['type']) { case 'branch': $marker_type = ArcanistMarkerRef::TYPE_BRANCH; break; case 'bookmark': $marker_type = ArcanistMarkerRef::TYPE_BOOKMARK; break; - case 'commit': - $marker_type = null; + case 'commit-state': + $marker_type = ArcanistMarkerRef::TYPE_COMMIT_STATE; + break; + case 'branch-state': + $marker_type = ArcanistMarkerRef::TYPE_BRANCH_STATE; break; default: throw new Exception( pht( 'Call to "hg arc-ls-markers" returned marker of unknown '. 'type "%s".', $item['type'])); } - if ($marker_type === null) { - // NOTE: For now, we ignore the virtual "head" marker. - continue; - } - $commit_ref = $api->newCommitRef() ->setCommitHash($node); $marker_ref = id(new ArcanistMarkerRef()) ->setName($item['name']) ->setCommitHash($node) ->attachCommitRef($commit_ref); if (isset($item['description'])) { $description = $item['description']; $commit_ref->attachMessage($description); $description_lines = phutil_split_lines($description, false); $marker_ref->setSummary(head($description_lines)); } $marker_ref ->setMarkerType($marker_type) ->setIsActive(!empty($item['isActive'])); $markers[] = $marker_ref; } return $markers; } } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php index a9ad0a31..fd551547 100644 --- a/src/repository/state/ArcanistMercurialLocalState.php +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -1,116 +1,187 @@ getRepositoryAPI(); $log = $this->getWorkflow()->getLogEngine(); - // TODO: Both of these can be pulled from "hg arc-ls-markers" more - // efficiently. - - $this->localCommit = $api->getCanonicalRevisionName('.'); - - list($branch) = $api->execxLocal('branch'); - $this->localBranch = trim($branch); - - $log->writeTrace( - pht('SAVE STATE'), - pht( + $markers = $api->newMarkerRefQuery() + ->execute(); + + $local_commit = null; + foreach ($markers as $marker) { + if ($marker->isCommitState()) { + $local_commit = $marker->getCommitHash(); + } + } + + if ($local_commit === null) { + throw new Exception( + pht( + 'Unable to identify the current commit in the working copy.')); + } + + $this->localCommit = $local_commit; + + $local_branch = null; + foreach ($markers as $marker) { + if ($marker->isBranchState()) { + $local_branch = $marker->getName(); + break; + } + } + + if ($local_branch === null) { + throw new Exception( + pht( + 'Unable to identify the current branch in the working copy.')); + } + + if ($local_branch !== null) { + $this->localBranch = $local_branch; + } + + $local_bookmark = null; + foreach ($markers as $marker) { + if ($marker->isBookmark()) { + if ($marker->getIsActive()) { + $local_bookmark = $marker->getName(); + break; + } + } + } + + if ($local_bookmark !== null) { + $this->localBookmark = $local_bookmark; + } + + $has_bookmark = ($this->localBookmark !== null); + + if ($has_bookmark) { + $location = pht( + 'Saving local state (at "%s" on branch "%s", bookmarked as "%s").', + $api->getDisplayHash($this->localCommit), + $this->localBranch, + $this->localBookmark); + } else { + $location = pht( 'Saving local state (at "%s" on branch "%s").', $api->getDisplayHash($this->localCommit), - $this->localBranch)); + $this->localBranch); + } + + $log->writeTrace(pht('SAVE STATE'), $location); } protected function executeRestoreLocalState() { $api = $this->getRepositoryAPI(); $log = $this->getWorkflow()->getLogEngine(); - $log->writeStatus( - pht('LOAD STATE'), - pht( + if ($this->localBookmark !== null) { + $location = pht( + 'Restoring local state (at "%s" on branch "%s", bookmarked as "%s").', + $api->getDisplayHash($this->localCommit), + $this->localBranch, + $this->localBookmark); + } else { + $location = pht( 'Restoring local state (at "%s" on branch "%s").', $api->getDisplayHash($this->localCommit), - $this->localBranch)); + $this->localBranch); + } + + $log->writeStatus(pht('LOAD STATE'), $location); $api->execxLocal('update -- %s', $this->localCommit); $api->execxLocal('branch --force -- %s', $this->localBranch); + + if ($this->localBookmark !== null) { + $api->execxLocal('bookmark --force -- %s', $this->localBookmark); + } } protected function executeDiscardLocalState() { return; } protected function canStashChanges() { $api = $this->getRepositoryAPI(); return $api->getMercurialFeature('shelve'); } protected function getIgnoreHints() { return array( pht( 'To configure Mercurial to ignore certain files in the working '. 'copy, add them to ".hgignore".'), ); } protected function newRestoreCommandsForDisplay() { $api = $this->getRepositoryAPI(); $commands = array(); $commands[] = csprintf( 'hg update -- %s', $api->getDisplayHash($this->localCommit)); $commands[] = csprintf( 'hg branch --force -- %s', $this->localBranch); + if ($this->localBookmark !== null) { + $commands[] = csprintf( + 'hg bookmark --force -- %s', + $this->localBookmark); + } + return $commands; } protected function saveStash() { $api = $this->getRepositoryAPI(); $log = $this->getWorkflow()->getLogEngine(); $stash_ref = sprintf( 'arc-%s', Filesystem::readRandomCharacters(12)); $api->execxLocal( '--config extensions.shelve= shelve --unknown --name %s --', $stash_ref); $log->writeStatus( pht('SHELVE'), pht('Shelving uncommitted changes from working copy.')); return $stash_ref; } protected function restoreStash($stash_ref) { $api = $this->getRepositoryAPI(); $log = $this->getWorkflow()->getLogEngine(); $log->writeStatus( pht('UNSHELVE'), pht('Restoring uncommitted changes to working copy.')); $api->execxLocal( '--config extensions.shelve= unshelve --keep --name %s --', $stash_ref); } protected function discardStash($stash_ref) { $api = $this->getRepositoryAPI(); $api->execxLocal( '--config extensions.shelve= shelve --delete %s --', $stash_ref); } } diff --git a/support/hg/arc-hg.py b/support/hg/arc-hg.py index 9bf18aa0..01ac3907 100644 --- a/support/hg/arc-hg.py +++ b/support/hg/arc-hg.py @@ -1,208 +1,199 @@ from __future__ import absolute_import import sys is_python_3 = sys.version_info[0] >= 3 if is_python_3: def arc_items(dict): return dict.items() else: def arc_items(dict): return dict.iteritems() import os import json from mercurial import ( cmdutil, bookmarks, bundlerepo, error, hg, i18n, node, registrar, ) _ = i18n._ cmdtable = {} command = registrar.command(cmdtable) @command( b'arc-ls-markers', [(b'', b'output', b'', _(b'file to output refs to'), _(b'FILE')), ] + cmdutil.remoteopts, _(b'[--output FILENAME] [SOURCE]')) def lsmarkers(ui, repo, source=None, **opts): """list markers Show the current branch heads and bookmarks in the local working copy, or a specified path/URL. Markers are printed to stdout in JSON. (This is an Arcanist extension to Mercurial.) Returns 0 if listing the markers succeeds, 1 otherwise. """ if source is None: markers = localmarkers(ui, repo) else: markers = remotemarkers(ui, repo, source, opts) for m in markers: if m['name'] != None: m['name'] = m['name'].decode('utf-8') if m['node'] != None: m['node'] = m['node'].decode('utf-8') if m['description'] != None: m['description'] = m['description'].decode('utf-8') json_opts = { 'indent': 2, 'sort_keys': True, } output_file = opts.get('output') if output_file: if os.path.exists(output_file): raise error.Abort(_('File "%s" already exists.' % output_file)) with open(output_file, 'w+') as f: json.dump(markers, f, **json_opts) else: json_data = json.dumps(markers, **json_opts) print(json_data) return 0 def localmarkers(ui, repo): markers = [] active_node = repo[b'.'].node() all_heads = set(repo.heads()) current_name = repo.dirstate.branch() - saw_current = False - saw_active = False branch_list = repo.branchmap().iterbranches() for branch_name, branch_heads, tip_node, is_closed in branch_list: for head_node in branch_heads: - is_active = (head_node == active_node) - is_tip = (head_node == tip_node) - is_current = (branch_name == current_name) - if is_current: - saw_current = True + is_active = False + if branch_name == current_name: + if head_node == active_node: + is_active = True - if is_active: - saw_active = True + is_tip = (head_node == tip_node) if is_closed: head_closed = True else: head_closed = bool(head_node not in all_heads) description = repo[head_node].description() markers.append({ 'type': 'branch', 'name': branch_name, 'node': node.hex(head_node), 'isActive': is_active, 'isClosed': head_closed, 'isTip': is_tip, - 'isCurrent': is_current, 'description': description, }) - # If the current branch (selected with "hg branch X") is not reflected in - # the list of heads we selected, add a virtual head for it so callers get - # a complete picture of repository marker state. - - if not saw_current: - markers.append({ - 'type': 'branch', - 'name': current_name, - 'node': None, - 'isActive': False, - 'isClosed': False, - 'isTip': False, - 'isCurrent': True, - 'description': None, - }) - bookmarks = repo._bookmarks active_bookmark = repo._activebookmark for bookmark_name, bookmark_node in arc_items(bookmarks): is_active = (active_bookmark == bookmark_name) description = repo[bookmark_node].description() - if is_active: - saw_active = True - markers.append({ 'type': 'bookmark', 'name': bookmark_name, 'node': node.hex(bookmark_node), 'isActive': is_active, 'description': description, }) - # If the current working copy state is not the head of a branch and there is - # also no active bookmark, add a virtual marker for it so callers can figure - # out exactly where we are. - - if not saw_active: - markers.append({ - 'type': 'commit', - 'name': None, - 'node': node.hex(active_node), - 'isActive': False, - 'isClosed': False, - 'isTip': False, - 'isCurrent': True, - 'description': repo[b'.'].description(), - }) + # Add virtual markers for the current commit state and current branch state + # so callers can figure out exactly where we are. + + # Common cases where this matters include: + + # You run "hg update 123" to update to an older revision. Your working + # copy commit will not be a branch head or a bookmark. + + # You run "hg branch X" to create a new branch, but have not made any commits + # yet. Your working copy branch will not be reflected in any commits. + + markers.append({ + 'type': 'branch-state', + 'name': current_name, + 'node': None, + 'isActive': True, + 'isClosed': False, + 'isTip': False, + 'description': None, + }) + + markers.append({ + 'type': 'commit-state', + 'name': None, + 'node': node.hex(active_node), + 'isActive': True, + 'isClosed': False, + 'isTip': False, + 'description': repo[b'.'].description(), + }) return markers def remotemarkers(ui, repo, source, opts): # Disable status output from fetching a remote. ui.quiet = True markers = [] source, branches = hg.parseurl(ui.expandpath(source)) remote = hg.peer(repo, opts, source) with remote.commandexecutor() as e: branchmap = e.callcommand(b'branchmap', {}).result() for branch_name in branchmap: for branch_node in branchmap[branch_name]: markers.append({ 'type': 'branch', 'name': branch_name, 'node': node.hex(branch_node), 'description': None, }) with remote.commandexecutor() as e: remotemarks = bookmarks.unhexlifybookmarks(e.callcommand(b'listkeys', { b'namespace': b'bookmarks', }).result()) for mark in remotemarks: markers.append({ 'type': 'bookmark', 'name': mark, 'node': node.hex(remotemarks[mark]), 'description': None, }) return markers