sub-repository extension

Azra Aiyl azraiyl at gmail.com
Wed Nov 5 16:38:11 UTC 2008


I've created a small (not bullet-proof) sub-repository extension for
mercurial.

The company I work for, has more than user that only likes to use TortoiseHg or
two or three commands on the command line. The forest extensions therefore is
overkill and AFAIK not integrated into Eclipse/Tortoise or any GUI. Here is the
basic idea how it works:

You have a repository were you normally work that needs some libraries in
another repository. You add this sub-repository inside your current
main-repository. If you don't have any extension the sub-repositories have to be
updated manually by everyone to the correct revision. This extensions only adds
some commands to this ideas:

subrepofind -> Search inside the current repository if there is any other
repository. If there is any repository print the source location, the name and
the current revision of this repository. This command is not really needed, you
can do this manually as well.

subrepoadd -> Add any sub-repository to the file .hgsubrepo. The source
location, name and current revision is added to this file. If you like you can
modify this file manually, it is readable. Command is therefore as
usefull/useless as hg tag.

subrepogather -> Search through every repository in .hgsubrepo and update the
revision to the most current revision. This command is normally called with a
hook automatically.

The extension adds a precommit hook and an update hook. There is also a hack to
dynamically add a preprecommit hook. The downside of the normal precommit hook
is that it is only called if there is any change in the current repository and
the hook can't see if there are any changes made in the precommit hook itself.
This preprecommit is called everytime.

How does it work?

Whenever anyone commits a change to main-repository the preprecommit hook
updates the .hgsubrepo file in the main-repository with the latest revision of
all previsouly registered sub-repositories. Hg itselfs checks if the file does
not change, therefore it does not matter if we rewrite this file every time.

Then the precommit hook is called. It checks if there is any modification in any
sub-repository. If there is any modification a commit to the main-repository is
disallowed. If you don't like this behaviour just remove the precommit hook.

Whenever you update your main-repository all sub-repository are updated
according to the .hgsubrepo file.

There is only (to some extent) clever idea here. If you manually update a
sub-repository here to a new revision and commit the main-repository, the
extension rewrites the .hgsubrepo file and then commits the new revision. If
you push this modification anyone that pulls and updates the repository
automagically has updated sub-repositories. Because of this it integrates not
that bad into an existing environment without the need to add a new commands to
existing scripts.

If anyone is interested in this I'm open for ideas, improvements and
maybe writing a short tutorial.

Azraiyl


[Mercurial.ini]

subrepo = subrepo.py
[hooks]
precommit = python:subrepo.precommit
update = python:subrepo.update

[subrepo.py]

import string
import sys
import os
import os.path

from mercurial import hg, node, localrepo, util

def readconfig(repo):
	"""read a subrepo file"""
	subrepos = []
	filename = os.path.join(repo.root, ".hgsubrepo")
	if os.path.exists(filename):
		for line in open(filename, "rb").readlines():
			if (not line.startswith("#")) and (not line.startswith(";")) and
(len(line) >= len("? ? ?")):
				line = line.replace("\t", " ").replace("=", " ")
				tokens = line.split(" ")
				source = tokens[0].strip()
				name = tokens[1].strip()
				rev = tokens[2].strip()
				subrepos += [ [ source, name, rev ] ]
	return subrepos

def writeconfig(repo, subrepos):
	"""write subrepo list to a file and remove duplicates"""
	filename = os.path.join(repo.root, ".hgsubrepo")
	content = ""
	unique = []
	for subrepo in subrepos:
		if subrepo[1] not in unique:
			content += "%s %s %s\n" % tuple(subrepo)
			unique += [ subrepo[1] ]
	# do not create a file if there is no subrepo
	if len(content) > 0:
		open(filename, "wb").write(content)

def repocurrent(repo):
	"""get current revision"""
	return node.hex(repo.parents()[0].node())

def repotip(repo):
	"""get tip revision"""
	return node.hex(repo.heads()[0])

def precommit(ui, repo, parent1, parent2, hooktype):
	"""check if there are any uncommited changes inside any sub repository"""

	#print "[PreCommit Hook I]"

	#print "ui", [ui]
	#print "repo", [repo]
	#print "parent1", [parent1]
	#print "parent2", [parent2]
	#print "hooktype", [hooktype]

	subrepos = []
	for subrepo in readconfig(repo):
		subreposource, subreponame, subreporev = subrepo
		subrepopath = os.path.join(repo.root, subreponame)
		subrepo = hg.repository(ui, subrepopath)
		modified, added, removed, deleted = subrepo.status()[:4]
		if modified or added or removed or deleted:
			ui.write("sub repository \"%s\" has modifications!\n" % (subreponame))
			return True

	#print "[PreCommit Hook O]"
	#return True # Abort

