blob: fc39d4276e4f2f4c9491eb6cffbbf847565b3978 [file] [log] [blame]
import os
import re
import shutil
import logging
from binascii import b2a_hex
from datetime import datetime
from cStringIO import StringIO
from ConfigParser import ConfigParser
import tg
from pylons import app_globals as g
os.environ['HGRCPATH'] = '' # disable loading .hgrc
from mercurial import ui, hg
from pymongo.errors import DuplicateKeyError
from ming.base import Object
from ming.orm import Mapper, session
from ming.utils import LazyProperty
from allura import model as M
from allura.lib import helpers as h
from allura.model.repository import topological_sort, GitLikeTree
log = logging.getLogger(__name__)
class Repository(M.Repository):
tool_name='Hg'
repo_id='hg'
type_s='Hg Repository'
class __mongometa__:
name='hg-repository'
@LazyProperty
def _impl(self):
return HgImplementation(self)
def merge_command(self, merge_request):
'''Return the command to merge a given commit into a given target branch'''
return 'hg checkout %s\nhg pull -r %s %s' % (
merge_request.target_branch,
merge_request.downstream.commit_id,
merge_request.downstream_repo_url,
)
def count(self, branch='default'):
return super(Repository, self).count(branch)
def log(self, branch='default', offset=0, limit=10):
return super(Repository, self).log(branch, offset, limit)
class HgUI(ui.ui):
'''Hg UI subclass that suppresses reporting of untrusted hgrc files.'''
def __init__(self, *args, **kwargs):
super(HgUI, self).__init__(*args, **kwargs)
self._reportuntrusted = False
class HgImplementation(M.RepositoryImplementation):
re_hg_user = re.compile('(.*) <(.*)>')
def __init__(self, repo):
self._repo = repo
@LazyProperty
def _hg(self):
return hg.repository(HgUI(), self._repo.full_fs_path)
def init(self):
fullname = self._setup_paths()
log.info('hg init %s', fullname)
if os.path.exists(fullname):
shutil.rmtree(fullname)
repo = hg.repository(
ui.ui(), self._repo.full_fs_path, create=True)
self.__dict__['_hg'] = repo
self._setup_special_files()
self._repo.status = 'ready'
def clone_from(self, source_url):
'''Initialize a repo as a clone of another'''
self._repo.status = 'cloning'
session(self._repo).flush(self._repo)
log.info('Initialize %r as a clone of %s',
self._repo, source_url)
try:
fullname = self._setup_paths(create_repo_dir=False)
if os.path.exists(fullname):
shutil.rmtree(fullname)
# !$ hg doesn't like unicode as urls
src, repo = hg.clone(
ui.ui(),
source_url.encode('utf-8'),
self._repo.full_fs_path.encode('utf-8'),
update=False)
self.__dict__['_hg'] = repo
self._setup_special_files(source_url)
except:
self._repo.status = 'ready'
session(self._repo).flush(self._repo)
raise
log.info('... %r cloned', self._repo)
g.post_event('repo_cloned', source_url)
self._repo.refresh(notify=False)
def commit(self, rev):
'''Return a Commit object. rev can be _id or a branch/tag name'''
# See if the rev is a named ref that we have cached, and use the sha1
# from the cache. This ensures that we don't return a sha1 that we
# don't have indexed into mongo yet.
for ref in self._repo.heads + self._repo.branches + self._repo.repo_tags:
if ref.name == rev:
rev = ref.object_id
break
result = M.repo.Commit.query.get(_id=rev)
if result is None:
try:
impl = self._hg[str(rev)]
result = M.repo.Commit.query.get(_id=impl.hex())
except Exception, e:
log.exception(e)
if result is None: return None
result.set_context(self._repo)
return result
def real_parents(self, ci):
"""Return all parents of a commit, excluding the 'null revision' (a
fake revision added as the parent of the root commit by the Hg api).
"""
return [p for p in ci.parents() if p]
def all_commit_ids(self):
"""Return a list of commit ids, starting with the head(s) and ending
with the root (first commit) of the tree.
"""
graph = {}
to_visit = [ self._hg[hd] for hd in self._hg.heads() ]
while to_visit:
obj = to_visit.pop()
if obj.hex() in graph: continue
parents = self.real_parents(obj)
graph[obj.hex()] = set(
p.hex() for p in parents
if p.hex() != obj.hex())
to_visit += parents
return reversed([ ci for ci in topological_sort(graph) ])
def new_commits(self, all_commits=False):
graph = {}
to_visit = [ self._hg[hd] for hd in self._hg.heads() ]
while to_visit:
obj = to_visit.pop()
if obj.hex() in graph: continue
if not all_commits:
# Look up the object
if M.repo.Commit.query.find(dict(_id=obj.hex())).count():
graph[obj.hex()] = set() # mark as parentless
continue
parents = self.real_parents(obj)
graph[obj.hex()] = set(
p.hex() for p in parents
if p.hex() != obj.hex())
to_visit += parents
return list(topological_sort(graph))
def refresh_heads(self):
self._repo.heads = [
Object(name=None, object_id=self._hg[head].hex())
for head in self._hg.heads()]
self._repo.branches = []
for name, tag in self._hg.branchtags().iteritems():
if ("close" not in self._hg.changelog.read(tag)[5]):
self._repo.branches.append(
Object(name=name, object_id=self._hg[tag].hex()))
self._repo.repo_tags = [
Object(name=name, object_id=self._hg[tag].hex())
for name, tag in self._hg.tags().iteritems()]
session(self._repo).flush()
def refresh_commit_info(self, oid, seen, lazy=True):
from allura.model.repo import CommitDoc
ci_doc = CommitDoc.m.get(_id=oid)
if ci_doc and lazy: return False
obj = self._hg[oid]
# Save commit metadata
mo = self.re_hg_user.match(obj.user())
if mo:
user_name, user_email = mo.groups()
else:
user_name = user_email = obj.user()
user = Object(
name=h.really_unicode(user_name),
email=h.really_unicode(user_email),
date=datetime.utcfromtimestamp(obj.date()[0]))
fake_tree = self._tree_from_changectx(obj)
args = dict(
tree_id=fake_tree.hex(),
committed=user,
authored=user,
message=h.really_unicode(obj.description() or ''),
child_ids=[],
parent_ids=[ p.hex() for p in self.real_parents(obj)
if p.hex() != obj.hex() ])
if ci_doc:
ci_doc.update(args)
ci_doc.m.save()
else:
ci_doc = CommitDoc(dict(args, _id=oid))
try:
ci_doc.m.insert(safe=True)
except DuplicateKeyError:
if lazy: return False
self.refresh_tree_info(fake_tree, seen, lazy)
return True
def refresh_tree_info(self, tree, seen, lazy=True):
from allura.model.repo import TreeDoc
if lazy and tree.hex() in seen: return
seen.add(tree.hex())
doc = TreeDoc(dict(
_id=tree.hex(),
tree_ids=[],
blob_ids=[],
other_ids=[]))
for name, t in tree.trees.iteritems():
self.refresh_tree_info(t, seen, lazy)
doc.tree_ids.append(
dict(name=h.really_unicode(name), id=t.hex()))
for name, oid in tree.blobs.iteritems():
doc.blob_ids.append(
dict(name=h.really_unicode(name), id=oid))
doc.m.save(safe=False)
return doc
def log(self, object_id, skip, count):
obj = self._hg[object_id]
candidates = [ obj ]
result = []
seen = set()
while count and candidates:
candidates.sort(key=lambda c:sum(c.date()))
obj = candidates.pop(-1)
if obj.hex() in seen: continue
seen.add(obj.hex())
if skip == 0:
result.append(obj.hex())
count -= 1
else:
skip -= 1
candidates += self.real_parents(obj)
return result, [ p.hex() for p in candidates ]
def open_blob(self, blob):
fctx = self._hg[blob.commit._id][h.really_unicode(blob.path()).encode('utf-8')[1:]]
return StringIO(fctx.data())
def blob_size(self, blob):
fctx = self._hg[blob.commit._id][h.really_unicode(blob.path()).encode('utf-8')[1:]]
return fctx.size()
def _setup_hooks(self, source_path=None):
'Set up the hg changegroup hook'
hgrc = os.path.join(self._repo.fs_path, self._repo.name, '.hg', 'hgrc')
cp = ConfigParser()
cp.read(hgrc)
if not cp.has_section('hooks'):
cp.add_section('hooks')
url = (tg.config.get('base_url', 'http://localhost:8080')
+ '/auth/refresh_repo' + self._repo.url())
cp.set('hooks','; = [the next line is required for site integration, do not remove/modify]', '')
cp.set('hooks','changegroup.sourceforge','curl -s %s' % url)
with open(hgrc, 'w') as fp:
cp.write(fp)
os.chmod(hgrc, 0755)
def _tree_from_changectx(self, changectx):
'''Build a fake git-like tree from a changectx and its manifest'''
root = GitLikeTree()
for filepath in changectx.manifest():
fctx = changectx[filepath]
oid = b2a_hex(fctx.filenode())
root.set_blob(filepath, oid)
return root
def symbolics_for_commit(self, commit):
branch_heads, tags = super(self.__class__, self).symbolics_for_commit(commit)
ctx = self._hg[commit._id]
return [ctx.branch()], tags
def compute_tree_new(self, commit, tree_path='/'):
ctx = self._hg[commit._id]
fake_tree = self._tree_from_changectx(ctx)
fake_tree = fake_tree.get_tree(tree_path)
tree = self.refresh_tree_info(fake_tree, set())
return tree._id
Mapper.compile_all()