diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -353,13 +353,56 @@ // Load the refs we're planning to fetch from the remote repository. $remote_refs = $this->loadGitRemoteRefs( $repository, - $repository->getRemoteURIEnvelope()); + $repository->getRemoteURIEnvelope(), + $is_local = false); // Load the refs we're planning to fetch from the local repository, by // using the local working copy path as the "remote" repository URI. $local_refs = $this->loadGitRemoteRefs( $repository, - new PhutilOpaqueEnvelope($path)); + new PhutilOpaqueEnvelope($path), + $is_local = true); + + // See T13448. The "git fetch --prune ..." flag only prunes local refs + // matching the refspecs we pass it. If "Fetch Refs" is configured, we'll + // pass it a very narrow list of refspecs, and it won't prune older refs + // that aren't currently subject to fetching. + + // Since we want to prune everything that isn't (a) on the fetch list and + // (b) in the remote, handle pruning of any surplus leftover refs ourselves + // before we fetch anything. + + // (We don't have to do this if "Fetch Refs" isn't set up, since "--prune" + // will work in that case, but it's a little simpler to always go down the + // same code path.) + + $surplus_refs = array(); + foreach ($local_refs as $local_ref => $local_hash) { + $remote_hash = idx($remote_refs, $local_ref); + if ($remote_hash === null) { + $surplus_refs[] = $local_ref; + } + } + + if ($surplus_refs) { + $this->log( + pht( + 'Found %s surplus local ref(s) to delete.', + phutil_count($surplus_refs))); + foreach ($surplus_refs as $surplus_ref) { + $this->log( + pht( + 'Deleting surplus local ref "%s" ("%s").', + $surplus_ref, + $local_refs[$surplus_ref])); + + $repository->execLocalCommand( + 'update-ref -d %R --', + $surplus_ref); + + unset($local_refs[$surplus_ref]); + } + } if ($remote_refs === $local_refs) { $this->log( @@ -378,7 +421,7 @@ // checked out. See T13280. $future = $repository->getRemoteCommandFuture( - 'fetch --prune --update-head-ok -- %P %Ls', + 'fetch --update-head-ok -- %P %Ls', $repository->getRemoteURIEnvelope(), $fetch_rules); @@ -474,21 +517,32 @@ private function loadGitRemoteRefs( PhabricatorRepository $repository, - PhutilOpaqueEnvelope $remote_envelope) { + PhutilOpaqueEnvelope $remote_envelope, + $is_local) { - $ref_rules = $this->getGitRefRules($repository); + // See T13448. When listing local remotes, we want to list everything, + // not just refs we expect to fetch. This allows us to detect that we have + // undesirable refs (which have been deleted in the remote, but are still + // present locally) so we can update our state to reflect the correct + // remote state. - // NOTE: "git ls-remote" does not support "--" until circa January 2016. - // See T12416. None of the flags to "ls-remote" appear dangerous, but - // refuse to list any refs beginning with "-" just in case. + if ($is_local) { + $ref_rules = array(); + } else { + $ref_rules = $this->getGitRefRules($repository); - foreach ($ref_rules as $ref_rule) { - if (preg_match('/^-/', $ref_rule)) { - throw new Exception( - pht( - 'Refusing to list potentially dangerous ref ("%s") beginning '. - 'with "-".', - $ref_rule)); + // NOTE: "git ls-remote" does not support "--" until circa January 2016. + // See T12416. None of the flags to "ls-remote" appear dangerous, but + // refuse to list any refs beginning with "-" just in case. + + foreach ($ref_rules as $ref_rule) { + if (preg_match('/^-/', $ref_rule)) { + throw new Exception( + pht( + 'Refusing to list potentially dangerous ref ("%s") beginning '. + 'with "-".', + $ref_rule)); + } } }