def update(ui, repo, parent1, parent2, hooktype, error):
	"""update (and clone if necessary) all sub repositories"""

	#print "[Update Hook I]"

	#print "ui", [ui]
	#print "repo", [repo]
	#print "parent1", [parent1]
	#print "parent2", [parent2]
	#print "hooktype", [hooktype]
	#print "erro", [error]

	for subrepo in readconfig(repo):
		subreposource, subreponame, subreporev = subrepo
		subrepopath = os.path.join(repo.root, subreponame)
		if not os.path.exists(subrepopath):
			hg.clone(ui, subreposource, subrepopath) # see hg.py for more parameters
		subrepo = hg.repository(ui, subrepopath)
		modified, added, removed, deleted = subrepo.status()[:4]
		if modified or added or removed or deleted:
			ui.write("sub repository \"%s\" has modifications! no update!\n" %
(subreponame))
		else:
			hg.update(subrepo, node.bin(subreporev))

	#print "[Update Hook O]"
	#return True # Abort

def subrepogather(ui, repo, *dirs, **opts):
	"""get the revision of all known sub repositories and rewrite .hgsubrepo"""

	#print "[SubRepoGather I]"

	#print "ui", [ui]
	#print "repo", [repo]
	#print "dirs", [dirs]
	#print "opts", [opts]

	subrepos = []
	for subrepo in readconfig(repo):
		subreposource, subreponame, subreporev = subrepo
		subrepopath = os.path.join(repo.root, subreponame)
		subrepo = hg.repository(ui, subrepopath)
		subreporev = repocurrent(subrepo)
		subrepos += [ [ subreposource, subreponame, subreporev ] ]
	writeconfig(repo, subrepos)

	#print "[SubRepogather O]"

def subrepofind(ui, repo, *dirs, **opts):
	"""try to find find any sub repository recurisvely"""

	#print "[SubRepoFind I]"

	#print "ui", [ui]
	#print "repo", [repo]
	#print "dirs", [dirs]
	#print "opts", [opts]

	ui.write("# source path revision\n")
	for path, directories, files in os.walk(repo.root):
		if (".hg" in directories) and (path != repo.root):
			subrepo = hg.repository(ui, path)
			# FIXME that is a bit hackish there is maybe a more generic way to
get a relative path
			subreponame = subrepo.root[len(repo.root) + 1:]
			#ui.write("Path: %s\n" % subrepo.root)
			#ui.write("Source: %s\n" % subrepo.ui.expandpath("default"))
			#ui.write("Current Revision: %s\n" % repocurrent(subrepo))
			#ui.write("Tip Revision: %s\n" % repotip(subrepo))
			ui.write("%s %s %s\n" % (subrepo.ui.expandpath("default"),
subreponame, repocurrent(subrepo)))

	#print "[SubRepoFind O]"

def subrepoadd(ui, repo, *dirs, **opts):
	"""add a subrepo to .hgsubrepo"""

	#print "[SubRepoAdd I]"

	#print "ui", [ui]
	#print "repo", [repo]
	#print "dirs", [dirs]
	#print "opts", [opts]

	if len(dirs) == 1:
		subreponame = dirs[0]
		ui.write("add sub repository \"%s\"\n" % subreponame)
		subrepopath = os.path.join(repo.root, subreponame)
		subrepo = hg.repository(ui, subrepopath)
		subreposource = subrepo.ui.expandpath("default")
		subreponame = subreponame
		subreporev = repocurrent(subrepo)
		subrepos = readconfig(repo)
		subrepos += [ [ subreposource, subreponame, subreporev ] ]
		writeconfig(repo, subrepos)

	#print "[SubRepoAdd O]"

cmdtable = {
	"subrepogather|srgather": (subrepogather, [], "hg subrepogather"),
	"subrepofind|srfind": (subrepofind, [], "hg subrepofind"),
	"subrepoadd|sradd": (subrepoadd, [], "hg subrepoadd [SUBREPOSITORY]"),
}

def preprecommit(ui, repo):
	#print "[PrePreCommit Hook I]"

	#print "ui", [ui]
	#print "repo", [repo]

	subrepogather(ui, repo)

	#print "[PrePreCommit Hook O]"

	return False

def commit(self, *args, **kwargs):
	"""
	Hack to get a real precommit hook. One that allows to modify files e.g.
	The precommit hook is only called in localrepo.commit. I hope it is
	safe to only decorate localrepo.commit.
	"""
	if preprecommit(self.ui, self):
		raise util.Abort("preprecommit hook failed")
	self.origcommit(*args, **kwargs)

localrepo.localrepository.origcommit = localrepo.localrepository.commit
localrepo.localrepository.commit = commit



More information about the Mercurial mailing list