diff options
author | Qijiang Fan <fqj1994@gmail.com> | 2011-10-25 12:37:40 +0800 |
---|---|---|
committer | Qijiang Fan <fqj1994@gmail.com> | 2011-10-25 12:37:40 +0800 |
commit | 4d96cae645ca33d56b4a4961f2b59bef82773b58 (patch) | |
tree | 3a7085b11447f986526447bceca48170e3602af4 /hgsubversion | |
parent | 6e8f1b6597c85527b4721f376e0360bb25543cc8 (diff) |
Upstream version 1.3
Diffstat (limited to 'hgsubversion')
-rw-r--r-- | hgsubversion/__init__.py | 19 | ||||
-rw-r--r-- | hgsubversion/editor.py | 43 | ||||
-rw-r--r-- | hgsubversion/maps.py | 86 | ||||
-rw-r--r-- | hgsubversion/pushmod.py | 11 | ||||
-rw-r--r-- | hgsubversion/replay.py | 4 | ||||
-rw-r--r-- | hgsubversion/stupid.py | 140 | ||||
-rw-r--r-- | hgsubversion/svncommands.py | 14 | ||||
-rw-r--r-- | hgsubversion/svnexternals.py | 27 | ||||
-rw-r--r-- | hgsubversion/svnmeta.py | 16 | ||||
-rw-r--r-- | hgsubversion/svnrepo.py | 27 | ||||
-rw-r--r-- | hgsubversion/svnwrap/common.py | 2 | ||||
-rw-r--r-- | hgsubversion/svnwrap/subvertpy_wrapper.py | 17 | ||||
-rw-r--r-- | hgsubversion/svnwrap/svn_swig_wrapper.py | 4 | ||||
-rw-r--r-- | hgsubversion/util.py | 78 | ||||
-rw-r--r-- | hgsubversion/wrappers.py | 43 |
15 files changed, 380 insertions, 151 deletions
diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py index 10fb63f..cc336c8 100644 --- a/hgsubversion/__init__.py +++ b/hgsubversion/__init__.py @@ -38,14 +38,14 @@ try: # force demandimport to load templatekw templatekw.keywords except ImportError: - templatekw = None + templatekw = None try: from mercurial import revset # force demandimport to load revset revset.methods except ImportError: - revset = None + revset = None try: from mercurial import subrepo @@ -99,14 +99,21 @@ wrapcmds = { # cmd: generic, target, fixdoc, ppopts, opts ]), } - -# only need the discovery variant of this code when we drop hg < 1.6 try: from mercurial import discovery + def findcommonoutgoing(orig, *args, **opts): + capable = getattr(args[1], 'capable', lambda x: False) + if capable('subversion'): + return wrappers.findcommonoutgoing(*args, **opts) + else: + return orig(*args, **opts) + extensions.wrapfunction(discovery, 'findcommonoutgoing', findcommonoutgoing) +except AttributeError: + # only need the discovery variant of this code when we drop hg < 1.6 def findoutgoing(orig, *args, **opts): capable = getattr(args[1], 'capable', lambda x: False) if capable('subversion'): - return wrappers.outgoing(*args, **opts) + return wrappers.findoutgoing(*args, **opts) else: return orig(*args, **opts) extensions.wrapfunction(discovery, 'findoutgoing', findoutgoing) @@ -169,7 +176,7 @@ def extsetup(): def reposetup(ui, repo): if repo.local(): - svnrepo.generate_repo_class(ui, repo) + svnrepo.generate_repo_class(ui, repo) _old_local = hg.schemes['file'] def _lookup(url): diff --git a/hgsubversion/editor.py b/hgsubversion/editor.py index 44c39b4..54c04ac 100644 --- a/hgsubversion/editor.py +++ b/hgsubversion/editor.py @@ -8,6 +8,7 @@ from mercurial import node import svnwrap import util +import svnexternals class RevisionData(object): @@ -118,7 +119,7 @@ class HgEditor(svnwrap.Editor): # assuming it is a directory self.current.externals[path] = None map(self.current.delete, [pat for pat in self.current.files.iterkeys() - if pat.startswith(path+'/')]) + if pat.startswith(path + '/')]) for f in ctx.walk(util.PrefixMatch(br_path2)): f_p = '%s/%s' % (path, f[len(br_path2):]) if f_p not in self.current.files: @@ -231,11 +232,11 @@ class HgEditor(svnwrap.Editor): if tag: changeid = self.meta.tags[tag] source_rev, source_branch = self.meta.get_source_rev(changeid)[:2] - cp_f = '' + frompath = '' else: source_rev = copyfrom_revision - cp_f, source_branch = self.meta.split_branch_path(copyfrom_path)[:2] - if cp_f == '' and br_path == '': + frompath, source_branch = self.meta.split_branch_path(copyfrom_path)[:2] + if frompath == '' and br_path == '': assert br_path is not None tmp = source_branch, source_rev, self.current.rev.revnum self.meta.branches[branch] = tmp @@ -243,23 +244,22 @@ class HgEditor(svnwrap.Editor): if new_hash == node.nullid: self.current.missing.add('%s/' % path) return path - cp_f_ctx = self.repo.changectx(new_hash) - if cp_f != '/' and cp_f != '': - cp_f = '%s/' % cp_f + fromctx = self.repo.changectx(new_hash) + if frompath != '/' and frompath != '': + frompath = '%s/' % frompath else: - cp_f = '' + frompath = '' copies = {} - for f in cp_f_ctx: - if not f.startswith(cp_f): + for f in fromctx: + if not f.startswith(frompath): continue - f2 = f[len(cp_f):] - fctx = cp_f_ctx.filectx(f) - fp_c = path + '/' + f2 - self.current.set(fp_c, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) - if fp_c in self.current.deleted: - del self.current.deleted[fp_c] + fctx = fromctx.filectx(f) + dest = path + '/' + f[len(frompath):] + self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) + if dest in self.current.deleted: + del self.current.deleted[dest] if branch == source_branch: - copies[fp_c] = f + copies[dest] = f if copies: # Preserve the directory copy records if no file was changed between # the source and destination revisions, or discard it completely. @@ -267,8 +267,15 @@ class HgEditor(svnwrap.Editor): if parentid != revlog.nullid: parentctx = self.repo.changectx(parentid) for k, v in copies.iteritems(): - if util.issamefile(parentctx, cp_f_ctx, v): + if util.issamefile(parentctx, fromctx, v): self.current.copies[k] = v + # Copy the externals definitions of copied directories + fromext = svnexternals.parse(self.ui, fromctx) + for p, v in fromext.iteritems(): + pp = p and (p + '/') or '' + if pp.startswith(frompath): + dest = (path + '/' + pp[len(frompath):]).rstrip('/') + self.current.externals[dest] = v return path @svnwrap.ieditor diff --git a/hgsubversion/maps.py b/hgsubversion/maps.py index 7fbe2cf..93a3cbb 100644 --- a/hgsubversion/maps.py +++ b/hgsubversion/maps.py @@ -5,6 +5,7 @@ from mercurial import util as hgutil from mercurial import node import svncommands +import util class AuthorMap(dict): '''A mapping from Subversion-style authors to Mercurial-style @@ -34,6 +35,8 @@ class AuthorMap(dict): def load(self, path): ''' Load mappings from a file at the specified path. ''' + + path = os.path.expandvars(path) if not os.path.exists(path): return @@ -43,12 +46,9 @@ class AuthorMap(dict): self.ui.note('reading authormap from %s\n' % path) f = open(path, 'r') - for number, line in enumerate(f): - - if writing: - writing.write(line) + for number, line_org in enumerate(f): - line = line.split('#')[0] + line = line_org.split('#')[0] if not line.strip(): continue @@ -61,10 +61,15 @@ class AuthorMap(dict): src = src.strip() dst = dst.strip() - self.ui.debug('adding author %s to author map\n' % src) - if src in self and dst != self[src]: - msg = 'overriding author: "%s" to "%s" (%s)\n' - self.ui.status(msg % (self[src], dst, src)) + + if writing: + if not src in self: + self.ui.debug('adding author %s to author map\n' % src) + elif dst != self[src]: + msg = 'overriding author: "%s" to "%s" (%s)\n' + self.ui.status(msg % (self[src], dst, src)) + writing.write(line_org) + self[src] = dst f.close() @@ -132,14 +137,14 @@ class Tags(dict): print 'tagmap too new -- please upgrade' raise NotImplementedError for l in f: - hash, revision, tag = l.split(' ', 2) + ha, revision, tag = l.split(' ', 2) revision = int(revision) tag = tag[:-1] if self.endrev is not None and revision > self.endrev: break if not tag: continue - dict.__setitem__(self, tag, node.bin(hash)) + dict.__setitem__(self, tag, node.bin(ha)) f.close() def _write(self): @@ -164,11 +169,11 @@ class Tags(dict): def __setitem__(self, tag, info): if not tag: raise hgutil.Abort('tag cannot be empty') - hash, revision = info + ha, revision = info f = open(self.path, 'a') - f.write('%s %s %s\n' % (node.hex(hash), revision, tag)) + f.write('%s %s %s\n' % (node.hex(ha), revision, tag)) f.close() - dict.__setitem__(self, tag, hash) + dict.__setitem__(self, tag, ha) class RevMap(dict): @@ -178,13 +183,28 @@ class RevMap(dict): def __init__(self, repo): dict.__init__(self) self.path = os.path.join(repo.path, 'svn', 'rev_map') - self.youngest = 0 + self.ypath = os.path.join(repo.path, 'svn', 'lastpulled') + # TODO(durin42): Consider moving management of the youngest + # file to svnmeta itself rather than leaving it here. + # must load youngest file first, or else self._load() can + # clobber the info + _yonngest_str = util.load_string(self.ypath, '0') + self._youngest = int(_yonngest_str.strip()) self.oldest = 0 if os.path.isfile(self.path): self._load() else: self._write() + def _set_youngest(self, rev): + self._youngest = max(self._youngest, rev) + util.save_string(self.ypath, str(self._youngest) + '\n') + + def _get_youngest(self): + return self._youngest + + youngest = property(_get_youngest, _set_youngest) + def hashes(self): return dict((v, k) for (k, v) in self.iteritems()) @@ -199,7 +219,7 @@ class RevMap(dict): print 'revmap too new -- please upgrade' raise NotImplementedError for l in f: - revnum, hash, branch = l.split(' ', 2) + revnum, ha, branch = l.split(' ', 2) if branch == '\n': branch = None else: @@ -209,7 +229,7 @@ class RevMap(dict): self.youngest = revnum if revnum < self.oldest or not self.oldest: self.oldest = revnum - dict.__setitem__(self, (revnum, branch), node.bin(hash)) + dict.__setitem__(self, (revnum, branch), node.bin(ha)) f.close() def _write(self): @@ -217,17 +237,17 @@ class RevMap(dict): f.write('%s\n' % self.VERSION) f.close() - def __setitem__(self, key, hash): + def __setitem__(self, key, ha): revnum, branch = key f = open(self.path, 'a') b = branch or '' - f.write(str(revnum) + ' ' + node.hex(hash) + ' ' + b + '\n') + f.write(str(revnum) + ' ' + node.hex(ha) + ' ' + b + '\n') f.close() if revnum > self.youngest or not self.youngest: self.youngest = revnum if revnum < self.oldest or not self.oldest: self.oldest = revnum - dict.__setitem__(self, (revnum, branch), hash) + dict.__setitem__(self, (revnum, branch), ha) class FileMap(object): @@ -247,12 +267,12 @@ class FileMap(object): yield name[:e], name[e+1:] e = name.rfind('/', 0, e) - def check(self, map, path): - map = getattr(self, map) - for pre, suf in self._rpairs(path): - if pre not in map: + def check(self, m, path): + m = getattr(self, m) + for pre, _suf in self._rpairs(path): + if pre not in m: continue - return map[pre] + return m[pre] return None def __contains__(self, path): @@ -268,13 +288,17 @@ class FileMap(object): return False return True - def add(self, fn, map, path): - mapping = getattr(self, map) + # Needed so empty filemaps are false + def __len__(self): + return len(self.include) + len(self.exclude) + + def add(self, fn, m, path): + mapping = getattr(self, m) if path in mapping: msg = 'duplicate %s entry in %s: "%s"\n' - self.ui.status(msg % (map, fn, path)) + self.ui.status(msg % (m, fn, path)) return - bits = map.strip('e'), path + bits = m.strip('e'), path self.ui.debug('%sing %s\n' % bits) mapping[path] = path @@ -365,8 +389,8 @@ class TagMap(dict): oldname = newname other = - The oldname tag from SVN will be represented as newname in the hg tags; - the other tag will not be reflected in the hg repository. + The oldname tag from SVN will be represented as newname in the hg tags; + the other tag will not be reflected in the hg repository. ''' def __init__(self, ui, path): diff --git a/hgsubversion/pushmod.py b/hgsubversion/pushmod.py index e1f5231..7c0a104 100644 --- a/hgsubversion/pushmod.py +++ b/hgsubversion/pushmod.py @@ -122,6 +122,9 @@ def commit(ui, repo, rev_ctx, meta, base_revision, svn): props.setdefault(file, {})['svn:executable'] = '*' if 'l' in fctx.flags(): props.setdefault(file, {})['svn:special'] = '*' + isbinary = hgutil.binary(new_data) + if isbinary: + props.setdefault(file, {})['svn:mime-type'] = 'application/octet-stream' if file not in parent: renamed = fctx.renamed() @@ -141,6 +144,8 @@ def commit(ui, repo, rev_ctx, meta, base_revision, svn): if ('l' in parent.filectx(file).flags() and 'l' not in rev_ctx.filectx(file).flags()): props.setdefault(file, {})['svn:special'] = None + if hgutil.binary(base_data) and not isbinary: + props.setdefault(file, {})['svn:mime-type'] = None action = 'modify' else: pos = file.rfind('/') @@ -178,11 +183,7 @@ def commit(ui, repo, rev_ctx, meta, base_revision, svn): if tf in file_data and tf != ntf: file_data[ntf] = file_data[tf] if tf in props: - props[ntf] = props[tf] - del props[tf] - if hgutil.binary(file_data[ntf][1]): - props.setdefault(ntf, {}).update(props.get(ntf, {})) - props.setdefault(ntf, {})['svn:mime-type'] = 'application/octet-stream' + props[ntf] = props.pop(tf) del file_data[tf] addeddirs = [svnpath(d) for d in addeddirs] diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py index 92db01e..10d3b48 100644 --- a/hgsubversion/replay.py +++ b/hgsubversion/replay.py @@ -174,7 +174,7 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun): date, extra) - new_hash = meta.repo.commitctx(current_ctx) + new_hash = meta.repo.svn_commitctx(current_ctx) util.describe_commit(ui, new_hash, branch) if (rev.revnum, branch) not in meta.revmap and not tag: meta.revmap[rev.revnum, branch] = new_hash @@ -209,7 +209,7 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun): meta.authors[rev.author], date, extra) - new_hash = meta.repo.commitctx(current_ctx) + new_hash = meta.repo.svn_commitctx(current_ctx) util.describe_commit(ui, new_hash, branch) if (rev.revnum, branch) not in meta.revmap: meta.revmap[rev.revnum, branch] = new_hash diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py index 50e13d6..159396e 100644 --- a/hgsubversion/stupid.py +++ b/hgsubversion/stupid.py @@ -2,10 +2,11 @@ import cStringIO import errno import re -from mercurial import patch -from mercurial import node from mercurial import context +from mercurial import node +from mercurial import patch from mercurial import revlog +from mercurial import util as hgutil import svnwrap import svnexternals @@ -49,7 +50,6 @@ def print_your_svn_is_old_message(ui): #pragma: no cover ui.status("In light of that, I'll fall back and do diffs, but it won't do " "as good a job. You should really upgrade your server.\n") - def mempatchproxy(parentctx, files): # Avoid circular references patch.patchfile -> mempatch patchfile = patch.patchfile @@ -79,17 +79,32 @@ def mempatchproxy(parentctx, files): def filteriterhunks(meta): iterhunks = patch.iterhunks - def filterhunks(ui, fp, sourcefile=None, textmode=False): + def filterhunks(*args, **kwargs): + # ui, fp, sourcefile=None, textmode=False applycurrent = False # Passing False instead of textmode because we should never # be ignoring EOL type. - if not iterhunks.func_defaults: - # Since 1.7 (cfedc529e4a1) - gen = iterhunks(ui, fp) - elif len(iterhunks.func_defaults) == 1: - gen = iterhunks(ui, fp, sourcefile) + if iterhunks.func_code.co_argcount == 1: + # Since 1.9 (28762bb767dc) + fp = args[0] + gen = iterhunks(fp) else: - gen = iterhunks(ui, fp, sourcefile, textmode) + ui, fp = args[:2] + if len(args) > 2: + sourcefile = args[2] + else: + sourcefile = kwargs.get('sourcefile', None) + if len(args) > 3: + textmode = args[3] + else: + textmode = kwargs.get('textmode', False) + if not iterhunks.func_defaults: + # Since 1.7 (cfedc529e4a1) + gen = iterhunks(ui, fp) + elif len(iterhunks.func_defaults) == 1: + gen = iterhunks(ui, fp, sourcefile) + else: + gen = iterhunks(ui, fp, sourcefile, textmode) for data in gen: if data[0] == 'file': if data[1][1] in meta.filemap: @@ -101,6 +116,71 @@ def filteriterhunks(meta): yield data return filterhunks +def patchrepoold(ui, meta, parentctx, patchfp): + files = {} + try: + oldpatchfile = patch.patchfile + olditerhunks = patch.iterhunks + patch.patchfile = mempatchproxy(parentctx, files) + patch.iterhunks = filteriterhunks(meta) + try: + # We can safely ignore the changed list since we are + # handling non-git patches. Touched files are known + # by our memory patcher. + patch_st = patch.applydiff(ui, patchfp, {}, strip=0) + finally: + patch.patchfile = oldpatchfile + patch.iterhunks = olditerhunks + except patch.PatchError: + # TODO: this happens if the svn server has the wrong mime + # type stored and doesn't know a file is binary. It would + # be better to do one file at a time and only do a + # full fetch on files that had problems. + raise BadPatchApply('patching failed') + # if this patch didn't apply right, fall back to exporting the + # entire rev. + if patch_st == -1: + assert False, ('This should only happen on case-insensitive' + ' volumes.') + elif patch_st == 1: + # When converting Django, I saw fuzz on .po files that was + # causing revisions to end up failing verification. If that + # can be fixed, maybe this won't ever be reached. + raise BadPatchApply('patching succeeded with fuzz') + return files + +try: + class svnbackend(patch.repobackend): + def getfile(self, fname): + data, (islink, isexec) = super(svnbackend, self).getfile(fname) + if islink: + data = 'link ' + data + return data, (islink, isexec) +except AttributeError: + svnbackend = None + +def patchrepo(ui, meta, parentctx, patchfp): + if not svnbackend: + return patchrepoold(ui, meta, parentctx, patchfp) + store = patch.filestore() + try: + touched = set() + backend = svnbackend(ui, meta.repo, parentctx, store) + ret = patch.patchbackend(ui, backend, patchfp, 0, touched) + if ret < 0: + raise BadPatchApply('patching failed') + if ret > 0: + raise BadPatchApply('patching succeeded with fuzz') + files = {} + for f in touched: + try: + data, mode, copied = store.getfile(f) + files[f] = data + except IOError: + files[f] = None + return files + finally: + store.close() def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx): """Extract all 'branch' content at a given revision. @@ -146,38 +226,9 @@ def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx): # are marked as touched. Content is loaded on demand. touched_files.update(any_file_re.findall(d)) if d2.strip() and len(re.findall('\n[-+]', d2.strip())) > 0: - try: - oldpatchfile = patch.patchfile - olditerhunks = patch.iterhunks - patch.patchfile = mempatchproxy(parentctx, files_data) - patch.iterhunks = filteriterhunks(meta) - try: - # We can safely ignore the changed list since we are - # handling non-git patches. Touched files are known - # by our memory patcher. - patch_st = patch.applydiff(ui, cStringIO.StringIO(d2), - {}, strip=0) - finally: - patch.patchfile = oldpatchfile - patch.iterhunks = olditerhunks - except patch.PatchError: - # TODO: this happens if the svn server has the wrong mime - # type stored and doesn't know a file is binary. It would - # be better to do one file at a time and only do a - # full fetch on files that had problems. - raise BadPatchApply('patching failed') + files_data = patchrepo(ui, meta, parentctx, cStringIO.StringIO(d2)) for x in files_data.iterkeys(): ui.note('M %s\n' % x) - # if this patch didn't apply right, fall back to exporting the - # entire rev. - if patch_st == -1: - assert False, ('This should only happen on case-insensitive' - ' volumes.') - elif patch_st == 1: - # When converting Django, I saw fuzz on .po files that was - # causing revisions to end up failing verification. If that - # can be fixed, maybe this won't ever be reached. - raise BadPatchApply('patching succeeded with fuzz') else: ui.status('Not using patch for %s, diff had no hunks.\n' % r.revnum) @@ -357,7 +408,7 @@ def fetch_externals(ui, svn, branchpath, r, parentctx): # revision in the common case. dirs = set(externals) if parentctx.node() == revlog.nullid: - dirs.update([p for p,k in svn.list_files(branchpath, r.revnum) if k == 'd']) + dirs.update([p for p, k in svn.list_files(branchpath, r.revnum) if k == 'd']) dirs.add('') else: branchprefix = (branchpath and branchpath + '/') or branchpath @@ -509,7 +560,7 @@ def branches_in_paths(meta, tbdelta, paths, revnum, checkpath, listdir): # we need to detect those branches. It's a little thorny and slow, but # seems to be the best option. elif paths[p].copyfrom_path and not p.startswith('tags/'): - paths_need_discovery.extend(['%s/%s' % (p,x[0]) + paths_need_discovery.extend(['%s/%s' % (p, x[0]) for x in listdir(p, revnum) if x[1] == 'f']) @@ -539,6 +590,9 @@ def branches_in_paths(meta, tbdelta, paths, revnum, checkpath, listdir): def convert_rev(ui, meta, svn, r, tbdelta, firstrun): # this server fails at replay + if meta.filemap: + raise hgutil.Abort('filemaps currently unsupported with stupid replay.') + branches = branches_in_paths(meta, tbdelta, r.paths, r.revnum, svn.checkpath, svn.list_files) brpaths = branches.values() @@ -659,7 +713,7 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun): meta.authors[r.author], date, extra) - ha = meta.repo.commitctx(current_ctx) + ha = meta.repo.svn_commitctx(current_ctx) if not tag: if (not origbranch in meta.branches diff --git a/hgsubversion/svncommands.py b/hgsubversion/svncommands.py index 529810c..0ae1262 100644 --- a/hgsubversion/svncommands.py +++ b/hgsubversion/svncommands.py @@ -55,7 +55,7 @@ def verify(ui, repo, args=None, **opts): svnfiles.add(fn) fp = fn if branchpath: - fp = branchpath + '/' + fn + fp = branchpath + '/' + fn data, mode = svn.get_file(posixpath.normpath(fp), srev) fctx = ctx[fn] dmatch = fctx.data() == data @@ -95,6 +95,7 @@ def rebuildmeta(ui, repo, args, **opts): if not os.path.exists(svnmetadir): os.makedirs(svnmetadir) + lastpulled = open(os.path.join(svnmetadir, 'lastpulled'), 'wb') revmap = open(os.path.join(svnmetadir, 'rev_map'), 'w') revmap.write('1\n') last_rev = -1 @@ -120,13 +121,18 @@ def rebuildmeta(ui, repo, args, **opts): # it would make us use O(revisions^2) time, so we perform an extra traversal # of the repository instead. During this traversal, we find all converted # changesets that close a branch, and store their first parent + youngest = 0 for rev in repo: util.progress(ui, 'prepare', rev, total=numrevs) ctx = repo[rev] extra = ctx.extra() convinfo = extra.get('convert_revision', None) + if not convinfo: + continue + svnrevnum = int(convinfo.rsplit('@', 1)[1]) + youngest = max(youngest, svnrevnum) - if not convinfo or not extra.get('close', None): + if extra.get('close', None) is None: continue droprev = lambda x: x.rsplit('@', 1)[0] @@ -136,6 +142,7 @@ def rebuildmeta(ui, repo, args, **opts): if droprev(parentinfo) == droprev(convinfo): closed.add(parentctx.rev()) + lastpulled.write(str(youngest) + '\n') util.progress(ui, 'prepare', None, total=numrevs) for rev in repo: @@ -238,7 +245,8 @@ def rebuildmeta(ui, repo, args, **opts): if parentpath.startswith('tags/') and parentextra.get('close'): continue - elif parentpath.startswith('branches/'): branch = parentpath[len('branches/'):] + elif parentpath.startswith('branches/'): + branch = parentpath[len('branches/'):] elif parentpath == 'trunk': branch = None else: diff --git a/hgsubversion/svnexternals.py b/hgsubversion/svnexternals.py index 51a5c9a..38cc40b 100644 --- a/hgsubversion/svnexternals.py +++ b/hgsubversion/svnexternals.py @@ -12,6 +12,14 @@ try: except (ImportError, AttributeError), e: subrepo = None +passpegrev = True # see svnsubrepo below +try: + canonpath = hgutil.canonpath +except (ImportError, AttributeError): + from mercurial import scmutil + canonpath = scmutil.canonpath + passpegrev = False + import util class externalsfile(dict): @@ -51,7 +59,6 @@ class externalsfile(dict): def read(self, data): self.clear() fp = cStringIO.StringIO(data) - dirs = {} target = None for line in fp.readlines(): if not line.strip(): @@ -112,7 +119,7 @@ def parsedefinition(line): revgroup = 2 path, rev, source = m.group(1, 2, 3) try: - nrev = int(rev) + int(rev) # ensure revision is int()able, so we bail otherwise norevline = line[:m.start(revgroup)] + '{REV}' + line[m.end(revgroup):] except (TypeError, ValueError): norevline = line @@ -139,6 +146,9 @@ def parsedefinitions(ui, repo, svnroot, exts): defs = [] for base in sorted(exts): for line in exts[base]: + if not line.strip() or line.lstrip().startswith('#'): + # Ignore comments and blank lines + continue try: path, rev, source, pegrev, norevline = parsedefinition(line) except BadDefinition: @@ -148,7 +158,7 @@ def parsedefinitions(ui, repo, svnroot, exts): if source is None: continue wpath = hgutil.pconvert(os.path.join(base, path)) - wpath = hgutil.canonpath(repo.root, '', wpath) + wpath = canonpath(repo.root, '', wpath) defs.append((wpath, rev, source, pegrev, norevline, base)) # Check target dirs are not nested defs.sort() @@ -221,7 +231,7 @@ class externalsupdater: if rev: revspec = ['-r', rev] if os.path.isdir(path): - exturl, extroot, extrev = getsvninfo(path) + exturl, _extroot, extrev = getsvninfo(path) # Comparing the source paths is not enough, but I don't # know how to compare path+pegrev. The following update # might fail if the path was replaced by another unrelated @@ -336,7 +346,7 @@ def getchanges(ui, repo, parentctx, exts): if exts: defs = parsedefinitions(ui, repo, '', exts) hgsub, hgsubstate = [], [] - for path, rev, source, pegrev, norevline, base in sorted(defs): + for path, rev, _source, _pegrev, norevline, base in sorted(defs): hgsub.append('%s = [hgsubversion] %s:%s\n' % (path, base, norevline)) if rev is None: @@ -402,7 +412,12 @@ if subrepo: svnurl = self._ctx._repo.ui.expandpath('default') svnroot = getsvninfo(util.normalize_url(svnurl))[1] source = resolvesource(self._ui, svnroot, source) - if pegrev is not None: + # hg < 1.9 svnsubrepo calls "svn checkout" with --rev + # only, so peg revisions are correctly used. 1.9 and + # higher, append the rev as a peg revision to the source + # URL, so we cannot add our own. We assume that "-r10 + # url@2" will be similar to "url@10" most of the time. + if pegrev is not None and passpegrev: source = source + '@' + pegrev state = (source, state[1]) # hg-1.7.4-c19b9282d3a7 introduced the overwrite argument diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py index 3c91b0e..68ab4a7 100644 --- a/hgsubversion/svnmeta.py +++ b/hgsubversion/svnmeta.py @@ -257,7 +257,7 @@ class SVNMeta(object): branchpath = branch[3:] else: branchpath = 'branches/%s' % branch - path = '%s/%s' % (subdir , branchpath) + path = '%s/%s' % (subdir, branchpath) extra['convert_revision'] = 'svn:%(uuid)s%(path)s@%(rev)s' % { 'uuid': self.uuid, @@ -368,7 +368,7 @@ class SVNMeta(object): else: path = test.split('/')[-1] test = '/'.join(test.split('/')[:-1]) - ln = self.localname(test) + ln = self.localname(test) if ln and ln.startswith('../'): return None, None, None return path, ln, test @@ -424,7 +424,7 @@ class SVNMeta(object): branch_created_rev = self.branches[branch][2] if parent_branch == 'trunk': parent_branch = None - if branch_created_rev <= number+1 and branch != parent_branch: + if branch_created_rev <= number + 1 and branch != parent_branch: # did the branch exist in previous run if exact and branch in self.prevbranches: if self.prevbranches[branch][1] < real_num: @@ -450,7 +450,7 @@ class SVNMeta(object): return node.hex(self.revmap[tagged]) tag = fromtag # Reference an existing tag - limitedtags = maps.Tags(self.repo, endrev=number-1) + limitedtags = maps.Tags(self.repo, endrev=number - 1) if tag in limitedtags: return limitedtags[tag] r, br = self.get_parent_svn_branch_and_rev(number - 1, branch, exact) @@ -609,7 +609,7 @@ class SVNMeta(object): branchparent = branchparent.parents()[0] branch = self.get_source_rev(ctx=branchparent)[1] - parentctx = self.repo[self.get_parent_revision(rev.revnum+1, branch)] + parentctx = self.repo[self.get_parent_revision(rev.revnum + 1, branch)] if '.hgtags' in parentctx: tagdata = parentctx.filectx('.hgtags').data() else: @@ -639,7 +639,7 @@ class SVNMeta(object): self.authors[rev.author], date, parentctx.extra()) - new_hash = self.repo.commitctx(ctx) + new_hash = self.repo.svn_commitctx(ctx) if not newparent: assert self.revmap[revnum, branch] == parentctx.node() self.revmap[revnum, branch] = new_hash @@ -704,7 +704,7 @@ class SVNMeta(object): self.authors[rev.author], date, extra) - new = self.repo.commitctx(ctx) + new = self.repo.svn_commitctx(ctx) if not fromtag and (rev.revnum, b) not in self.revmap: self.revmap[rev.revnum, b] = new @@ -726,5 +726,5 @@ class SVNMeta(object): self.authors[rev.author], self.fixdate(rev.date), extra) - new = self.repo.commitctx(ctx) + new = self.repo.svn_commitctx(ctx) self.ui.status('Marked branch %s as closed.\n' % (branch or 'default')) diff --git a/hgsubversion/svnrepo.py b/hgsubversion/svnrepo.py index 58ce252..6bb3cd9 100644 --- a/hgsubversion/svnrepo.py +++ b/hgsubversion/svnrepo.py @@ -14,6 +14,8 @@ subclass: pull() is called on the instance pull *to*, but not the one pulled are used to distinguish and filter these operations from others. """ +import errno + from mercurial import error from mercurial import util as hgutil from mercurial import httprepo @@ -26,6 +28,25 @@ import svnmeta propertycache = hgutil.propertycache +class ctxctx(object): + """Proxies a ctx object and ensures files is never empty.""" + def __init__(self, ctx): + self._ctx = ctx + + def files(self): + return self._ctx.files() or ['.svn'] + + def filectx(self, path, filelog=None): + if path == '.svn': + raise IOError(errno.ENOENT, '.svn is a fake file') + return self._ctx.filectx(path, filelog=filelog) + + def __getattr__(self, name): + return getattr(self._ctx, name) + + def __getitem__(self, key): + return self._ctx[key] + def generate_repo_class(ui, repo): """ This function generates the local repository wrapper. """ @@ -53,6 +74,10 @@ def generate_repo_class(ui, repo): return wrapper class svnlocalrepo(superclass): + def svn_commitctx(self, ctx): + """Commits a ctx, but defeats manifest recycling introduced in hg 1.9.""" + return self.commitctx(ctxctx(ctx)) + # TODO use newbranch to allow branch creation in Subversion? @remotesvn def push(self, remote, force=False, revs=None, newbranch=None): @@ -64,7 +89,7 @@ def generate_repo_class(ui, repo): @remotesvn def findoutgoing(self, remote, base=None, heads=None, force=False): - return wrappers.outgoing(repo, remote, heads, force) + return wrappers.findoutgoing(repo, remote, heads, force) def svnmeta(self, uuid=None, subdir=None): return svnmeta.SVNMeta(self, uuid, subdir) diff --git a/hgsubversion/svnwrap/common.py b/hgsubversion/svnwrap/common.py index 9b42b6a..211ff6d 100644 --- a/hgsubversion/svnwrap/common.py +++ b/hgsubversion/svnwrap/common.py @@ -38,7 +38,7 @@ def parse_url(url, user=None, passwd=None): user, passwd = userpass, '' user, passwd = urllib.unquote(user), urllib.unquote(passwd) if user and scheme == 'svn+ssh': - netloc = '@'.join((user, netloc, )) + netloc = '@'.join((user, netloc,)) url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) return (user or None, passwd or None, url) diff --git a/hgsubversion/svnwrap/subvertpy_wrapper.py b/hgsubversion/svnwrap/subvertpy_wrapper.py index ab8cff8..0023d5a 100644 --- a/hgsubversion/svnwrap/subvertpy_wrapper.py +++ b/hgsubversion/svnwrap/subvertpy_wrapper.py @@ -87,7 +87,7 @@ class PathAdapter(object): self.copyfrom_path = intern(self.copyfrom_path) class AbstractEditor(object): - __slots__ = ('editor', ) + __slots__ = ('editor',) def __init__(self, editor): self.editor = editor @@ -131,7 +131,7 @@ class AbstractEditor(object): self.editor.delete_entry(path, revnum, None) class FileEditor(AbstractEditor): - __slots__ = ('path', ) + __slots__ = ('path',) def __init__(self, editor, path): super(FileEditor, self).__init__(editor) @@ -145,7 +145,7 @@ class FileEditor(AbstractEditor): del self.path class DirectoryEditor(AbstractEditor): - __slots__ = ('path', ) + __slots__ = ('path',) def __init__(self, editor, path): super(DirectoryEditor, self).__init__(editor) @@ -306,7 +306,7 @@ class SubversionRepo(object): # ra.get_log(), even with chunk_size set, takes a while # when converting the 65k+ rev. in LLVM. self.remote.get_log(paths=paths, revprops=revprops, - start=start+1, end=stop, limit=chunk_size, + start=start + 1, end=stop, limit=chunk_size, discover_changed_paths=True, callback=callback) except SubversionException, e: @@ -417,9 +417,10 @@ class SubversionRepo(object): try: self.remote.replay(revision, oldestrev, AbstractEditor(editor)) - except SubversionException, e: #pragma: no cover + except (SubversionException, NotImplementedError), e: #pragma: no cover # can I depend on this number being constant? - if (e.args[1] == subvertpy.ERR_RA_NOT_IMPLEMENTED or + if (isinstance(e, NotImplementedError) or + e.args[1] == subvertpy.ERR_RA_NOT_IMPLEMENTED or e.args[1] == subvertpy.ERR_UNSUPPORTED_FEATURE): msg = ('This Subversion server is older than 1.4.0, and ' 'cannot satisfy replay requests.') @@ -476,7 +477,7 @@ class SubversionRepo(object): # File not found raise IOError(errno.ENOENT, e.args[0]) raise - if mode == 'l': + if mode == 'l': linkprefix = "link " if data.startswith(linkprefix): data = data[len(linkprefix):] @@ -530,4 +531,4 @@ class SubversionRepo(object): if not path or path == '.': return self.svn_url assert path[0] != '/', path - return '/'.join((self.svn_url, urllib.quote(path).rstrip('/'), )) + return '/'.join((self.svn_url, urllib.quote(path).rstrip('/'),)) diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py index a6bd906..50ec898 100644 --- a/hgsubversion/svnwrap/svn_swig_wrapper.py +++ b/hgsubversion/svnwrap/svn_swig_wrapper.py @@ -293,7 +293,7 @@ class SubversionRepo(object): # when converting the 65k+ rev. in LLVM. ra.get_log(self.ra, paths, - start+1, + start + 1, stop, chunk_size, #limit of how many log messages to load True, # don't need to know changed paths @@ -497,7 +497,7 @@ class SubversionRepo(object): if e.args[1] in notfound: # File not found raise IOError(errno.ENOENT, e.args[0]) raise - if mode == 'l': + if mode == 'l': linkprefix = "link " if data.startswith(linkprefix): data = data[len(linkprefix):] diff --git a/hgsubversion/util.py b/hgsubversion/util.py index c0dfd1b..f55893e 100644 --- a/hgsubversion/util.py +++ b/hgsubversion/util.py @@ -35,7 +35,7 @@ def filterdiff(diff, oldrev, newrev): diff) oldrev = formatrev(oldrev) newrev = formatrev(newrev) - diff = a_re.sub(r'--- \1'+ oldrev, diff) + diff = a_re.sub(r'--- \1' + oldrev, diff) diff = b_re.sub(r'+++ \1' + newrev, diff) diff = devnull_re.sub(r'\1 /dev/null\t(working copy)', diff) diff = header_re.sub(r'Index: \1' + '\n' + ('=' * 67), diff) @@ -53,16 +53,21 @@ def parentrev(ui, repo, meta, hashes): def islocalrepo(url): - if not url.startswith('file:///'): + path = str(url) # convert once up front + if path.startswith('file:///'): + prefixlen = len('file://') + elif path.startswith('file:/'): + prefixlen = len('file:') + else: return False - if '#' in url.split('/')[-1]: # strip off #anchor - url = url[:url.rfind('#')] - path = url[len('file://'):] + if '#' in path.split('/')[-1]: # strip off #anchor + path = path[:path.rfind('#')] + path = url[prefixlen:] path = urllib.url2pathname(path).replace(os.sep, '/') while '/' in path: - if reduce(lambda x,y: x and y, + if reduce(lambda x, y: x and y, map(lambda p: os.path.exists(os.path.join(path, p)), - ('hooks', 'format', 'db', ))): + ('hooks', 'format', 'db',))): return True path = path.rsplit('/', 1)[0] return False @@ -94,6 +99,29 @@ def normalize_url(url): url = '%s#%s' % (url, checkout) return url + +def load_string(file_path, default=None, limit=1024): + if not os.path.exists(file_path): + return default + try: + f = open(file_path, 'r') + ret = f.read(limit) + f.close() + except: + return default + if ret == '': + return default + return ret + + +def save_string(file_path, string): + if string is None: + string = "" + f = open(file_path, 'wb') + f.write(str(string)) + f.close() + + # TODO remove when we drop 1.3 support def progress(ui, *args, **kwargs): if getattr(ui, 'progress', False): @@ -149,6 +177,24 @@ def outgoing_revisions(repo, reverse_map, sourcerev): if sourcerev.node() != node.nullid: return outgoing_rev_hashes +def outgoing_common_and_heads(repo, reverse_map, sourcerev): + """Given a repo and an hg_editor, determines outgoing revisions for the + current working copy state. Returns a tuple (common, heads) like + discovery.findcommonoutgoing does. + """ + if sourcerev in reverse_map: + return ([sourcerev], [sourcerev]) # nothing outgoing + sourcecx = repo[sourcerev] + while (not sourcecx.node() in reverse_map + and sourcecx.node() != node.nullid): + ps = sourcecx.parents() + if len(ps) != 1: + raise hgutil.Abort("Sorry, can't find svn parent of a merge revision.") + sourcecx = ps[0] + if sourcecx.node() != node.nullid: + return ([sourcecx.node()], [sourcerev]) + return ([sourcerev], [sourcerev]) # nothing outgoing + def default_commit_msg(ui): return ui.config('hgsubversion', 'defaultmessage', '') @@ -203,10 +249,22 @@ def _templatehelper(ctx, kw): else: raise hgutil.Abort('unrecognized hgsubversion keyword %s' % kw) +def svnrevkw(**args): + """:svnrev: String. Converted subversion revision number.""" + return _templatehelper(args['ctx'], 'svnrev') + +def svnpathkw(**args): + """:svnpath: String. Converted subversion revision project path.""" + return _templatehelper(args['ctx'], 'svnpath') + +def svnuuidkw(**args): + """:svnuuid: String. Converted subversion revision repository identifier.""" + return _templatehelper(args['ctx'], 'svnuuid') + templatekeywords = { - 'svnrev': (lambda repo, ctx, templ, **a: _templatehelper(ctx, 'svnrev')), - 'svnpath': (lambda repo, ctx, templ, **a: _templatehelper(ctx, 'svnpath')), - 'svnuuid': (lambda repo, ctx, templ, **a: _templatehelper(ctx, 'svnuuid')), + 'svnrev': svnrevkw, + 'svnpath': svnpathkw, + 'svnuuid': svnuuidkw, } def revset_fromsvn(repo, subset, x): diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py index c698447..3c16b31 100644 --- a/hgsubversion/wrappers.py +++ b/hgsubversion/wrappers.py @@ -15,6 +15,12 @@ import svnwrap import svnrepo import util +try: + from mercurial import scmutil + revpair = scmutil.revpair +except ImportError: + revpair = cmdutil.revpair + pullfuns = { True: replay.convert_rev, False: stupidmod.convert_rev, @@ -77,13 +83,22 @@ def incoming(orig, ui, repo, origsource='default', **opts): ui.status('%s%s\n' % (l1.ljust(13), val)) -def outgoing(repo, dest=None, heads=None, force=False): +def findcommonoutgoing(repo, other, onlyheads=None, force=False, commoninc=None): + assert other.capable('subversion') + # split off #rev; TODO implement --revision/#rev support + svn = other.svn + meta = repo.svnmeta(svn.uuid, svn.subdir) + parent = repo.parents()[0].node() + hashes = meta.revmap.hashes() + return util.outgoing_common_and_heads(repo, hashes, parent) + + +def findoutgoing(repo, dest=None, heads=None, force=False): """show changesets not found in the Subversion repository """ assert dest.capable('subversion') - # split off #rev; TODO implement --revision/#rev support - svnurl, revs, checkout = util.parseurl(dest.svnurl, heads) + #svnurl, revs, checkout = util.parseurl(dest.svnurl, heads) svn = dest.svn meta = repo.svnmeta(svn.uuid, svn.subdir) parent = repo.parents()[0].node() @@ -104,7 +119,7 @@ def diff(orig, ui, repo, *args, **opts): if o_r: parent = repo[o_r[-1]].parents()[0] opts['rev'] = ['%s:.' % node.hex(parent.node()), ] - node1, node2 = cmdutil.revpair(repo, opts['rev']) + node1, node2 = revpair(repo, opts['rev']) baserev, _junk = hashes.get(node1, (-1, 'junk')) newrev, _junk = hashes.get(node2, (-1, 'junk')) it = patch.diff(repo, node1, node2, @@ -122,7 +137,11 @@ def push(repo, dest, force, revs): """push revisions starting at a specified head back to Subversion. """ assert not revs, 'designated revisions for push remains unimplemented.' - cmdutil.bail_if_changed(repo) + if hasattr(cmdutil, 'bail_if_changed'): + cmdutil.bail_if_changed(repo) + else: + # Since 1.9 (d68ddccf276b) + cmdutil.bailifchanged(repo) checkpush = getattr(repo, 'checkpush', None) if checkpush: checkpush(force, revs) @@ -291,11 +310,13 @@ def pull(repo, source, heads=[], force=False): total = stopat_rev - start else: total = svn.HEAD - start + lastpulled = None try: try: # start converting revisions firstrun = True for r in svn.revisions(start=start, stop=stopat_rev): + lastpulled = r.revnum if (r.author is None and r.message == 'This is an empty revision for padding.'): continue @@ -352,6 +373,8 @@ def pull(repo, source, heads=[], force=False): util.progress(ui, 'pull', None, total=total) util.swap_out_encoding(old_encoding) + if lastpulled is not None: + meta.revmap.youngest = lastpulled revisions = len(meta.revmap) - oldrevisions if revisions == 0: @@ -431,7 +454,13 @@ def clone(orig, ui, source, dest=None, **opts): """ data = {} - def hgclonewrapper(orig, ui, origsource, dest, **opts): + def hgclonewrapper(orig, ui, *args, **opts): + if getattr(hg, 'peer', None): + # Since 1.9 (d976542986d2) + origsource = args[1] + else: + origsource = args[0] + if isinstance(origsource, str): source, branch, checkout = util.parseurl(ui.expandpath(origsource), opts.get('branch')) @@ -445,7 +474,7 @@ def clone(orig, ui, source, dest=None, **opts): data['branches'] = branches ui.setconfig('hgsubversion', 'branch', branches[-1]) - data['srcrepo'], data['dstrepo'] = orig(ui, origsource, dest, **opts) + data['srcrepo'], data['dstrepo'] = orig(ui, *args, **opts) for opt, (section, name) in optionmap.iteritems(): if opt in opts and opts[opt]: |