diff --git a/hgext/convert/subversion.py b/hgext/convert/subversion.py --- a/hgext/convert/subversion.py +++ b/hgext/convert/subversion.py @@ -185,6 +185,8 @@ class svn_source(converter_source): self.ra = self.transport.ra self.ctx = self.transport.client self.base = svn.ra.get_repos_root(self.ra) + # Module is either empty or a repository path starting with + # a slash and not ending with a slash. self.module = self.url[len(self.base):] self.rootmodule = self.module self.commits = {} @@ -535,24 +537,6 @@ class svn_source(converter_source): svn.ra.reparent(self.ra, svn_url.encode(self.encoding)) def expandpaths(self, rev, paths, parents): - def get_entry_from_path(path, module=self.module): - # Given the repository url of this wc, say - # "http://server/plone/CMFPlone/branches/Plone-2_0-branch" - # extract the "entry" portion (a relative path) from what - # svn log --xml says, ie - # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py" - # that is to say "tests/PloneTestCase.py" - if path.startswith(module): - relative = path[len(module):] - if relative.startswith('/'): - return relative[1:] - else: - return relative - - # The path is outside our tracked tree... - self.ui.debug('%r is not under %r, ignoring\n' % (path, module)) - return None - entries = [] copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions. copies = {} @@ -563,24 +547,25 @@ class svn_source(converter_source): self.reparent(self.module) for path, ent in paths: - entrypath = get_entry_from_path(path, module=self.module) + entrypath = self.getrelpath(path) entry = entrypath.decode(self.encoding) kind = svn.ra.check_path(self.ra, entrypath, revnum) if kind == svn.core.svn_node_file: - if ent.copyfrom_path: - copyfrom_path = get_entry_from_path(ent.copyfrom_path) - if copyfrom_path: - self.ui.debug("Copied to %s from %s@%s\n" % - (entrypath, copyfrom_path, - ent.copyfrom_rev)) - # It's probably important for hg that the source - # exists in the revision's parent, not just the - # ent.copyfrom_rev - fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev) - if fromkind != 0: - copies[self.recode(entry)] = self.recode(copyfrom_path) entries.append(self.recode(entry)) + if not ent.copyfrom_path or not parents: + continue + # Copy sources not in parent revisions cannot be represented, + # ignore their origin for now + pmodule, prevnum = self.revsplit(parents[0])[1:] + if ent.copyfrom_rev < prevnum: + continue + copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule) + if not copyfrom_path: + continue + self.ui.debug("copied to %s from %s@%s\n" % + (entrypath, copyfrom_path, ent.copyfrom_rev)) + copies[self.recode(entry)] = self.recode(copyfrom_path) elif kind == 0: # gone, but had better be a deleted *file* self.ui.debug("gone from %s\n" % ent.copyfrom_rev) @@ -590,8 +575,8 @@ class svn_source(converter_source): # a root revision. uuid, old_module, fromrev = self.revsplit(parents[0]) - basepath = old_module + "/" + get_entry_from_path(path, module=self.module) - entrypath = old_module + "/" + get_entry_from_path(path, module=self.module) + basepath = old_module + "/" + self.getrelpath(path) + entrypath = basepath def lookup_parts(p): rc = None @@ -647,7 +632,7 @@ class svn_source(converter_source): # parent in the same commit? (probably can). Could # cause problems if instead of revnum -1, # we have to look in (copyfrom_path, revnum - 1) - entrypath = get_entry_from_path("/" + child, module=old_module) + entrypath = self.getrelpath("/" + child, module=old_module) if entrypath: entry = self.recode(entrypath.decode(self.encoding)) if entry in copies: @@ -680,7 +665,7 @@ class svn_source(converter_source): # parent in the same commit? (probably can). Could # cause problems if instead of revnum -1, # we have to look in (copyfrom_path, revnum - 1) - entrypath = get_entry_from_path("/" + child, module=self.module) + entrypath = self.getrelpath("/" + child) # print child, self.module, entrypath if entrypath: # Need to filter out directories here... @@ -691,39 +676,30 @@ class svn_source(converter_source): # Copies here (must copy all from source) # Probably not a real problem for us if # source does not exist - - # Can do this with the copy command "hg copy" - # if ent.copyfrom_path: - # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding), - # module=self.module) - # copyto_entry = entrypath - # - # print "copy directory", copyfrom_entry, 'to', copyto_entry - # - # copies.append((copyfrom_entry, copyto_entry)) - - if ent.copyfrom_path: - copyfrom_path = ent.copyfrom_path.decode(self.encoding) - copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module) - if copyfrom_entry: - copyfrom[path] = ent - self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path])) - - # Good, /probably/ a regular copy. Really should check - # to see whether the parent revision actually contains - # the directory in question. - children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev) - children.sort() - for child in children: - entrypath = get_entry_from_path("/" + child, module=self.module) - if entrypath: - entry = entrypath.decode(self.encoding) - # print "COPY COPY From", copyfrom_entry, entry - copyto_path = path + entry[len(copyfrom_entry):] - copyto_entry = get_entry_from_path(copyto_path, module=self.module) - # print "COPY", entry, "COPY To", copyto_entry - copies[self.recode(copyto_entry)] = self.recode(entry) - # copy from quux splort/quuxfile + if not ent.copyfrom_path or not parents: + continue + # Copy sources not in parent revisions cannot be represented, + # ignore their origin for now + pmodule, prevnum = self.revsplit(parents[0])[1:] + if ent.copyfrom_rev < prevnum: + continue + copyfrompath = ent.copyfrom_path.decode(self.encoding) + copyfrompath = self.getrelpath(copyfrompath, pmodule) + if not copyfrompath: + continue + copyfrom[path] = ent + self.ui.debug("mark %s came from %s:%d\n" + % (path, copyfrompath, ent.copyfrom_rev)) + children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev) + children.sort() + for child in children: + entrypath = self.getrelpath("/" + child, pmodule) + if not entrypath: + continue + entry = entrypath.decode(self.encoding) + copytopath = path + entry[len(copyfrompath):] + copytopath = self.getrelpath(copytopath) + copies[self.recode(copytopath)] = self.recode(entry, pmodule) return (util.unique(entries), copies) @@ -732,6 +708,13 @@ class svn_source(converter_source): from_revnum, to_revnum = to_revnum, from_revnum self.child_cset = None + + def isdescendantof(parent, child): + if not child or not parent or not child.startswith(parent): + return False + subpath = child[len(parent):] + return len(subpath) > 1 and subpath[0] == '/' + def parselogentry(orig_paths, revnum, author, date, message): """Return the parsed commit object or None, and True if the revision is a branch root. @@ -755,10 +738,21 @@ class svn_source(converter_source): if root_paths: path, ent = root_paths[-1] if ent.copyfrom_path: + # If dir was moved while one of its file was removed + # the log may look like: + # A /dir (from /dir:x) + # A /dir/a (from /dir/a:y) + # A /dir/b (from /dir/b:z) + # ... + # for all remaining children. + # Let's take the highest child element from rev as source. + copies = [(p,e) for p,e in orig_paths[:-1] + if isdescendantof(ent.copyfrom_path, e.copyfrom_path)] + fromrev = max([e.copyfrom_rev for p,e in copies] + [ent.copyfrom_rev]) branched = True newpath = ent.copyfrom_path + self.module[len(path):] # ent.copyfrom_rev may not be the actual last revision - previd = self.latest(newpath, ent.copyfrom_rev) + previd = self.latest(newpath, fromrev) if previd is not None: prevmodule, prevnum = self.revsplit(previd)[1:] if prevnum >= self.startrev: @@ -771,8 +765,7 @@ class svn_source(converter_source): paths = [] # filter out unrelated paths for path, ent in orig_paths: - if not path.startswith(self.module): - self.ui.debug("boring@%s: %s\n" % (revnum, path)) + if self.getrelpath(path) is None: continue paths.append((path, ent)) @@ -885,6 +878,26 @@ class svn_source(converter_source): rpath = '/'.join([self.base, path]).strip('/') return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()] + def getrelpath(self, path, module=None): + if module is None: + module = self.module + # Given the repository url of this wc, say + # "http://server/plone/CMFPlone/branches/Plone-2_0-branch" + # extract the "entry" portion (a relative path) from what + # svn log --xml says, ie + # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py" + # that is to say "tests/PloneTestCase.py" + if path.startswith(module): + relative = path.rstrip('/')[len(module):] + if relative.startswith('/'): + return relative[1:] + elif relative == '': + return relative + + # The path is outside our tracked tree... + self.ui.debug('%r is not under %r, ignoring\n' % (path, module)) + return None + pre_revprop_change = '''#!/bin/sh REPOS="$1" diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -1018,11 +1018,16 @@ def walkchangerevs(ui, repo, pats, chang minrev, maxrev = min(revs), max(revs) for file_, node in iterfiles(): filelog = repo.file(file_) - # A zero count may be a directory or deleted file, so - # try to find matching entries on the slow path. if filelog.count() == 0: - slowpath = True - break + if node is None: + # A zero count may be a directory or deleted file, so + # try to find matching entries on the slow path. + slowpath = True + break + else: + ui.warn(_('%s:%s copy source revision cannot be found!\n') + % (file_, short(node))) + continue for rev, copied in filerevgen(filelog, node): if rev <= maxrev: if rev < minrev: diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -712,17 +712,17 @@ class localrepository(repo.repository): # meta["copy"] = cp if not manifest2: # not a branch merge - meta["copyrev"] = hex(manifest1.get(cp, nullid)) + meta["copyrev"] = hex(manifest1[cp]) fp2 = nullid elif fp2 != nullid: # copied on remote side - meta["copyrev"] = hex(manifest1.get(cp, nullid)) + meta["copyrev"] = hex(manifest1[cp]) elif fp1 != nullid: # copied on local side, reversed - meta["copyrev"] = hex(manifest2.get(cp)) + meta["copyrev"] = hex(manifest2[cp]) fp2 = fp1 elif cp in manifest2: # directory rename on local side meta["copyrev"] = hex(manifest2[cp]) else: # directory rename on remote side - meta["copyrev"] = hex(manifest1.get(cp, nullid)) + meta["copyrev"] = hex(manifest1[cp]) self.ui.debug(_(" %s: copy %s:%s\n") % (fn, cp, meta["copyrev"])) fp1 = nullid diff --git a/mercurial/verify.py b/mercurial/verify.py --- a/mercurial/verify.py +++ b/mercurial/verify.py @@ -245,7 +245,14 @@ def _verify(repo): rp = fl.renamed(n) if rp: fl2 = repo.file(rp[0]) - rev = fl2.rev(rp[1]) + if fl2.count() == 0: + err(flr, _("empty or missing copy source revlog %s:%s") + % (rp[0], short(rp[1])), f) + elif rp[1] == nullid: + err(flr, _("copy source revision is nullid %s:%s") + % (rp[0], short(rp[1])), f) + else: + rev = fl2.rev(rp[1]) except KeyboardInterrupt: repo.ui.warn(_("interrupted")) raise diff --git a/tests/test-convert-svn-branches b/tests/test-convert-svn-branches --- a/tests/test-convert-svn-branches +++ b/tests/test-convert-svn-branches @@ -35,41 +35,48 @@ svn import -m "init projA" projA $svnurl echo % update svn repository svn co $svnurl A | fix_path cd A -echo hello > trunk/letter.txt -echo hey > trunk/letter2.txt -echo ho > trunk/letter3.txt +echo a > trunk/a +echo b > trunk/b +echo c > trunk/c # Add a file within branches, used to confuse branch detection -echo a > branches/readme.txt -svn add trunk/letter.txt trunk/letter2.txt trunk/letter3.txt branches/readme.txt +echo d > branches/notinbranch +svn add trunk/a trunk/b trunk/c branches/notinbranch svn ci -m hello -echo % branch to old letters +echo % branch to old svn copy trunk branches/old -svn rm branches/old/letter3.txt -svn ci -m "branch trunk, remove letter3" +svn rm branches/old/c +svn ci -m "branch trunk, remove c" svn up echo % update trunk -"$TESTDIR/svn-safe-append.py" "what can I say ?" trunk/letter.txt -svn ci -m "change letter" +"$TESTDIR/svn-safe-append.py" a trunk/a +svn ci -m "change a" echo % update old branch -"$TESTDIR/svn-safe-append.py" "what's up ?" branches/old/letter2.txt -svn ci -m "change letter2" +"$TESTDIR/svn-safe-append.py" b branches/old/b +svn ci -m "change b" echo % create a cross-branch revision -svn move -m "move letter2" trunk/letter2.txt \ - branches/old/letter3.txt -"$TESTDIR/svn-safe-append.py" "I am fine" branches/old/letter3.txt -svn ci -m "move and update letter3.txt" +svn move -m "move b" trunk/b branches/old/c +"$TESTDIR/svn-safe-append.py" c branches/old/c +svn ci -m "move and update c" echo % update old branch again -"$TESTDIR/svn-safe-append.py" "bye" branches/old/letter2.txt -svn ci -m "change letter2 again" +"$TESTDIR/svn-safe-append.py" b branches/old/b +svn ci -m "change b again" + +echo % move back and forth between branch of similar names +# This used to generate fake copy records +svn up +svn move branches/old branches/old2 +svn ci -m "move to old2" +svn move branches/old2 branches/old +svn ci -m "move back to old" echo % update trunk again -"$TESTDIR/svn-safe-append.py" "how are you ?" trunk/letter.txt -svn ci -m "last change to letter" +"$TESTDIR/svn-safe-append.py" a trunk/a +svn ci -m "last change to a" cd .. echo % convert trunk and branches @@ -77,15 +84,15 @@ hg convert --datesort $svnurl A-hg echo % branch again from a converted revision cd A -svn copy -r 1 $svnurl/trunk branches/old2 -svn ci -m "branch trunk@1 into old2" +svn copy -r 1 $svnurl/trunk branches/old3 +svn ci -m "branch trunk@1 into old3" cd .. echo % convert again hg convert --datesort $svnurl A-hg cd A-hg -hg glog --template '#rev# #desc|firstline# files: #files#\n' +hg glog --template 'branch=#branches# #rev# #desc|firstline# files: #files#\n' hg branches | sed 's/:.*/:/' hg tags -q cd .. diff --git a/tests/test-convert-svn-branches.out b/tests/test-convert-svn-branches.out --- a/tests/test-convert-svn-branches.out +++ b/tests/test-convert-svn-branches.out @@ -9,95 +9,122 @@ A A/trunk A A/branches A A/tags Checked out revision 1. -A trunk/letter.txt -A trunk/letter2.txt -A trunk/letter3.txt -A branches/readme.txt -Adding branches/readme.txt -Adding trunk/letter.txt -Adding trunk/letter2.txt -Adding trunk/letter3.txt +A trunk/a +A trunk/b +A trunk/c +A branches/notinbranch +Adding branches/notinbranch +Adding trunk/a +Adding trunk/b +Adding trunk/c Transmitting file data .... Committed revision 2. -% branch to old letters +% branch to old A branches/old -D branches/old/letter3.txt +D branches/old/c Adding branches/old -Adding branches/old/letter.txt -Adding branches/old/letter2.txt -Deleting branches/old/letter3.txt +Adding branches/old/a +Adding branches/old/b +Deleting branches/old/c Committed revision 3. At revision 3. % update trunk -Sending trunk/letter.txt +Sending trunk/a Transmitting file data . Committed revision 4. % update old branch -Sending branches/old/letter2.txt +Sending branches/old/b Transmitting file data . Committed revision 5. % create a cross-branch revision -A branches/old/letter3.txt -D trunk/letter2.txt -Adding branches/old/letter3.txt -Deleting trunk/letter2.txt +A branches/old/c +D trunk/b +Adding branches/old/c +Deleting trunk/b Transmitting file data . Committed revision 6. % update old branch again -Sending branches/old/letter2.txt +Sending branches/old/b Transmitting file data . Committed revision 7. +% move back and forth between branch of similar names +At revision 7. +A branches/old2 +D branches/old/a +D branches/old/b +D branches/old/c +D branches/old +Deleting branches/old +Adding branches/old2 + +Committed revision 8. +A branches/old +D branches/old2/a +D branches/old2/b +D branches/old2/c +D branches/old2 +Adding branches/old +Deleting branches/old2 + +Committed revision 9. % update trunk again -Sending trunk/letter.txt +Sending trunk/a Transmitting file data . -Committed revision 8. +Committed revision 10. % convert trunk and branches initializing destination A-hg repository scanning source... sorting... converting... -8 init projA -7 hello -6 branch trunk, remove letter3 -5 change letter -4 change letter2 -3 move and update letter3.txt -2 move and update letter3.txt -1 change letter2 again -0 last change to letter +10 init projA +9 hello +8 branch trunk, remove c +7 change a +6 change b +5 move and update c +4 move and update c +3 change b again +2 move to old2 +1 move back to old +0 last change to a % branch again from a converted revision Checked out revision 1. -A branches/old2 -Adding branches/old2 +A branches/old3 +Adding branches/old3 -Committed revision 9. +Committed revision 11. % convert again scanning source... sorting... converting... -0 branch trunk@1 into old2 -o 9 branch trunk@1 into old2 files: +0 branch trunk@1 into old3 +o branch=old3 11 branch trunk@1 into old3 files: | -| o 8 last change to letter files: letter.txt +| o branch= 10 last change to a files: a | | -| | o 7 change letter2 again files: letter2.txt +| | o branch=old 9 move back to old files: | | | -| o | 6 move and update letter3.txt files: letter2.txt +| | o branch=old2 8 move to old2 files: | | | -| | o 5 move and update letter3.txt files: letter3.txt +| | o branch=old 7 change b again files: b | | | -| | o 4 change letter2 files: letter2.txt +| o | branch= 6 move and update c files: b +| | | +| | o branch=old 5 move and update c files: c | | | -| o | 3 change letter files: letter.txt +| | o branch=old 4 change b files: b +| | | +| o | branch= 3 change a files: a | | | -+---o 2 branch trunk, remove letter3 files: letter.txt letter2.txt -| | -| o 1 hello files: letter.txt letter2.txt letter3.txt +| | o branch=old 2 branch trunk, remove c files: a b +| |/ +| o branch= 1 hello files: a b c |/ -o 0 init projA files: +o branch= 0 init projA files: -old2 9: -default 8: -old 7: +old3 11: +default 10: +old 9: +old2 8: tip diff --git a/tests/test-convert-svn-move b/tests/test-convert-svn-move --- a/tests/test-convert-svn-move +++ b/tests/test-convert-svn-move @@ -27,8 +27,10 @@ cd projA mkdir trunk echo a > trunk/a mkdir trunk/d1 +mkdir trunk/d2 echo b > trunk/d1/b echo c > trunk/d1/c +echo d > trunk/d2/d cd .. svnurl=file://$svnpath/svn-repo/projA @@ -50,10 +52,16 @@ mkdir subproject/branches svn add subproject/branches svn ci -m createbranches svn mv $svnurl/subproject/d1 $svnurl/subproject/trunk/d1 -m moved1 +svn mv $svnurl/subproject/d2 $svnurl/subproject/trunk/d2 -m moved2 svn up "$TESTDIR/svn-safe-append.py" b subproject/trunk/d1/b -svn ci -m changeb +svn rm subproject/trunk/d2 +svn ci -m "changeb and rm d2" svn mv $svnurl/subproject/trunk/d1 $svnurl/subproject/branches/d1 -m moved1again +echo % copy a file from a past revision +svn copy -r 7 $svnurl/subproject/trunk/d2/d $svnurl/subproject/trunk -m copyfilefrompast +echo % copy a directory from a past revision +svn copy -r 7 $svnurl/subproject/trunk/d2 $svnurl/subproject/trunk -m copydirfrompast cd .. echo % convert trunk and branches diff --git a/tests/test-convert-svn-move.out b/tests/test-convert-svn-move.out --- a/tests/test-convert-svn-move.out +++ b/tests/test-convert-svn-move.out @@ -4,6 +4,8 @@ Adding projA/trunk/a Adding projA/trunk/d1 Adding projA/trunk/d1/b Adding projA/trunk/d1/c +Adding projA/trunk/d2 +Adding projA/trunk/d2/d Committed revision 1. % update svn repository @@ -12,6 +14,8 @@ A A/trunk/a A A/trunk/d1 A A/trunk/d1/b A A/trunk/d1/c +A A/trunk/d2 +A A/trunk/d2/d Checked out revision 1. Sending trunk/a Sending trunk/d1/c @@ -25,6 +29,8 @@ A subproject/a A subproject/d1 A subproject/d1/b A subproject/d1/c +A subproject/d2 +A subproject/d2/d Updated to revision 3. A subproject/trunk Adding subproject/trunk @@ -36,35 +42,58 @@ Adding subproject/branches Committed revision 5. Committed revision 6. + +Committed revision 7. A subproject/trunk/d1 A subproject/trunk/d1/b A subproject/trunk/d1/c +A subproject/trunk/d2 +A subproject/trunk/d2/d D subproject/d1 -Updated to revision 6. +D subproject/d2 +Updated to revision 7. +D subproject/trunk/d2/d +D subproject/trunk/d2 Sending subproject/trunk/d1/b +Deleting subproject/trunk/d2 Transmitting file data . -Committed revision 7. +Committed revision 8. + +Committed revision 9. +% copy a file from a past revision -Committed revision 8. +Committed revision 10. +% copy a directory from a past revision + +Committed revision 11. % convert trunk and branches initializing destination A-hg repository scanning source... sorting... converting... -6 createtrunk -5 moved1 -4 moved1 -3 changeb -2 changeb -1 moved1again -0 moved1again -o 6 moved1again files: d1/b d1/c +9 createtrunk +8 moved1 +7 moved1 +6 moved2 +5 changeb and rm d2 +4 changeb and rm d2 +3 moved1again +2 moved1again +1 copyfilefrompast +0 copydirfrompast +o 9 copydirfrompast files: d2/d | -| o 5 moved1again files: +o 8 copyfilefrompast files: d +| +o 7 moved1again files: d1/b d1/c +| +| o 6 moved1again files: | | -o | 4 changeb files: d1/b +o | 5 changeb and rm d2 files: d1/b d2/d | | -| o 3 changeb files: b +| o 4 changeb and rm d2 files: b +| | +o | 3 moved2 files: d2/d | | o | 2 moved1 files: d1/b d1/c | | @@ -72,5 +101,5 @@ o | 2 moved1 files: d1/b d1/c | o 0 createtrunk files: -default 6: -d1 5: +default 9: +d1 6: