Commits

kankri  committed 8f7625c

Initial version of an extension to control subrepository push behavior.

Adds "ui.pushsubrepos" configuration variable with allowed values
"push", "no_push" and "abort_if_outgoing" to control the push strategy.

  • Participants

Comments (0)

Files changed (1)

+"""control subrepository push behavior
+
+This extension adds two alternative strategies for subrepository handling
+when :hg:`push` is issued in the parent repository.
+
+The behavior is controlled by the setting ``pushsubrepos`` in the ``[ui]``
+section. This setting is specified in the parent repository ``hgrc`` file, or
+through the other standard ways of setting configuration options (see
+:hg:`help config`).
+
+One of the following values can be set for ``ui.pushsubrepos``:
+
+- ``push`` This is the default, built-in Mercurial policy.
+
+  When :hg:`push` is issued in the parent repository, Mercurial first loops
+  through each subrepository and attempts to push _any_ outgoing changes to
+  the same repository where the the subrepository was originally pulled from
+  (specified in the ``.hgsub`` file).
+
+  If pushing the subrepository fails (e.g. no push permission), the whole
+  command is aborted.
+
+  This strategy tries to prevent pushing a parent repository state which
+  can't be pulled by others because not all the subrepository contents are
+  publicly available.
+
+- ``no_push`` Never push anything from the subrepositories.
+
+  When subrepositories are used to pull in read-only 3rd party dependencies,
+  the standard strategy can be inefficient: it will always query the remote
+  repositories which can be slow, especially with ``ssh`` connections and when
+  there are multiple repositories involved.
+
+  If it is known that the subrepositories are not locally modified, the
+  ``no_push`` strategy can be used to skip all subrepository checks.
+
+  .. note::
+    Using ``no_push`` makes it possible to accidentally push a parent
+    repository state which refers to subrepositories not available to others.
+
+    However, it is already possible to commit and push partial directory
+    contents (e.g. forgetting to add a new file under version control) and make
+    a project non-working for others. Fixing unavailable subrepository commits
+    can be handled the same way as forgotted file additions, and should even
+    be easier to spot.
+ 
+- ``abort_if_outgoing`` Abort push if outgoing changes are detected
+
+  Mercurial normally tries to push any outgoing changes it finds in a
+  subrepository back to the repository where it was pulled from. This can be
+  either useless (the user doesn't have push permission) or dangerous (the
+  user has push permission).
+
+  In many cases development is done by pulling from an upstream repository and
+  pushing to a fork so that new contributions can be reviewed and tested.
+  Trying to push directly to the upstream repository is undesirable.
+ 
+  Using the ``abort_if_outgoing`` strategy, any locally committed modifications
+  not present in the upstream will abort the operation. The user can then e.g.
+  push those changes to a fork of the original repository, edit ``.hgsub``
+  to point there and try to push again.
+
+  .. note::
+    Mercurial's default strategy seems to want to push any outgoing revisions,
+    whether they have been snapshotted in the parent repository or not.
+
+    ``abort_if_outgoing`` stops the command only if the *latest* snapshotted
+    subrepository revision is not present in the remote repository, even if
+    there are other outgoing commits in the subrepository.
+
+    The ideal behavior would be to stop only if *any* of the snapshotted
+    subrepository revisions are not in the remote repository. This is
+    more complicated to implement and the current behavior is usually
+    good enough, so the ideal implementation is postponed for now.
+"""
+
+from mercurial.subrepo import _abssource, subrelpath, hgsubrepo, state
+from mercurial import subrepo, discovery, node, util, error
+from mercurial.i18n import _
+from mercurial import hg
+
+def missing_in_remote(self, remote, force, revs):
+	outgoing = discovery.findcommonoutgoing(
+		self, remote, onlyheads=revs, force=force)
+	return outgoing.missing
+
+def wrap_repo(superclass):
+	class pushctl_subrepo(superclass):
+		def push(self, opts):
+			parent_ui = self._repo._subparent.ui
+			ui_pushsubrepos = parent_ui.config('ui', 'pushsubrepos', 'push')
+
+			if ui_pushsubrepos == 'push':
+				parent_ui.debug('pushing %s if it has outgoing changes\n' %
+					subrelpath(self))
+				return superclass.push(self, opts)
+
+			dsturl = _abssource(self._repo, True)
+
+			if ui_pushsubrepos == 'no_push':
+				parent_ui.note(_('not pushing subrepo %s to %s\n') % (
+					subrelpath(self), dsturl
+				))
+				return None
+
+			if ui_pushsubrepos == 'abort_if_outgoing':
+				# push subrepos depth-first for coherent ordering
+				c = self._repo['']
+				subs = c.substate # only repos that are committed
+				for s in sorted(subs):
+					if c.sub(s).push(opts) == 0:
+						return False
+
+				force = opts.get('force')
+				ssh = opts.get('ssh')
+
+				# TODO: should check all the subrepo revisions in all
+				# outgoing .hgsubstates
+				other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
+				miss = missing_in_remote(
+					self._repo, other, force, [node.bin(self._state[1])]
+				)
+				if miss:
+					raise util.Abort('changes of %s not present in %s' %
+						(subrelpath(self), dsturl))
+				parent_ui.note('no changes in %s: not pushing to %s\n' %
+					(subrelpath(self), dsturl))
+				return
+
+			def fmt_list(*x):
+				return ', '.join(['"%s"' % s for s in (x)])
+			raise error.ConfigError(
+				_('"ui.pushsubrepos" should be one of (%s), not "%s"') % (
+					fmt_list('push', 'no_push', 'abort_if_outgoing'),
+					ui_pushsubrepos,
+				)
+			)
+	return pushctl_subrepo
+
+for k in subrepo.types.keys():
+	subrepo.types[k] = wrap_repo(subrepo.types[k])