##// END OF EJS Templates
convert: tell the source repository when a rev has been converted...
Bryan O'Sullivan -
r5554:2147a734 default
parent child Browse files
Show More
@@ -0,0 +1,66 b''
1 #!/bin/sh
2
3 "$TESTDIR/hghave" svn svn-bindings || exit 80
4
5 fix_path()
6 {
7 tr '\\' /
8 }
9
10 echo "[extensions]" >> $HGRCPATH
11 echo "convert = " >> $HGRCPATH
12
13 svnpath=`pwd`/svn-repo
14 svnadmin create $svnpath
15
16 cat > $svnpath/hooks/pre-revprop-change <<'EOF'
17 #!/bin/sh
18
19 REPOS="$1"
20 REV="$2"
21 USER="$3"
22 PROPNAME="$4"
23 ACTION="$5"
24
25 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
26 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
27 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
28
29 echo "Changing prohibited revision property" >&2
30 exit 1
31 EOF
32 chmod +x $svnpath/hooks/pre-revprop-change
33
34 svnurl=file://$svnpath
35 svn co $svnurl $svnpath-wc
36
37 cd $svnpath-wc
38 echo a > a
39 svn add a
40 svn ci -m'added a' a
41
42 cd ..
43
44 echo % initial roundtrip
45 hg convert -s svn -d hg $svnpath-wc $svnpath-hg | grep -v initializing
46 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
47
48 echo % second roundtrip should do nothing
49 hg convert -s svn -d hg $svnpath-wc $svnpath-hg
50 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
51
52 echo % new hg rev
53
54 hg clone $svnpath-hg $svnpath-work
55 echo b > $svnpath-work/b
56 hg --cwd $svnpath-work add b
57 hg --cwd $svnpath-work ci -mb
58
59 echo % echo hg to svn
60 hg --cwd $svnpath-hg pull -q $svnpath-work
61 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
62
63 echo % svn back to hg should do nothing
64 hg convert -s svn -d hg $svnpath-wc $svnpath-hg
65 echo % hg back to svn should do nothing
66 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
@@ -0,0 +1,35 b''
1 Checked out revision 0.
2 A a
3 Adding a
4 Transmitting file data .
5 Committed revision 1.
6 % initial roundtrip
7 scanning source...
8 sorting...
9 converting...
10 0 added a
11 scanning source...
12 sorting...
13 converting...
14 % second roundtrip should do nothing
15 scanning source...
16 sorting...
17 converting...
18 scanning source...
19 sorting...
20 converting...
21 % new hg rev
22 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
23 % echo hg to svn
24 scanning source...
25 sorting...
26 converting...
27 0 b
28 % svn back to hg should do nothing
29 scanning source...
30 sorting...
31 converting...
32 % hg back to svn should do nothing
33 scanning source...
34 sorting...
35 converting...
@@ -1,382 +1,383 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile
8 from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from darcs import darcs_source
10 from darcs import darcs_source
11 from git import convert_git
11 from git import convert_git
12 from hg import mercurial_source, mercurial_sink
12 from hg import mercurial_source, mercurial_sink
13 from subversion import debugsvnlog, svn_source, svn_sink
13 from subversion import debugsvnlog, svn_source, svn_sink
14 import filemap
14 import filemap
15
15
16 import os, shutil
16 import os, shutil
17 from mercurial import hg, ui, util, commands
17 from mercurial import hg, ui, util, commands
18 from mercurial.i18n import _
18 from mercurial.i18n import _
19
19
20 commands.norepo += " convert debugsvnlog"
20 commands.norepo += " convert debugsvnlog"
21
21
22 source_converters = [
22 source_converters = [
23 ('cvs', convert_cvs),
23 ('cvs', convert_cvs),
24 ('git', convert_git),
24 ('git', convert_git),
25 ('svn', svn_source),
25 ('svn', svn_source),
26 ('hg', mercurial_source),
26 ('hg', mercurial_source),
27 ('darcs', darcs_source),
27 ('darcs', darcs_source),
28 ]
28 ]
29
29
30 sink_converters = [
30 sink_converters = [
31 ('hg', mercurial_sink),
31 ('hg', mercurial_sink),
32 ('svn', svn_sink),
32 ('svn', svn_sink),
33 ]
33 ]
34
34
35 def convertsource(ui, path, type, rev):
35 def convertsource(ui, path, type, rev):
36 exceptions = []
36 exceptions = []
37 for name, source in source_converters:
37 for name, source in source_converters:
38 try:
38 try:
39 if not type or name == type:
39 if not type or name == type:
40 return source(ui, path, rev)
40 return source(ui, path, rev)
41 except NoRepo, inst:
41 except NoRepo, inst:
42 exceptions.append(inst)
42 exceptions.append(inst)
43 if not ui.quiet:
43 if not ui.quiet:
44 for inst in exceptions:
44 for inst in exceptions:
45 ui.write(_("%s\n") % inst)
45 ui.write(_("%s\n") % inst)
46 raise util.Abort('%s: unknown repository type' % path)
46 raise util.Abort('%s: unknown repository type' % path)
47
47
48 def convertsink(ui, path, type):
48 def convertsink(ui, path, type):
49 for name, sink in sink_converters:
49 for name, sink in sink_converters:
50 try:
50 try:
51 if not type or name == type:
51 if not type or name == type:
52 return sink(ui, path)
52 return sink(ui, path)
53 except NoRepo, inst:
53 except NoRepo, inst:
54 ui.note(_("convert: %s\n") % inst)
54 ui.note(_("convert: %s\n") % inst)
55 raise util.Abort('%s: unknown repository type' % path)
55 raise util.Abort('%s: unknown repository type' % path)
56
56
57 class converter(object):
57 class converter(object):
58 def __init__(self, ui, source, dest, revmapfile, opts):
58 def __init__(self, ui, source, dest, revmapfile, opts):
59
59
60 self.source = source
60 self.source = source
61 self.dest = dest
61 self.dest = dest
62 self.ui = ui
62 self.ui = ui
63 self.opts = opts
63 self.opts = opts
64 self.commitcache = {}
64 self.commitcache = {}
65 self.authors = {}
65 self.authors = {}
66 self.authorfile = None
66 self.authorfile = None
67
67
68 self.map = mapfile(ui, revmapfile)
68 self.map = mapfile(ui, revmapfile)
69
69
70 # Read first the dst author map if any
70 # Read first the dst author map if any
71 authorfile = self.dest.authorfile()
71 authorfile = self.dest.authorfile()
72 if authorfile and os.path.exists(authorfile):
72 if authorfile and os.path.exists(authorfile):
73 self.readauthormap(authorfile)
73 self.readauthormap(authorfile)
74 # Extend/Override with new author map if necessary
74 # Extend/Override with new author map if necessary
75 if opts.get('authors'):
75 if opts.get('authors'):
76 self.readauthormap(opts.get('authors'))
76 self.readauthormap(opts.get('authors'))
77 self.authorfile = self.dest.authorfile()
77 self.authorfile = self.dest.authorfile()
78
78
79 def walktree(self, heads):
79 def walktree(self, heads):
80 '''Return a mapping that identifies the uncommitted parents of every
80 '''Return a mapping that identifies the uncommitted parents of every
81 uncommitted changeset.'''
81 uncommitted changeset.'''
82 visit = heads
82 visit = heads
83 known = {}
83 known = {}
84 parents = {}
84 parents = {}
85 while visit:
85 while visit:
86 n = visit.pop(0)
86 n = visit.pop(0)
87 if n in known or n in self.map: continue
87 if n in known or n in self.map: continue
88 known[n] = 1
88 known[n] = 1
89 commit = self.cachecommit(n)
89 commit = self.cachecommit(n)
90 parents[n] = []
90 parents[n] = []
91 for p in commit.parents:
91 for p in commit.parents:
92 parents[n].append(p)
92 parents[n].append(p)
93 visit.append(p)
93 visit.append(p)
94
94
95 return parents
95 return parents
96
96
97 def toposort(self, parents):
97 def toposort(self, parents):
98 '''Return an ordering such that every uncommitted changeset is
98 '''Return an ordering such that every uncommitted changeset is
99 preceeded by all its uncommitted ancestors.'''
99 preceeded by all its uncommitted ancestors.'''
100 visit = parents.keys()
100 visit = parents.keys()
101 seen = {}
101 seen = {}
102 children = {}
102 children = {}
103
103
104 while visit:
104 while visit:
105 n = visit.pop(0)
105 n = visit.pop(0)
106 if n in seen: continue
106 if n in seen: continue
107 seen[n] = 1
107 seen[n] = 1
108 # Ensure that nodes without parents are present in the 'children'
108 # Ensure that nodes without parents are present in the 'children'
109 # mapping.
109 # mapping.
110 children.setdefault(n, [])
110 children.setdefault(n, [])
111 for p in parents[n]:
111 for p in parents[n]:
112 if not p in self.map:
112 if not p in self.map:
113 visit.append(p)
113 visit.append(p)
114 children.setdefault(p, []).append(n)
114 children.setdefault(p, []).append(n)
115
115
116 s = []
116 s = []
117 removed = {}
117 removed = {}
118 visit = children.keys()
118 visit = children.keys()
119 while visit:
119 while visit:
120 n = visit.pop(0)
120 n = visit.pop(0)
121 if n in removed: continue
121 if n in removed: continue
122 dep = 0
122 dep = 0
123 if n in parents:
123 if n in parents:
124 for p in parents[n]:
124 for p in parents[n]:
125 if p in self.map: continue
125 if p in self.map: continue
126 if p not in removed:
126 if p not in removed:
127 # we're still dependent
127 # we're still dependent
128 visit.append(n)
128 visit.append(n)
129 dep = 1
129 dep = 1
130 break
130 break
131
131
132 if not dep:
132 if not dep:
133 # all n's parents are in the list
133 # all n's parents are in the list
134 removed[n] = 1
134 removed[n] = 1
135 if n not in self.map:
135 if n not in self.map:
136 s.append(n)
136 s.append(n)
137 if n in children:
137 if n in children:
138 for c in children[n]:
138 for c in children[n]:
139 visit.insert(0, c)
139 visit.insert(0, c)
140
140
141 if self.opts.get('datesort'):
141 if self.opts.get('datesort'):
142 depth = {}
142 depth = {}
143 for n in s:
143 for n in s:
144 depth[n] = 0
144 depth[n] = 0
145 pl = [p for p in self.commitcache[n].parents
145 pl = [p for p in self.commitcache[n].parents
146 if p not in self.map]
146 if p not in self.map]
147 if pl:
147 if pl:
148 depth[n] = max([depth[p] for p in pl]) + 1
148 depth[n] = max([depth[p] for p in pl]) + 1
149
149
150 s = [(depth[n], self.commitcache[n].date, n) for n in s]
150 s = [(depth[n], self.commitcache[n].date, n) for n in s]
151 s.sort()
151 s.sort()
152 s = [e[2] for e in s]
152 s = [e[2] for e in s]
153
153
154 return s
154 return s
155
155
156 def writeauthormap(self):
156 def writeauthormap(self):
157 authorfile = self.authorfile
157 authorfile = self.authorfile
158 if authorfile:
158 if authorfile:
159 self.ui.status('Writing author map file %s\n' % authorfile)
159 self.ui.status('Writing author map file %s\n' % authorfile)
160 ofile = open(authorfile, 'w+')
160 ofile = open(authorfile, 'w+')
161 for author in self.authors:
161 for author in self.authors:
162 ofile.write("%s=%s\n" % (author, self.authors[author]))
162 ofile.write("%s=%s\n" % (author, self.authors[author]))
163 ofile.close()
163 ofile.close()
164
164
165 def readauthormap(self, authorfile):
165 def readauthormap(self, authorfile):
166 afile = open(authorfile, 'r')
166 afile = open(authorfile, 'r')
167 for line in afile:
167 for line in afile:
168 try:
168 try:
169 srcauthor = line.split('=')[0].strip()
169 srcauthor = line.split('=')[0].strip()
170 dstauthor = line.split('=')[1].strip()
170 dstauthor = line.split('=')[1].strip()
171 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
171 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
172 self.ui.status(
172 self.ui.status(
173 'Overriding mapping for author %s, was %s, will be %s\n'
173 'Overriding mapping for author %s, was %s, will be %s\n'
174 % (srcauthor, self.authors[srcauthor], dstauthor))
174 % (srcauthor, self.authors[srcauthor], dstauthor))
175 else:
175 else:
176 self.ui.debug('Mapping author %s to %s\n'
176 self.ui.debug('Mapping author %s to %s\n'
177 % (srcauthor, dstauthor))
177 % (srcauthor, dstauthor))
178 self.authors[srcauthor] = dstauthor
178 self.authors[srcauthor] = dstauthor
179 except IndexError:
179 except IndexError:
180 self.ui.warn(
180 self.ui.warn(
181 'Ignoring bad line in author file map %s: %s\n'
181 'Ignoring bad line in author file map %s: %s\n'
182 % (authorfile, line))
182 % (authorfile, line))
183 afile.close()
183 afile.close()
184
184
185 def cachecommit(self, rev):
185 def cachecommit(self, rev):
186 commit = self.source.getcommit(rev)
186 commit = self.source.getcommit(rev)
187 commit.author = self.authors.get(commit.author, commit.author)
187 commit.author = self.authors.get(commit.author, commit.author)
188 self.commitcache[rev] = commit
188 self.commitcache[rev] = commit
189 return commit
189 return commit
190
190
191 def copy(self, rev):
191 def copy(self, rev):
192 commit = self.commitcache[rev]
192 commit = self.commitcache[rev]
193 do_copies = hasattr(self.dest, 'copyfile')
193 do_copies = hasattr(self.dest, 'copyfile')
194 filenames = []
194 filenames = []
195
195
196 changes = self.source.getchanges(rev)
196 changes = self.source.getchanges(rev)
197 if isinstance(changes, basestring):
197 if isinstance(changes, basestring):
198 if changes == SKIPREV:
198 if changes == SKIPREV:
199 dest = SKIPREV
199 dest = SKIPREV
200 else:
200 else:
201 dest = self.map[changes]
201 dest = self.map[changes]
202 self.map[rev] = dest
202 self.map[rev] = dest
203 return
203 return
204 files, copies = changes
204 files, copies = changes
205 parents = [self.map[r] for r in commit.parents]
205 parents = [self.map[r] for r in commit.parents]
206 if commit.parents:
206 if commit.parents:
207 prev = commit.parents[0]
207 prev = commit.parents[0]
208 if prev not in self.commitcache:
208 if prev not in self.commitcache:
209 self.cachecommit(prev)
209 self.cachecommit(prev)
210 pbranch = self.commitcache[prev].branch
210 pbranch = self.commitcache[prev].branch
211 else:
211 else:
212 pbranch = None
212 pbranch = None
213 self.dest.setbranch(commit.branch, pbranch, parents)
213 self.dest.setbranch(commit.branch, pbranch, parents)
214 for f, v in files:
214 for f, v in files:
215 filenames.append(f)
215 filenames.append(f)
216 try:
216 try:
217 data = self.source.getfile(f, v)
217 data = self.source.getfile(f, v)
218 except IOError, inst:
218 except IOError, inst:
219 self.dest.delfile(f)
219 self.dest.delfile(f)
220 else:
220 else:
221 e = self.source.getmode(f, v)
221 e = self.source.getmode(f, v)
222 self.dest.putfile(f, e, data)
222 self.dest.putfile(f, e, data)
223 if do_copies:
223 if do_copies:
224 if f in copies:
224 if f in copies:
225 copyf = copies[f]
225 copyf = copies[f]
226 # Merely marks that a copy happened.
226 # Merely marks that a copy happened.
227 self.dest.copyfile(copyf, f)
227 self.dest.copyfile(copyf, f)
228
228
229 newnode = self.dest.putcommit(filenames, parents, commit)
229 newnode = self.dest.putcommit(filenames, parents, commit)
230 self.source.converted(rev, newnode)
230 self.map[rev] = newnode
231 self.map[rev] = newnode
231
232
232 def convert(self):
233 def convert(self):
233 try:
234 try:
234 self.source.before()
235 self.source.before()
235 self.dest.before()
236 self.dest.before()
236 self.source.setrevmap(self.map)
237 self.source.setrevmap(self.map)
237 self.ui.status("scanning source...\n")
238 self.ui.status("scanning source...\n")
238 heads = self.source.getheads()
239 heads = self.source.getheads()
239 parents = self.walktree(heads)
240 parents = self.walktree(heads)
240 self.ui.status("sorting...\n")
241 self.ui.status("sorting...\n")
241 t = self.toposort(parents)
242 t = self.toposort(parents)
242 num = len(t)
243 num = len(t)
243 c = None
244 c = None
244
245
245 self.ui.status("converting...\n")
246 self.ui.status("converting...\n")
246 for c in t:
247 for c in t:
247 num -= 1
248 num -= 1
248 desc = self.commitcache[c].desc
249 desc = self.commitcache[c].desc
249 if "\n" in desc:
250 if "\n" in desc:
250 desc = desc.splitlines()[0]
251 desc = desc.splitlines()[0]
251 self.ui.status("%d %s\n" % (num, desc))
252 self.ui.status("%d %s\n" % (num, desc))
252 self.copy(c)
253 self.copy(c)
253
254
254 tags = self.source.gettags()
255 tags = self.source.gettags()
255 ctags = {}
256 ctags = {}
256 for k in tags:
257 for k in tags:
257 v = tags[k]
258 v = tags[k]
258 if self.map.get(v, SKIPREV) != SKIPREV:
259 if self.map.get(v, SKIPREV) != SKIPREV:
259 ctags[k] = self.map[v]
260 ctags[k] = self.map[v]
260
261
261 if c and ctags:
262 if c and ctags:
262 nrev = self.dest.puttags(ctags)
263 nrev = self.dest.puttags(ctags)
263 # write another hash correspondence to override the previous
264 # write another hash correspondence to override the previous
264 # one so we don't end up with extra tag heads
265 # one so we don't end up with extra tag heads
265 if nrev:
266 if nrev:
266 self.map[c] = nrev
267 self.map[c] = nrev
267
268
268 self.writeauthormap()
269 self.writeauthormap()
269 finally:
270 finally:
270 self.cleanup()
271 self.cleanup()
271
272
272 def cleanup(self):
273 def cleanup(self):
273 try:
274 try:
274 self.dest.after()
275 self.dest.after()
275 finally:
276 finally:
276 self.source.after()
277 self.source.after()
277 self.map.close()
278 self.map.close()
278
279
279 def convert(ui, src, dest=None, revmapfile=None, **opts):
280 def convert(ui, src, dest=None, revmapfile=None, **opts):
280 """Convert a foreign SCM repository to a Mercurial one.
281 """Convert a foreign SCM repository to a Mercurial one.
281
282
282 Accepted source formats:
283 Accepted source formats:
283 - Mercurial
284 - Mercurial
284 - CVS
285 - CVS
285 - Darcs
286 - Darcs
286 - git
287 - git
287 - Subversion
288 - Subversion
288
289
289 Accepted destination formats:
290 Accepted destination formats:
290 - Mercurial
291 - Mercurial
291 - Subversion (history on branches is not preserved)
292 - Subversion (history on branches is not preserved)
292
293
293 If no revision is given, all revisions will be converted. Otherwise,
294 If no revision is given, all revisions will be converted. Otherwise,
294 convert will only import up to the named revision (given in a format
295 convert will only import up to the named revision (given in a format
295 understood by the source).
296 understood by the source).
296
297
297 If no destination directory name is specified, it defaults to the
298 If no destination directory name is specified, it defaults to the
298 basename of the source with '-hg' appended. If the destination
299 basename of the source with '-hg' appended. If the destination
299 repository doesn't exist, it will be created.
300 repository doesn't exist, it will be created.
300
301
301 If <MAPFILE> isn't given, it will be put in a default location
302 If <MAPFILE> isn't given, it will be put in a default location
302 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
303 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
303 file that maps each source commit ID to the destination ID for
304 file that maps each source commit ID to the destination ID for
304 that revision, like so:
305 that revision, like so:
305 <source ID> <destination ID>
306 <source ID> <destination ID>
306
307
307 If the file doesn't exist, it's automatically created. It's updated
308 If the file doesn't exist, it's automatically created. It's updated
308 on each commit copied, so convert-repo can be interrupted and can
309 on each commit copied, so convert-repo can be interrupted and can
309 be run repeatedly to copy new commits.
310 be run repeatedly to copy new commits.
310
311
311 The [username mapping] file is a simple text file that maps each source
312 The [username mapping] file is a simple text file that maps each source
312 commit author to a destination commit author. It is handy for source SCMs
313 commit author to a destination commit author. It is handy for source SCMs
313 that use unix logins to identify authors (eg: CVS). One line per author
314 that use unix logins to identify authors (eg: CVS). One line per author
314 mapping and the line format is:
315 mapping and the line format is:
315 srcauthor=whatever string you want
316 srcauthor=whatever string you want
316
317
317 The filemap is a file that allows filtering and remapping of files
318 The filemap is a file that allows filtering and remapping of files
318 and directories. Comment lines start with '#'. Each line can
319 and directories. Comment lines start with '#'. Each line can
319 contain one of the following directives:
320 contain one of the following directives:
320
321
321 include path/to/file
322 include path/to/file
322
323
323 exclude path/to/file
324 exclude path/to/file
324
325
325 rename from/file to/file
326 rename from/file to/file
326
327
327 The 'include' directive causes a file, or all files under a
328 The 'include' directive causes a file, or all files under a
328 directory, to be included in the destination repository, and the
329 directory, to be included in the destination repository, and the
329 exclusion of all other files and dirs not explicitely included.
330 exclusion of all other files and dirs not explicitely included.
330 The 'exclude' directive causes files or directories to be omitted.
331 The 'exclude' directive causes files or directories to be omitted.
331 The 'rename' directive renames a file or directory. To rename from a
332 The 'rename' directive renames a file or directory. To rename from a
332 subdirectory into the root of the repository, use '.' as the path to
333 subdirectory into the root of the repository, use '.' as the path to
333 rename to.
334 rename to.
334 """
335 """
335
336
336 util._encoding = 'UTF-8'
337 util._encoding = 'UTF-8'
337
338
338 if not dest:
339 if not dest:
339 dest = hg.defaultdest(src) + "-hg"
340 dest = hg.defaultdest(src) + "-hg"
340 ui.status("assuming destination %s\n" % dest)
341 ui.status("assuming destination %s\n" % dest)
341
342
342 destc = convertsink(ui, dest, opts.get('dest_type'))
343 destc = convertsink(ui, dest, opts.get('dest_type'))
343
344
344 try:
345 try:
345 srcc = convertsource(ui, src, opts.get('source_type'),
346 srcc = convertsource(ui, src, opts.get('source_type'),
346 opts.get('rev'))
347 opts.get('rev'))
347 except Exception:
348 except Exception:
348 for path in destc.created:
349 for path in destc.created:
349 shutil.rmtree(path, True)
350 shutil.rmtree(path, True)
350 raise
351 raise
351
352
352 fmap = opts.get('filemap')
353 fmap = opts.get('filemap')
353 if fmap:
354 if fmap:
354 srcc = filemap.filemap_source(ui, srcc, fmap)
355 srcc = filemap.filemap_source(ui, srcc, fmap)
355 destc.setfilemapmode(True)
356 destc.setfilemapmode(True)
356
357
357 if not revmapfile:
358 if not revmapfile:
358 try:
359 try:
359 revmapfile = destc.revmapfile()
360 revmapfile = destc.revmapfile()
360 except:
361 except:
361 revmapfile = os.path.join(destc, "map")
362 revmapfile = os.path.join(destc, "map")
362
363
363 c = converter(ui, srcc, destc, revmapfile, opts)
364 c = converter(ui, srcc, destc, revmapfile, opts)
364 c.convert()
365 c.convert()
365
366
366
367
367 cmdtable = {
368 cmdtable = {
368 "convert":
369 "convert":
369 (convert,
370 (convert,
370 [('A', 'authors', '', 'username mapping filename'),
371 [('A', 'authors', '', 'username mapping filename'),
371 ('d', 'dest-type', '', 'destination repository type'),
372 ('d', 'dest-type', '', 'destination repository type'),
372 ('', 'filemap', '', 'remap file names using contents of file'),
373 ('', 'filemap', '', 'remap file names using contents of file'),
373 ('r', 'rev', '', 'import up to target revision REV'),
374 ('r', 'rev', '', 'import up to target revision REV'),
374 ('s', 'source-type', '', 'source repository type'),
375 ('s', 'source-type', '', 'source repository type'),
375 ('', 'datesort', None, 'try to sort changesets by date')],
376 ('', 'datesort', None, 'try to sort changesets by date')],
376 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
377 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
377 "debugsvnlog":
378 "debugsvnlog":
378 (debugsvnlog,
379 (debugsvnlog,
379 [],
380 [],
380 'hg debugsvnlog'),
381 'hg debugsvnlog'),
381 }
382 }
382
383
@@ -1,292 +1,297 b''
1 # common code for the convert extension
1 # common code for the convert extension
2 import base64, errno
2 import base64, errno
3 import cPickle as pickle
3 import cPickle as pickle
4 from mercurial import util
4 from mercurial import util
5 from mercurial.i18n import _
5 from mercurial.i18n import _
6
6
7 def encodeargs(args):
7 def encodeargs(args):
8 def encodearg(s):
8 def encodearg(s):
9 lines = base64.encodestring(s)
9 lines = base64.encodestring(s)
10 lines = [l.splitlines()[0] for l in lines]
10 lines = [l.splitlines()[0] for l in lines]
11 return ''.join(lines)
11 return ''.join(lines)
12
12
13 s = pickle.dumps(args)
13 s = pickle.dumps(args)
14 return encodearg(s)
14 return encodearg(s)
15
15
16 def decodeargs(s):
16 def decodeargs(s):
17 s = base64.decodestring(s)
17 s = base64.decodestring(s)
18 return pickle.loads(s)
18 return pickle.loads(s)
19
19
20 def checktool(exe, name=None):
20 def checktool(exe, name=None):
21 name = name or exe
21 name = name or exe
22 if not util.find_exe(exe):
22 if not util.find_exe(exe):
23 raise util.Abort('cannot find required "%s" tool' % name)
23 raise util.Abort('cannot find required "%s" tool' % name)
24
24
25 class NoRepo(Exception): pass
25 class NoRepo(Exception): pass
26
26
27 SKIPREV = 'SKIP'
27 SKIPREV = 'SKIP'
28
28
29 class commit(object):
29 class commit(object):
30 def __init__(self, author, date, desc, parents, branch=None, rev=None,
30 def __init__(self, author, date, desc, parents, branch=None, rev=None,
31 extra={}):
31 extra={}):
32 self.author = author
32 self.author = author
33 self.date = date
33 self.date = date
34 self.desc = desc
34 self.desc = desc
35 self.parents = parents
35 self.parents = parents
36 self.branch = branch
36 self.branch = branch
37 self.rev = rev
37 self.rev = rev
38 self.extra = extra
38 self.extra = extra
39
39
40 class converter_source(object):
40 class converter_source(object):
41 """Conversion source interface"""
41 """Conversion source interface"""
42
42
43 def __init__(self, ui, path, rev=None):
43 def __init__(self, ui, path, rev=None):
44 """Initialize conversion source (or raise NoRepo("message")
44 """Initialize conversion source (or raise NoRepo("message")
45 exception if path is not a valid repository)"""
45 exception if path is not a valid repository)"""
46 self.ui = ui
46 self.ui = ui
47 self.path = path
47 self.path = path
48 self.rev = rev
48 self.rev = rev
49
49
50 self.encoding = 'utf-8'
50 self.encoding = 'utf-8'
51
51
52 def before(self):
52 def before(self):
53 pass
53 pass
54
54
55 def after(self):
55 def after(self):
56 pass
56 pass
57
57
58 def setrevmap(self, revmap):
58 def setrevmap(self, revmap):
59 """set the map of already-converted revisions"""
59 """set the map of already-converted revisions"""
60 pass
60 pass
61
61
62 def getheads(self):
62 def getheads(self):
63 """Return a list of this repository's heads"""
63 """Return a list of this repository's heads"""
64 raise NotImplementedError()
64 raise NotImplementedError()
65
65
66 def getfile(self, name, rev):
66 def getfile(self, name, rev):
67 """Return file contents as a string"""
67 """Return file contents as a string"""
68 raise NotImplementedError()
68 raise NotImplementedError()
69
69
70 def getmode(self, name, rev):
70 def getmode(self, name, rev):
71 """Return file mode, eg. '', 'x', or 'l'"""
71 """Return file mode, eg. '', 'x', or 'l'"""
72 raise NotImplementedError()
72 raise NotImplementedError()
73
73
74 def getchanges(self, version):
74 def getchanges(self, version):
75 """Returns a tuple of (files, copies)
75 """Returns a tuple of (files, copies)
76 Files is a sorted list of (filename, id) tuples for all files changed
76 Files is a sorted list of (filename, id) tuples for all files changed
77 in version, where id is the source revision id of the file.
77 in version, where id is the source revision id of the file.
78
78
79 copies is a dictionary of dest: source
79 copies is a dictionary of dest: source
80 """
80 """
81 raise NotImplementedError()
81 raise NotImplementedError()
82
82
83 def getcommit(self, version):
83 def getcommit(self, version):
84 """Return the commit object for version"""
84 """Return the commit object for version"""
85 raise NotImplementedError()
85 raise NotImplementedError()
86
86
87 def gettags(self):
87 def gettags(self):
88 """Return the tags as a dictionary of name: revision"""
88 """Return the tags as a dictionary of name: revision"""
89 raise NotImplementedError()
89 raise NotImplementedError()
90
90
91 def recode(self, s, encoding=None):
91 def recode(self, s, encoding=None):
92 if not encoding:
92 if not encoding:
93 encoding = self.encoding or 'utf-8'
93 encoding = self.encoding or 'utf-8'
94
94
95 if isinstance(s, unicode):
95 if isinstance(s, unicode):
96 return s.encode("utf-8")
96 return s.encode("utf-8")
97 try:
97 try:
98 return s.decode(encoding).encode("utf-8")
98 return s.decode(encoding).encode("utf-8")
99 except:
99 except:
100 try:
100 try:
101 return s.decode("latin-1").encode("utf-8")
101 return s.decode("latin-1").encode("utf-8")
102 except:
102 except:
103 return s.decode(encoding, "replace").encode("utf-8")
103 return s.decode(encoding, "replace").encode("utf-8")
104
104
105 def getchangedfiles(self, rev, i):
105 def getchangedfiles(self, rev, i):
106 """Return the files changed by rev compared to parent[i].
106 """Return the files changed by rev compared to parent[i].
107
107
108 i is an index selecting one of the parents of rev. The return
108 i is an index selecting one of the parents of rev. The return
109 value should be the list of files that are different in rev and
109 value should be the list of files that are different in rev and
110 this parent.
110 this parent.
111
111
112 If rev has no parents, i is None.
112 If rev has no parents, i is None.
113
113
114 This function is only needed to support --filemap
114 This function is only needed to support --filemap
115 """
115 """
116 raise NotImplementedError()
116 raise NotImplementedError()
117
117
118 def converted(self, rev, sinkrev):
119 '''Notify the source that a revision has been converted.'''
120 pass
121
122
118 class converter_sink(object):
123 class converter_sink(object):
119 """Conversion sink (target) interface"""
124 """Conversion sink (target) interface"""
120
125
121 def __init__(self, ui, path):
126 def __init__(self, ui, path):
122 """Initialize conversion sink (or raise NoRepo("message")
127 """Initialize conversion sink (or raise NoRepo("message")
123 exception if path is not a valid repository)
128 exception if path is not a valid repository)
124
129
125 created is a list of paths to remove if a fatal error occurs
130 created is a list of paths to remove if a fatal error occurs
126 later"""
131 later"""
127 self.ui = ui
132 self.ui = ui
128 self.path = path
133 self.path = path
129 self.created = []
134 self.created = []
130
135
131 def getheads(self):
136 def getheads(self):
132 """Return a list of this repository's heads"""
137 """Return a list of this repository's heads"""
133 raise NotImplementedError()
138 raise NotImplementedError()
134
139
135 def revmapfile(self):
140 def revmapfile(self):
136 """Path to a file that will contain lines
141 """Path to a file that will contain lines
137 source_rev_id sink_rev_id
142 source_rev_id sink_rev_id
138 mapping equivalent revision identifiers for each system."""
143 mapping equivalent revision identifiers for each system."""
139 raise NotImplementedError()
144 raise NotImplementedError()
140
145
141 def authorfile(self):
146 def authorfile(self):
142 """Path to a file that will contain lines
147 """Path to a file that will contain lines
143 srcauthor=dstauthor
148 srcauthor=dstauthor
144 mapping equivalent authors identifiers for each system."""
149 mapping equivalent authors identifiers for each system."""
145 return None
150 return None
146
151
147 def putfile(self, f, e, data):
152 def putfile(self, f, e, data):
148 """Put file for next putcommit().
153 """Put file for next putcommit().
149 f: path to file
154 f: path to file
150 e: '', 'x', or 'l' (regular file, executable, or symlink)
155 e: '', 'x', or 'l' (regular file, executable, or symlink)
151 data: file contents"""
156 data: file contents"""
152 raise NotImplementedError()
157 raise NotImplementedError()
153
158
154 def delfile(self, f):
159 def delfile(self, f):
155 """Delete file for next putcommit().
160 """Delete file for next putcommit().
156 f: path to file"""
161 f: path to file"""
157 raise NotImplementedError()
162 raise NotImplementedError()
158
163
159 def putcommit(self, files, parents, commit):
164 def putcommit(self, files, parents, commit):
160 """Create a revision with all changed files listed in 'files'
165 """Create a revision with all changed files listed in 'files'
161 and having listed parents. 'commit' is a commit object containing
166 and having listed parents. 'commit' is a commit object containing
162 at a minimum the author, date, and message for this changeset.
167 at a minimum the author, date, and message for this changeset.
163 Called after putfile() and delfile() calls. Note that the sink
168 Called after putfile() and delfile() calls. Note that the sink
164 repository is not told to update itself to a particular revision
169 repository is not told to update itself to a particular revision
165 (or even what that revision would be) before it receives the
170 (or even what that revision would be) before it receives the
166 file data."""
171 file data."""
167 raise NotImplementedError()
172 raise NotImplementedError()
168
173
169 def puttags(self, tags):
174 def puttags(self, tags):
170 """Put tags into sink.
175 """Put tags into sink.
171 tags: {tagname: sink_rev_id, ...}"""
176 tags: {tagname: sink_rev_id, ...}"""
172 raise NotImplementedError()
177 raise NotImplementedError()
173
178
174 def setbranch(self, branch, pbranch, parents):
179 def setbranch(self, branch, pbranch, parents):
175 """Set the current branch name. Called before the first putfile
180 """Set the current branch name. Called before the first putfile
176 on the branch.
181 on the branch.
177 branch: branch name for subsequent commits
182 branch: branch name for subsequent commits
178 pbranch: branch name of parent commit
183 pbranch: branch name of parent commit
179 parents: destination revisions of parent"""
184 parents: destination revisions of parent"""
180 pass
185 pass
181
186
182 def setfilemapmode(self, active):
187 def setfilemapmode(self, active):
183 """Tell the destination that we're using a filemap
188 """Tell the destination that we're using a filemap
184
189
185 Some converter_sources (svn in particular) can claim that a file
190 Some converter_sources (svn in particular) can claim that a file
186 was changed in a revision, even if there was no change. This method
191 was changed in a revision, even if there was no change. This method
187 tells the destination that we're using a filemap and that it should
192 tells the destination that we're using a filemap and that it should
188 filter empty revisions.
193 filter empty revisions.
189 """
194 """
190 pass
195 pass
191
196
192 def before(self):
197 def before(self):
193 pass
198 pass
194
199
195 def after(self):
200 def after(self):
196 pass
201 pass
197
202
198
203
199 class commandline(object):
204 class commandline(object):
200 def __init__(self, ui, command):
205 def __init__(self, ui, command):
201 self.ui = ui
206 self.ui = ui
202 self.command = command
207 self.command = command
203
208
204 def prerun(self):
209 def prerun(self):
205 pass
210 pass
206
211
207 def postrun(self):
212 def postrun(self):
208 pass
213 pass
209
214
210 def _run(self, cmd, *args, **kwargs):
215 def _run(self, cmd, *args, **kwargs):
211 cmdline = [self.command, cmd] + list(args)
216 cmdline = [self.command, cmd] + list(args)
212 for k, v in kwargs.iteritems():
217 for k, v in kwargs.iteritems():
213 if len(k) == 1:
218 if len(k) == 1:
214 cmdline.append('-' + k)
219 cmdline.append('-' + k)
215 else:
220 else:
216 cmdline.append('--' + k.replace('_', '-'))
221 cmdline.append('--' + k.replace('_', '-'))
217 try:
222 try:
218 if len(k) == 1:
223 if len(k) == 1:
219 cmdline.append('' + v)
224 cmdline.append('' + v)
220 else:
225 else:
221 cmdline[-1] += '=' + v
226 cmdline[-1] += '=' + v
222 except TypeError:
227 except TypeError:
223 pass
228 pass
224 cmdline = [util.shellquote(arg) for arg in cmdline]
229 cmdline = [util.shellquote(arg) for arg in cmdline]
225 cmdline += ['<', util.nulldev]
230 cmdline += ['<', util.nulldev]
226 cmdline = ' '.join(cmdline)
231 cmdline = ' '.join(cmdline)
227 self.ui.debug(cmdline, '\n')
232 self.ui.debug(cmdline, '\n')
228
233
229 self.prerun()
234 self.prerun()
230 try:
235 try:
231 return util.popen(cmdline)
236 return util.popen(cmdline)
232 finally:
237 finally:
233 self.postrun()
238 self.postrun()
234
239
235 def run(self, cmd, *args, **kwargs):
240 def run(self, cmd, *args, **kwargs):
236 fp = self._run(cmd, *args, **kwargs)
241 fp = self._run(cmd, *args, **kwargs)
237 output = fp.read()
242 output = fp.read()
238 self.ui.debug(output)
243 self.ui.debug(output)
239 return output, fp.close()
244 return output, fp.close()
240
245
241 def checkexit(self, status, output=''):
246 def checkexit(self, status, output=''):
242 if status:
247 if status:
243 if output:
248 if output:
244 self.ui.warn(_('%s error:\n') % self.command)
249 self.ui.warn(_('%s error:\n') % self.command)
245 self.ui.warn(output)
250 self.ui.warn(output)
246 msg = util.explain_exit(status)[0]
251 msg = util.explain_exit(status)[0]
247 raise util.Abort(_('%s %s') % (self.command, msg))
252 raise util.Abort(_('%s %s') % (self.command, msg))
248
253
249 def run0(self, cmd, *args, **kwargs):
254 def run0(self, cmd, *args, **kwargs):
250 output, status = self.run(cmd, *args, **kwargs)
255 output, status = self.run(cmd, *args, **kwargs)
251 self.checkexit(status, output)
256 self.checkexit(status, output)
252 return output
257 return output
253
258
254
259
255 class mapfile(dict):
260 class mapfile(dict):
256 def __init__(self, ui, path):
261 def __init__(self, ui, path):
257 super(mapfile, self).__init__()
262 super(mapfile, self).__init__()
258 self.ui = ui
263 self.ui = ui
259 self.path = path
264 self.path = path
260 self.fp = None
265 self.fp = None
261 self.order = []
266 self.order = []
262 self._read()
267 self._read()
263
268
264 def _read(self):
269 def _read(self):
265 try:
270 try:
266 fp = open(self.path, 'r')
271 fp = open(self.path, 'r')
267 except IOError, err:
272 except IOError, err:
268 if err.errno != errno.ENOENT:
273 if err.errno != errno.ENOENT:
269 raise
274 raise
270 return
275 return
271 for line in fp:
276 for line in fp:
272 key, value = line[:-1].split(' ', 1)
277 key, value = line[:-1].split(' ', 1)
273 if key not in self:
278 if key not in self:
274 self.order.append(key)
279 self.order.append(key)
275 super(mapfile, self).__setitem__(key, value)
280 super(mapfile, self).__setitem__(key, value)
276 fp.close()
281 fp.close()
277
282
278 def __setitem__(self, key, value):
283 def __setitem__(self, key, value):
279 if self.fp is None:
284 if self.fp is None:
280 try:
285 try:
281 self.fp = open(self.path, 'a')
286 self.fp = open(self.path, 'a')
282 except IOError, err:
287 except IOError, err:
283 raise util.Abort(_('could not open map file %r: %s') %
288 raise util.Abort(_('could not open map file %r: %s') %
284 (self.path, err.strerror))
289 (self.path, err.strerror))
285 self.fp.write('%s %s\n' % (key, value))
290 self.fp.write('%s %s\n' % (key, value))
286 self.fp.flush()
291 self.fp.flush()
287 super(mapfile, self).__setitem__(key, value)
292 super(mapfile, self).__setitem__(key, value)
288
293
289 def close(self):
294 def close(self):
290 if self.fp:
295 if self.fp:
291 self.fp.close()
296 self.fp.close()
292 self.fp = None
297 self.fp = None
@@ -1,260 +1,265 b''
1 # hg backend for convert extension
1 # hg backend for convert extension
2
2
3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
4 # the whitespace from the ends of commit messages, but new versions
4 # the whitespace from the ends of commit messages, but new versions
5 # do. Changesets created by those older versions, then converted, may
5 # do. Changesets created by those older versions, then converted, may
6 # thus have different hashes for changesets that are otherwise
6 # thus have different hashes for changesets that are otherwise
7 # identical.
7 # identical.
8
8
9
9
10 import os, time
10 import os, time
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import *
12 from mercurial.node import *
13 from mercurial import hg, lock, revlog, util
13 from mercurial import hg, lock, revlog, util
14
14
15 from common import NoRepo, commit, converter_source, converter_sink
15 from common import NoRepo, commit, converter_source, converter_sink
16
16
17 class mercurial_sink(converter_sink):
17 class mercurial_sink(converter_sink):
18 def __init__(self, ui, path):
18 def __init__(self, ui, path):
19 converter_sink.__init__(self, ui, path)
19 converter_sink.__init__(self, ui, path)
20 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
20 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
21 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
21 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
22 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
22 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
23 self.lastbranch = None
23 self.lastbranch = None
24 if os.path.isdir(path) and len(os.listdir(path)) > 0:
24 if os.path.isdir(path) and len(os.listdir(path)) > 0:
25 try:
25 try:
26 self.repo = hg.repository(self.ui, path)
26 self.repo = hg.repository(self.ui, path)
27 ui.status(_('destination %s is a Mercurial repository\n') %
28 path)
29 except hg.RepoError, err:
27 except hg.RepoError, err:
30 ui.print_exc()
28 ui.print_exc()
31 raise NoRepo(err.args[0])
29 raise NoRepo(err.args[0])
32 else:
30 else:
33 try:
31 try:
34 ui.status(_('initializing destination %s repository\n') % path)
32 ui.status(_('initializing destination %s repository\n') % path)
35 self.repo = hg.repository(self.ui, path, create=True)
33 self.repo = hg.repository(self.ui, path, create=True)
36 self.created.append(path)
34 self.created.append(path)
37 except hg.RepoError, err:
35 except hg.RepoError, err:
38 ui.print_exc()
36 ui.print_exc()
39 raise NoRepo("could not create hg repo %s as sink" % path)
37 raise NoRepo("could not create hg repo %s as sink" % path)
40 self.lock = None
38 self.lock = None
41 self.wlock = None
39 self.wlock = None
42 self.filemapmode = False
40 self.filemapmode = False
43
41
44 def before(self):
42 def before(self):
45 self.wlock = self.repo.wlock()
43 self.wlock = self.repo.wlock()
46 self.lock = self.repo.lock()
44 self.lock = self.repo.lock()
47 self.repo.dirstate.clear()
45 self.repo.dirstate.clear()
48
46
49 def after(self):
47 def after(self):
50 self.repo.dirstate.invalidate()
48 self.repo.dirstate.invalidate()
51 self.lock = None
49 self.lock = None
52 self.wlock = None
50 self.wlock = None
53
51
54 def revmapfile(self):
52 def revmapfile(self):
55 return os.path.join(self.path, ".hg", "shamap")
53 return os.path.join(self.path, ".hg", "shamap")
56
54
57 def authorfile(self):
55 def authorfile(self):
58 return os.path.join(self.path, ".hg", "authormap")
56 return os.path.join(self.path, ".hg", "authormap")
59
57
60 def getheads(self):
58 def getheads(self):
61 h = self.repo.changelog.heads()
59 h = self.repo.changelog.heads()
62 return [ hex(x) for x in h ]
60 return [ hex(x) for x in h ]
63
61
64 def putfile(self, f, e, data):
62 def putfile(self, f, e, data):
65 self.repo.wwrite(f, data, e)
63 self.repo.wwrite(f, data, e)
66 if f not in self.repo.dirstate:
64 if f not in self.repo.dirstate:
67 self.repo.dirstate.normallookup(f)
65 self.repo.dirstate.normallookup(f)
68
66
69 def copyfile(self, source, dest):
67 def copyfile(self, source, dest):
70 self.repo.copy(source, dest)
68 self.repo.copy(source, dest)
71
69
72 def delfile(self, f):
70 def delfile(self, f):
73 try:
71 try:
74 util.unlink(self.repo.wjoin(f))
72 util.unlink(self.repo.wjoin(f))
75 #self.repo.remove([f])
73 #self.repo.remove([f])
76 except OSError:
74 except OSError:
77 pass
75 pass
78
76
79 def setbranch(self, branch, pbranch, parents):
77 def setbranch(self, branch, pbranch, parents):
80 if (not self.clonebranches) or (branch == self.lastbranch):
78 if (not self.clonebranches) or (branch == self.lastbranch):
81 return
79 return
82
80
83 self.lastbranch = branch
81 self.lastbranch = branch
84 self.after()
82 self.after()
85 if not branch:
83 if not branch:
86 branch = 'default'
84 branch = 'default'
87 if not pbranch:
85 if not pbranch:
88 pbranch = 'default'
86 pbranch = 'default'
89
87
90 branchpath = os.path.join(self.path, branch)
88 branchpath = os.path.join(self.path, branch)
91 try:
89 try:
92 self.repo = hg.repository(self.ui, branchpath)
90 self.repo = hg.repository(self.ui, branchpath)
93 except:
91 except:
94 if not parents:
92 if not parents:
95 self.repo = hg.repository(self.ui, branchpath, create=True)
93 self.repo = hg.repository(self.ui, branchpath, create=True)
96 else:
94 else:
97 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
95 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
98 hg.clone(self.ui, os.path.join(self.path, pbranch),
96 hg.clone(self.ui, os.path.join(self.path, pbranch),
99 branchpath, rev=parents, update=False,
97 branchpath, rev=parents, update=False,
100 stream=True)
98 stream=True)
101 self.repo = hg.repository(self.ui, branchpath)
99 self.repo = hg.repository(self.ui, branchpath)
102 self.before()
100 self.before()
103
101
104 def putcommit(self, files, parents, commit):
102 def putcommit(self, files, parents, commit):
105 seen = {}
103 seen = {}
106 pl = []
104 pl = []
107 for p in parents:
105 for p in parents:
108 if p not in seen:
106 if p not in seen:
109 pl.append(p)
107 pl.append(p)
110 seen[p] = 1
108 seen[p] = 1
111 parents = pl
109 parents = pl
112 nparents = len(parents)
110 nparents = len(parents)
113 if self.filemapmode and nparents == 1:
111 if self.filemapmode and nparents == 1:
114 m1node = self.repo.changelog.read(bin(parents[0]))[0]
112 m1node = self.repo.changelog.read(bin(parents[0]))[0]
115 parent = parents[0]
113 parent = parents[0]
116
114
117 if len(parents) < 2: parents.append("0" * 40)
115 if len(parents) < 2: parents.append("0" * 40)
118 if len(parents) < 2: parents.append("0" * 40)
116 if len(parents) < 2: parents.append("0" * 40)
119 p2 = parents.pop(0)
117 p2 = parents.pop(0)
120
118
121 text = commit.desc
119 text = commit.desc
122 extra = commit.extra.copy()
120 extra = commit.extra.copy()
123 if self.branchnames and commit.branch:
121 if self.branchnames and commit.branch:
124 extra['branch'] = commit.branch
122 extra['branch'] = commit.branch
125 if commit.rev:
123 if commit.rev:
126 extra['convert_revision'] = commit.rev
124 extra['convert_revision'] = commit.rev
127
125
128 while parents:
126 while parents:
129 p1 = p2
127 p1 = p2
130 p2 = parents.pop(0)
128 p2 = parents.pop(0)
131 a = self.repo.rawcommit(files, text, commit.author, commit.date,
129 a = self.repo.rawcommit(files, text, commit.author, commit.date,
132 bin(p1), bin(p2), extra=extra)
130 bin(p1), bin(p2), extra=extra)
133 self.repo.dirstate.clear()
131 self.repo.dirstate.clear()
134 text = "(octopus merge fixup)\n"
132 text = "(octopus merge fixup)\n"
135 p2 = hg.hex(self.repo.changelog.tip())
133 p2 = hg.hex(self.repo.changelog.tip())
136
134
137 if self.filemapmode and nparents == 1:
135 if self.filemapmode and nparents == 1:
138 man = self.repo.manifest
136 man = self.repo.manifest
139 mnode = self.repo.changelog.read(bin(p2))[0]
137 mnode = self.repo.changelog.read(bin(p2))[0]
140 if not man.cmp(m1node, man.revision(mnode)):
138 if not man.cmp(m1node, man.revision(mnode)):
141 self.repo.rollback()
139 self.repo.rollback()
142 self.repo.dirstate.clear()
140 self.repo.dirstate.clear()
143 return parent
141 return parent
144 return p2
142 return p2
145
143
146 def puttags(self, tags):
144 def puttags(self, tags):
147 try:
145 try:
148 old = self.repo.wfile(".hgtags").read()
146 old = self.repo.wfile(".hgtags").read()
149 oldlines = old.splitlines(1)
147 oldlines = old.splitlines(1)
150 oldlines.sort()
148 oldlines.sort()
151 except:
149 except:
152 oldlines = []
150 oldlines = []
153
151
154 k = tags.keys()
152 k = tags.keys()
155 k.sort()
153 k.sort()
156 newlines = []
154 newlines = []
157 for tag in k:
155 for tag in k:
158 newlines.append("%s %s\n" % (tags[tag], tag))
156 newlines.append("%s %s\n" % (tags[tag], tag))
159
157
160 newlines.sort()
158 newlines.sort()
161
159
162 if newlines != oldlines:
160 if newlines != oldlines:
163 self.ui.status("updating tags\n")
161 self.ui.status("updating tags\n")
164 f = self.repo.wfile(".hgtags", "w")
162 f = self.repo.wfile(".hgtags", "w")
165 f.write("".join(newlines))
163 f.write("".join(newlines))
166 f.close()
164 f.close()
167 if not oldlines: self.repo.add([".hgtags"])
165 if not oldlines: self.repo.add([".hgtags"])
168 date = "%s 0" % int(time.mktime(time.gmtime()))
166 date = "%s 0" % int(time.mktime(time.gmtime()))
169 extra = {}
167 extra = {}
170 if self.tagsbranch != 'default':
168 if self.tagsbranch != 'default':
171 extra['branch'] = self.tagsbranch
169 extra['branch'] = self.tagsbranch
172 try:
170 try:
173 tagparent = self.repo.changectx(self.tagsbranch).node()
171 tagparent = self.repo.changectx(self.tagsbranch).node()
174 except hg.RepoError, inst:
172 except hg.RepoError, inst:
175 tagparent = nullid
173 tagparent = nullid
176 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
174 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
177 date, tagparent, nullid)
175 date, tagparent, nullid)
178 return hex(self.repo.changelog.tip())
176 return hex(self.repo.changelog.tip())
179
177
180 def setfilemapmode(self, active):
178 def setfilemapmode(self, active):
181 self.filemapmode = active
179 self.filemapmode = active
182
180
183 class mercurial_source(converter_source):
181 class mercurial_source(converter_source):
184 def __init__(self, ui, path, rev=None):
182 def __init__(self, ui, path, rev=None):
185 converter_source.__init__(self, ui, path, rev)
183 converter_source.__init__(self, ui, path, rev)
186 try:
184 try:
187 self.repo = hg.repository(self.ui, path)
185 self.repo = hg.repository(self.ui, path)
188 # try to provoke an exception if this isn't really a hg
186 # try to provoke an exception if this isn't really a hg
189 # repo, but some other bogus compatible-looking url
187 # repo, but some other bogus compatible-looking url
190 if not self.repo.local():
188 if not self.repo.local():
191 raise hg.RepoError()
189 raise hg.RepoError()
192 except hg.RepoError:
190 except hg.RepoError:
193 ui.print_exc()
191 ui.print_exc()
194 raise NoRepo("%s is not a local Mercurial repo" % path)
192 raise NoRepo("%s is not a local Mercurial repo" % path)
195 self.lastrev = None
193 self.lastrev = None
196 self.lastctx = None
194 self.lastctx = None
197 self._changescache = None
195 self._changescache = None
196 self.convertfp = None
198
197
199 def changectx(self, rev):
198 def changectx(self, rev):
200 if self.lastrev != rev:
199 if self.lastrev != rev:
201 self.lastctx = self.repo.changectx(rev)
200 self.lastctx = self.repo.changectx(rev)
202 self.lastrev = rev
201 self.lastrev = rev
203 return self.lastctx
202 return self.lastctx
204
203
205 def getheads(self):
204 def getheads(self):
206 if self.rev:
205 if self.rev:
207 return [hex(self.repo.changectx(self.rev).node())]
206 return [hex(self.repo.changectx(self.rev).node())]
208 else:
207 else:
209 return [hex(node) for node in self.repo.heads()]
208 return [hex(node) for node in self.repo.heads()]
210
209
211 def getfile(self, name, rev):
210 def getfile(self, name, rev):
212 try:
211 try:
213 return self.changectx(rev).filectx(name).data()
212 return self.changectx(rev).filectx(name).data()
214 except revlog.LookupError, err:
213 except revlog.LookupError, err:
215 raise IOError(err)
214 raise IOError(err)
216
215
217 def getmode(self, name, rev):
216 def getmode(self, name, rev):
218 m = self.changectx(rev).manifest()
217 m = self.changectx(rev).manifest()
219 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
218 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
220
219
221 def getchanges(self, rev):
220 def getchanges(self, rev):
222 ctx = self.changectx(rev)
221 ctx = self.changectx(rev)
223 if self._changescache and self._changescache[0] == rev:
222 if self._changescache and self._changescache[0] == rev:
224 m, a, r = self._changescache[1]
223 m, a, r = self._changescache[1]
225 else:
224 else:
226 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
225 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
227 changes = [(name, rev) for name in m + a + r]
226 changes = [(name, rev) for name in m + a + r]
228 changes.sort()
227 changes.sort()
229 return (changes, self.getcopies(ctx, m + a))
228 return (changes, self.getcopies(ctx, m + a))
230
229
231 def getcopies(self, ctx, files):
230 def getcopies(self, ctx, files):
232 copies = {}
231 copies = {}
233 for name in files:
232 for name in files:
234 try:
233 try:
235 copies[name] = ctx.filectx(name).renamed()[0]
234 copies[name] = ctx.filectx(name).renamed()[0]
236 except TypeError:
235 except TypeError:
237 pass
236 pass
238 return copies
237 return copies
239
238
240 def getcommit(self, rev):
239 def getcommit(self, rev):
241 ctx = self.changectx(rev)
240 ctx = self.changectx(rev)
242 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
241 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
243 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
242 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
244 desc=ctx.description(), rev=rev, parents=parents,
243 desc=ctx.description(), rev=rev, parents=parents,
245 branch=ctx.branch(), extra=ctx.extra())
244 branch=ctx.branch(), extra=ctx.extra())
246
245
247 def gettags(self):
246 def gettags(self):
248 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
247 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
249 return dict([(name, hex(node)) for name, node in tags])
248 return dict([(name, hex(node)) for name, node in tags])
250
249
251 def getchangedfiles(self, rev, i):
250 def getchangedfiles(self, rev, i):
252 ctx = self.changectx(rev)
251 ctx = self.changectx(rev)
253 i = i or 0
252 i = i or 0
254 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
253 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
255
254
256 if i == 0:
255 if i == 0:
257 self._changescache = (rev, changes)
256 self._changescache = (rev, changes)
258
257
259 return changes[0] + changes[1] + changes[2]
258 return changes[0] + changes[1] + changes[2]
260
259
260 def converted(self, rev, destrev):
261 if self.convertfp is None:
262 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
263 'a')
264 self.convertfp.write('%s %s\n' % (destrev, rev))
265 self.convertfp.flush()
@@ -1,881 +1,902 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 #
4 #
5 # Configuration options:
5 # Configuration options:
6 #
6 #
7 # convert.svn.trunk
7 # convert.svn.trunk
8 # Relative path to the trunk (default: "trunk")
8 # Relative path to the trunk (default: "trunk")
9 # convert.svn.branches
9 # convert.svn.branches
10 # Relative path to tree of branches (default: "branches")
10 # Relative path to tree of branches (default: "branches")
11 # convert.svn.tags
11 # convert.svn.tags
12 # Relative path to tree of tags (default: "tags")
12 # Relative path to tree of tags (default: "tags")
13 #
13 #
14 # Set these in a hgrc, or on the command line as follows:
14 # Set these in a hgrc, or on the command line as follows:
15 #
15 #
16 # hg convert --config convert.svn.trunk=wackoname [...]
16 # hg convert --config convert.svn.trunk=wackoname [...]
17
17
18 import locale
18 import locale
19 import os
19 import os
20 import re
20 import re
21 import sys
21 import sys
22 import cPickle as pickle
22 import cPickle as pickle
23 import tempfile
23 import tempfile
24
24
25 from mercurial import strutil, util
25 from mercurial import strutil, util
26 from mercurial.i18n import _
26 from mercurial.i18n import _
27
27
28 # Subversion stuff. Works best with very recent Python SVN bindings
28 # Subversion stuff. Works best with very recent Python SVN bindings
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 # these bindings.
30 # these bindings.
31
31
32 from cStringIO import StringIO
32 from cStringIO import StringIO
33
33
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 from common import commandline, converter_sink, mapfile
35 from common import commandline, converter_sink, mapfile
36
36
37 try:
37 try:
38 from svn.core import SubversionException, Pool
38 from svn.core import SubversionException, Pool
39 import svn
39 import svn
40 import svn.client
40 import svn.client
41 import svn.core
41 import svn.core
42 import svn.ra
42 import svn.ra
43 import svn.delta
43 import svn.delta
44 import transport
44 import transport
45 except ImportError:
45 except ImportError:
46 pass
46 pass
47
47
48 def geturl(path):
48 def geturl(path):
49 try:
49 try:
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 except SubversionException:
51 except SubversionException:
52 pass
52 pass
53 if os.path.isdir(path):
53 if os.path.isdir(path):
54 return 'file://%s' % os.path.normpath(os.path.abspath(path))
54 return 'file://%s' % os.path.normpath(os.path.abspath(path))
55 return path
55 return path
56
56
57 def optrev(number):
57 def optrev(number):
58 optrev = svn.core.svn_opt_revision_t()
58 optrev = svn.core.svn_opt_revision_t()
59 optrev.kind = svn.core.svn_opt_revision_number
59 optrev.kind = svn.core.svn_opt_revision_number
60 optrev.value.number = number
60 optrev.value.number = number
61 return optrev
61 return optrev
62
62
63 class changedpath(object):
63 class changedpath(object):
64 def __init__(self, p):
64 def __init__(self, p):
65 self.copyfrom_path = p.copyfrom_path
65 self.copyfrom_path = p.copyfrom_path
66 self.copyfrom_rev = p.copyfrom_rev
66 self.copyfrom_rev = p.copyfrom_rev
67 self.action = p.action
67 self.action = p.action
68
68
69 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
69 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
70 strict_node_history=False):
70 strict_node_history=False):
71 protocol = -1
71 protocol = -1
72 def receiver(orig_paths, revnum, author, date, message, pool):
72 def receiver(orig_paths, revnum, author, date, message, pool):
73 if orig_paths is not None:
73 if orig_paths is not None:
74 for k, v in orig_paths.iteritems():
74 for k, v in orig_paths.iteritems():
75 orig_paths[k] = changedpath(v)
75 orig_paths[k] = changedpath(v)
76 pickle.dump((orig_paths, revnum, author, date, message),
76 pickle.dump((orig_paths, revnum, author, date, message),
77 fp, protocol)
77 fp, protocol)
78
78
79 try:
79 try:
80 # Use an ra of our own so that our parent can consume
80 # Use an ra of our own so that our parent can consume
81 # our results without confusing the server.
81 # our results without confusing the server.
82 t = transport.SvnRaTransport(url=url)
82 t = transport.SvnRaTransport(url=url)
83 svn.ra.get_log(t.ra, paths, start, end, limit,
83 svn.ra.get_log(t.ra, paths, start, end, limit,
84 discover_changed_paths,
84 discover_changed_paths,
85 strict_node_history,
85 strict_node_history,
86 receiver)
86 receiver)
87 except SubversionException, (inst, num):
87 except SubversionException, (inst, num):
88 pickle.dump(num, fp, protocol)
88 pickle.dump(num, fp, protocol)
89 else:
89 else:
90 pickle.dump(None, fp, protocol)
90 pickle.dump(None, fp, protocol)
91 fp.close()
91 fp.close()
92
92
93 def debugsvnlog(ui, **opts):
93 def debugsvnlog(ui, **opts):
94 """Fetch SVN log in a subprocess and channel them back to parent to
94 """Fetch SVN log in a subprocess and channel them back to parent to
95 avoid memory collection issues.
95 avoid memory collection issues.
96 """
96 """
97 util.set_binary(sys.stdin)
97 util.set_binary(sys.stdin)
98 util.set_binary(sys.stdout)
98 util.set_binary(sys.stdout)
99 args = decodeargs(sys.stdin.read())
99 args = decodeargs(sys.stdin.read())
100 get_log_child(sys.stdout, *args)
100 get_log_child(sys.stdout, *args)
101
101
102 # SVN conversion code stolen from bzr-svn and tailor
102 # SVN conversion code stolen from bzr-svn and tailor
103 class svn_source(converter_source):
103 class svn_source(converter_source):
104 def __init__(self, ui, url, rev=None):
104 def __init__(self, ui, url, rev=None):
105 super(svn_source, self).__init__(ui, url, rev=rev)
105 super(svn_source, self).__init__(ui, url, rev=rev)
106
106
107 try:
107 try:
108 SubversionException
108 SubversionException
109 except NameError:
109 except NameError:
110 raise NoRepo('Subversion python bindings could not be loaded')
110 raise NoRepo('Subversion python bindings could not be loaded')
111
111
112 self.encoding = locale.getpreferredencoding()
112 self.encoding = locale.getpreferredencoding()
113 self.lastrevs = {}
113 self.lastrevs = {}
114
114
115 latest = None
115 latest = None
116 try:
116 try:
117 # Support file://path@rev syntax. Useful e.g. to convert
117 # Support file://path@rev syntax. Useful e.g. to convert
118 # deleted branches.
118 # deleted branches.
119 at = url.rfind('@')
119 at = url.rfind('@')
120 if at >= 0:
120 if at >= 0:
121 latest = int(url[at+1:])
121 latest = int(url[at+1:])
122 url = url[:at]
122 url = url[:at]
123 except ValueError, e:
123 except ValueError, e:
124 pass
124 pass
125 self.url = geturl(url)
125 self.url = geturl(url)
126 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
126 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
127 try:
127 try:
128 self.transport = transport.SvnRaTransport(url=self.url)
128 self.transport = transport.SvnRaTransport(url=self.url)
129 self.ra = self.transport.ra
129 self.ra = self.transport.ra
130 self.ctx = self.transport.client
130 self.ctx = self.transport.client
131 self.base = svn.ra.get_repos_root(self.ra)
131 self.base = svn.ra.get_repos_root(self.ra)
132 self.module = self.url[len(self.base):]
132 self.module = self.url[len(self.base):]
133 self.modulemap = {} # revision, module
133 self.modulemap = {} # revision, module
134 self.commits = {}
134 self.commits = {}
135 self.paths = {}
135 self.paths = {}
136 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
136 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
137 except SubversionException, e:
137 except SubversionException, e:
138 ui.print_exc()
138 ui.print_exc()
139 raise NoRepo("%s does not look like a Subversion repo" % self.url)
139 raise NoRepo("%s does not look like a Subversion repo" % self.url)
140
140
141 if rev:
141 if rev:
142 try:
142 try:
143 latest = int(rev)
143 latest = int(rev)
144 except ValueError:
144 except ValueError:
145 raise util.Abort('svn: revision %s is not an integer' % rev)
145 raise util.Abort('svn: revision %s is not an integer' % rev)
146
146
147 try:
147 try:
148 self.get_blacklist()
148 self.get_blacklist()
149 except IOError, e:
149 except IOError, e:
150 pass
150 pass
151
151
152 self.last_changed = self.latest(self.module, latest)
152 self.last_changed = self.latest(self.module, latest)
153
153
154 self.head = self.revid(self.last_changed)
154 self.head = self.revid(self.last_changed)
155 self._changescache = None
155 self._changescache = None
156
156
157 if os.path.exists(os.path.join(url, '.svn/entries')):
158 self.wc = url
159 else:
160 self.wc = None
161 self.convertfp = None
162
157 def setrevmap(self, revmap):
163 def setrevmap(self, revmap):
158 lastrevs = {}
164 lastrevs = {}
159 for revid in revmap.iterkeys():
165 for revid in revmap.iterkeys():
160 uuid, module, revnum = self.revsplit(revid)
166 uuid, module, revnum = self.revsplit(revid)
161 lastrevnum = lastrevs.setdefault(module, revnum)
167 lastrevnum = lastrevs.setdefault(module, revnum)
162 if revnum > lastrevnum:
168 if revnum > lastrevnum:
163 lastrevs[module] = revnum
169 lastrevs[module] = revnum
164 self.lastrevs = lastrevs
170 self.lastrevs = lastrevs
165
171
166 def exists(self, path, optrev):
172 def exists(self, path, optrev):
167 try:
173 try:
168 svn.client.ls(self.url.rstrip('/') + '/' + path,
174 svn.client.ls(self.url.rstrip('/') + '/' + path,
169 optrev, False, self.ctx)
175 optrev, False, self.ctx)
170 return True
176 return True
171 except SubversionException, err:
177 except SubversionException, err:
172 return False
178 return False
173
179
174 def getheads(self):
180 def getheads(self):
175 # detect standard /branches, /tags, /trunk layout
181 # detect standard /branches, /tags, /trunk layout
176 rev = optrev(self.last_changed)
182 rev = optrev(self.last_changed)
177 rpath = self.url.strip('/')
183 rpath = self.url.strip('/')
178 cfgtrunk = self.ui.config('convert', 'svn.trunk')
184 cfgtrunk = self.ui.config('convert', 'svn.trunk')
179 cfgbranches = self.ui.config('convert', 'svn.branches')
185 cfgbranches = self.ui.config('convert', 'svn.branches')
180 cfgtags = self.ui.config('convert', 'svn.tags')
186 cfgtags = self.ui.config('convert', 'svn.tags')
181 trunk = (cfgtrunk or 'trunk').strip('/')
187 trunk = (cfgtrunk or 'trunk').strip('/')
182 branches = (cfgbranches or 'branches').strip('/')
188 branches = (cfgbranches or 'branches').strip('/')
183 tags = (cfgtags or 'tags').strip('/')
189 tags = (cfgtags or 'tags').strip('/')
184 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
190 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
185 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
191 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
186 (trunk, branches, tags))
192 (trunk, branches, tags))
187 oldmodule = self.module
193 oldmodule = self.module
188 self.module += '/' + trunk
194 self.module += '/' + trunk
189 lt = self.latest(self.module, self.last_changed)
195 lt = self.latest(self.module, self.last_changed)
190 self.head = self.revid(lt)
196 self.head = self.revid(lt)
191 self.heads = [self.head]
197 self.heads = [self.head]
192 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
198 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
193 self.ctx)
199 self.ctx)
194 for branch in branchnames.keys():
200 for branch in branchnames.keys():
195 if oldmodule:
201 if oldmodule:
196 module = oldmodule + '/' + branches + '/' + branch
202 module = oldmodule + '/' + branches + '/' + branch
197 else:
203 else:
198 module = '/' + branches + '/' + branch
204 module = '/' + branches + '/' + branch
199 brevnum = self.latest(module, self.last_changed)
205 brevnum = self.latest(module, self.last_changed)
200 brev = self.revid(brevnum, module)
206 brev = self.revid(brevnum, module)
201 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
207 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
202 self.heads.append(brev)
208 self.heads.append(brev)
203
209
204 if oldmodule:
210 if oldmodule:
205 self.tags = '%s/%s' % (oldmodule, tags)
211 self.tags = '%s/%s' % (oldmodule, tags)
206 else:
212 else:
207 self.tags = '/%s' % tags
213 self.tags = '/%s' % tags
208
214
209 elif cfgtrunk or cfgbranches or cfgtags:
215 elif cfgtrunk or cfgbranches or cfgtags:
210 raise util.Abort('trunk/branch/tags layout expected, but not found')
216 raise util.Abort('trunk/branch/tags layout expected, but not found')
211 else:
217 else:
212 self.ui.note('working with one branch\n')
218 self.ui.note('working with one branch\n')
213 self.heads = [self.head]
219 self.heads = [self.head]
214 self.tags = tags
220 self.tags = tags
215 return self.heads
221 return self.heads
216
222
217 def getfile(self, file, rev):
223 def getfile(self, file, rev):
218 data, mode = self._getfile(file, rev)
224 data, mode = self._getfile(file, rev)
219 self.modecache[(file, rev)] = mode
225 self.modecache[(file, rev)] = mode
220 return data
226 return data
221
227
222 def getmode(self, file, rev):
228 def getmode(self, file, rev):
223 return self.modecache[(file, rev)]
229 return self.modecache[(file, rev)]
224
230
225 def getchanges(self, rev):
231 def getchanges(self, rev):
226 if self._changescache and self._changescache[0] == rev:
232 if self._changescache and self._changescache[0] == rev:
227 return self._changescache[1]
233 return self._changescache[1]
228 self._changescache = None
234 self._changescache = None
229 self.modecache = {}
235 self.modecache = {}
230 (paths, parents) = self.paths[rev]
236 (paths, parents) = self.paths[rev]
231 files, copies = self.expandpaths(rev, paths, parents)
237 files, copies = self.expandpaths(rev, paths, parents)
232 files.sort()
238 files.sort()
233 files = zip(files, [rev] * len(files))
239 files = zip(files, [rev] * len(files))
234
240
235 # caller caches the result, so free it here to release memory
241 # caller caches the result, so free it here to release memory
236 del self.paths[rev]
242 del self.paths[rev]
237 return (files, copies)
243 return (files, copies)
238
244
239 def getchangedfiles(self, rev, i):
245 def getchangedfiles(self, rev, i):
240 changes = self.getchanges(rev)
246 changes = self.getchanges(rev)
241 self._changescache = (rev, changes)
247 self._changescache = (rev, changes)
242 return [f[0] for f in changes[0]]
248 return [f[0] for f in changes[0]]
243
249
244 def getcommit(self, rev):
250 def getcommit(self, rev):
245 if rev not in self.commits:
251 if rev not in self.commits:
246 uuid, module, revnum = self.revsplit(rev)
252 uuid, module, revnum = self.revsplit(rev)
247 self.module = module
253 self.module = module
248 self.reparent(module)
254 self.reparent(module)
249 stop = self.lastrevs.get(module, 0)
255 stop = self.lastrevs.get(module, 0)
250 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
256 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
251 commit = self.commits[rev]
257 commit = self.commits[rev]
252 # caller caches the result, so free it here to release memory
258 # caller caches the result, so free it here to release memory
253 del self.commits[rev]
259 del self.commits[rev]
254 return commit
260 return commit
255
261
256 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
262 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
257 strict_node_history=False):
263 strict_node_history=False):
258
264
259 def parent(fp):
265 def parent(fp):
260 while True:
266 while True:
261 entry = pickle.load(fp)
267 entry = pickle.load(fp)
262 try:
268 try:
263 orig_paths, revnum, author, date, message = entry
269 orig_paths, revnum, author, date, message = entry
264 except:
270 except:
265 if entry is None:
271 if entry is None:
266 break
272 break
267 raise SubversionException("child raised exception", entry)
273 raise SubversionException("child raised exception", entry)
268 yield entry
274 yield entry
269
275
270 args = [self.url, paths, start, end, limit, discover_changed_paths,
276 args = [self.url, paths, start, end, limit, discover_changed_paths,
271 strict_node_history]
277 strict_node_history]
272 arg = encodeargs(args)
278 arg = encodeargs(args)
273 hgexe = util.hgexecutable()
279 hgexe = util.hgexecutable()
274 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
280 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
275 stdin, stdout = os.popen2(cmd, 'b')
281 stdin, stdout = os.popen2(cmd, 'b')
276
282
277 stdin.write(arg)
283 stdin.write(arg)
278 stdin.close()
284 stdin.close()
279
285
280 for p in parent(stdout):
286 for p in parent(stdout):
281 yield p
287 yield p
282
288
283 def gettags(self):
289 def gettags(self):
284 tags = {}
290 tags = {}
285 start = self.revnum(self.head)
291 start = self.revnum(self.head)
286 try:
292 try:
287 for entry in self.get_log([self.tags], 0, start):
293 for entry in self.get_log([self.tags], 0, start):
288 orig_paths, revnum, author, date, message = entry
294 orig_paths, revnum, author, date, message = entry
289 for path in orig_paths:
295 for path in orig_paths:
290 if not path.startswith(self.tags+'/'):
296 if not path.startswith(self.tags+'/'):
291 continue
297 continue
292 ent = orig_paths[path]
298 ent = orig_paths[path]
293 source = ent.copyfrom_path
299 source = ent.copyfrom_path
294 rev = ent.copyfrom_rev
300 rev = ent.copyfrom_rev
295 tag = path.split('/')[-1]
301 tag = path.split('/')[-1]
296 tags[tag] = self.revid(rev, module=source)
302 tags[tag] = self.revid(rev, module=source)
297 except SubversionException, (inst, num):
303 except SubversionException, (inst, num):
298 self.ui.note('no tags found at revision %d\n' % start)
304 self.ui.note('no tags found at revision %d\n' % start)
299 return tags
305 return tags
300
306
307 def converted(self, rev, destrev):
308 if not self.wc:
309 return
310 if self.convertfp is None:
311 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
312 'a')
313 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
314 self.convertfp.flush()
315
301 # -- helper functions --
316 # -- helper functions --
302
317
303 def revid(self, revnum, module=None):
318 def revid(self, revnum, module=None):
304 if not module:
319 if not module:
305 module = self.module
320 module = self.module
306 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
321 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
307 revnum)
322 revnum)
308
323
309 def revnum(self, rev):
324 def revnum(self, rev):
310 return int(rev.split('@')[-1])
325 return int(rev.split('@')[-1])
311
326
312 def revsplit(self, rev):
327 def revsplit(self, rev):
313 url, revnum = rev.encode(self.encoding).split('@', 1)
328 url, revnum = rev.encode(self.encoding).split('@', 1)
314 revnum = int(revnum)
329 revnum = int(revnum)
315 parts = url.split('/', 1)
330 parts = url.split('/', 1)
316 uuid = parts.pop(0)[4:]
331 uuid = parts.pop(0)[4:]
317 mod = ''
332 mod = ''
318 if parts:
333 if parts:
319 mod = '/' + parts[0]
334 mod = '/' + parts[0]
320 return uuid, mod, revnum
335 return uuid, mod, revnum
321
336
322 def latest(self, path, stop=0):
337 def latest(self, path, stop=0):
323 'find the latest revision affecting path, up to stop'
338 'find the latest revision affecting path, up to stop'
324 if not stop:
339 if not stop:
325 stop = svn.ra.get_latest_revnum(self.ra)
340 stop = svn.ra.get_latest_revnum(self.ra)
326 try:
341 try:
327 self.reparent('')
342 self.reparent('')
328 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
343 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
329 self.reparent(self.module)
344 self.reparent(self.module)
330 except SubversionException:
345 except SubversionException:
331 dirent = None
346 dirent = None
332 if not dirent:
347 if not dirent:
333 raise util.Abort('%s not found up to revision %d' % (path, stop))
348 raise util.Abort('%s not found up to revision %d' % (path, stop))
334
349
335 return dirent.created_rev
350 return dirent.created_rev
336
351
337 def get_blacklist(self):
352 def get_blacklist(self):
338 """Avoid certain revision numbers.
353 """Avoid certain revision numbers.
339 It is not uncommon for two nearby revisions to cancel each other
354 It is not uncommon for two nearby revisions to cancel each other
340 out, e.g. 'I copied trunk into a subdirectory of itself instead
355 out, e.g. 'I copied trunk into a subdirectory of itself instead
341 of making a branch'. The converted repository is significantly
356 of making a branch'. The converted repository is significantly
342 smaller if we ignore such revisions."""
357 smaller if we ignore such revisions."""
343 self.blacklist = util.set()
358 self.blacklist = util.set()
344 blacklist = self.blacklist
359 blacklist = self.blacklist
345 for line in file("blacklist.txt", "r"):
360 for line in file("blacklist.txt", "r"):
346 if not line.startswith("#"):
361 if not line.startswith("#"):
347 try:
362 try:
348 svn_rev = int(line.strip())
363 svn_rev = int(line.strip())
349 blacklist.add(svn_rev)
364 blacklist.add(svn_rev)
350 except ValueError, e:
365 except ValueError, e:
351 pass # not an integer or a comment
366 pass # not an integer or a comment
352
367
353 def is_blacklisted(self, svn_rev):
368 def is_blacklisted(self, svn_rev):
354 return svn_rev in self.blacklist
369 return svn_rev in self.blacklist
355
370
356 def reparent(self, module):
371 def reparent(self, module):
357 svn_url = self.base + module
372 svn_url = self.base + module
358 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
373 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
359 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
374 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
360
375
361 def expandpaths(self, rev, paths, parents):
376 def expandpaths(self, rev, paths, parents):
362 def get_entry_from_path(path, module=self.module):
377 def get_entry_from_path(path, module=self.module):
363 # Given the repository url of this wc, say
378 # Given the repository url of this wc, say
364 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
379 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
365 # extract the "entry" portion (a relative path) from what
380 # extract the "entry" portion (a relative path) from what
366 # svn log --xml says, ie
381 # svn log --xml says, ie
367 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
382 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
368 # that is to say "tests/PloneTestCase.py"
383 # that is to say "tests/PloneTestCase.py"
369 if path.startswith(module):
384 if path.startswith(module):
370 relative = path[len(module):]
385 relative = path[len(module):]
371 if relative.startswith('/'):
386 if relative.startswith('/'):
372 return relative[1:]
387 return relative[1:]
373 else:
388 else:
374 return relative
389 return relative
375
390
376 # The path is outside our tracked tree...
391 # The path is outside our tracked tree...
377 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
392 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
378 return None
393 return None
379
394
380 entries = []
395 entries = []
381 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
396 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
382 copies = {}
397 copies = {}
383 revnum = self.revnum(rev)
398 revnum = self.revnum(rev)
384
399
385 if revnum in self.modulemap:
400 if revnum in self.modulemap:
386 new_module = self.modulemap[revnum]
401 new_module = self.modulemap[revnum]
387 if new_module != self.module:
402 if new_module != self.module:
388 self.module = new_module
403 self.module = new_module
389 self.reparent(self.module)
404 self.reparent(self.module)
390
405
391 for path, ent in paths:
406 for path, ent in paths:
392 entrypath = get_entry_from_path(path, module=self.module)
407 entrypath = get_entry_from_path(path, module=self.module)
393 entry = entrypath.decode(self.encoding)
408 entry = entrypath.decode(self.encoding)
394
409
395 kind = svn.ra.check_path(self.ra, entrypath, revnum)
410 kind = svn.ra.check_path(self.ra, entrypath, revnum)
396 if kind == svn.core.svn_node_file:
411 if kind == svn.core.svn_node_file:
397 if ent.copyfrom_path:
412 if ent.copyfrom_path:
398 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
413 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
399 if copyfrom_path:
414 if copyfrom_path:
400 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
415 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
401 # It's probably important for hg that the source
416 # It's probably important for hg that the source
402 # exists in the revision's parent, not just the
417 # exists in the revision's parent, not just the
403 # ent.copyfrom_rev
418 # ent.copyfrom_rev
404 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
419 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
405 if fromkind != 0:
420 if fromkind != 0:
406 copies[self.recode(entry)] = self.recode(copyfrom_path)
421 copies[self.recode(entry)] = self.recode(copyfrom_path)
407 entries.append(self.recode(entry))
422 entries.append(self.recode(entry))
408 elif kind == 0: # gone, but had better be a deleted *file*
423 elif kind == 0: # gone, but had better be a deleted *file*
409 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
424 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
410
425
411 # if a branch is created but entries are removed in the same
426 # if a branch is created but entries are removed in the same
412 # changeset, get the right fromrev
427 # changeset, get the right fromrev
413 if parents:
428 if parents:
414 uuid, old_module, fromrev = self.revsplit(parents[0])
429 uuid, old_module, fromrev = self.revsplit(parents[0])
415 else:
430 else:
416 fromrev = revnum - 1
431 fromrev = revnum - 1
417 # might always need to be revnum - 1 in these 3 lines?
432 # might always need to be revnum - 1 in these 3 lines?
418 old_module = self.modulemap.get(fromrev, self.module)
433 old_module = self.modulemap.get(fromrev, self.module)
419
434
420 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
435 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
421 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
436 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
422
437
423 def lookup_parts(p):
438 def lookup_parts(p):
424 rc = None
439 rc = None
425 parts = p.split("/")
440 parts = p.split("/")
426 for i in range(len(parts)):
441 for i in range(len(parts)):
427 part = "/".join(parts[:i])
442 part = "/".join(parts[:i])
428 info = part, copyfrom.get(part, None)
443 info = part, copyfrom.get(part, None)
429 if info[1] is not None:
444 if info[1] is not None:
430 self.ui.debug("Found parent directory %s\n" % info[1])
445 self.ui.debug("Found parent directory %s\n" % info[1])
431 rc = info
446 rc = info
432 return rc
447 return rc
433
448
434 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
449 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
435
450
436 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
451 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
437
452
438 # need to remove fragment from lookup_parts and replace with copyfrom_path
453 # need to remove fragment from lookup_parts and replace with copyfrom_path
439 if frompath is not None:
454 if frompath is not None:
440 self.ui.debug("munge-o-matic\n")
455 self.ui.debug("munge-o-matic\n")
441 self.ui.debug(entrypath + '\n')
456 self.ui.debug(entrypath + '\n')
442 self.ui.debug(entrypath[len(frompath):] + '\n')
457 self.ui.debug(entrypath[len(frompath):] + '\n')
443 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
458 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
444 fromrev = froment.copyfrom_rev
459 fromrev = froment.copyfrom_rev
445 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
460 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
446
461
447 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
462 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
448 if fromkind == svn.core.svn_node_file: # a deleted file
463 if fromkind == svn.core.svn_node_file: # a deleted file
449 entries.append(self.recode(entry))
464 entries.append(self.recode(entry))
450 elif fromkind == svn.core.svn_node_dir:
465 elif fromkind == svn.core.svn_node_dir:
451 # print "Deleted/moved non-file:", revnum, path, ent
466 # print "Deleted/moved non-file:", revnum, path, ent
452 # children = self._find_children(path, revnum - 1)
467 # children = self._find_children(path, revnum - 1)
453 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
468 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
454 # Sometimes this is tricky. For example: in
469 # Sometimes this is tricky. For example: in
455 # The Subversion Repository revision 6940 a dir
470 # The Subversion Repository revision 6940 a dir
456 # was copied and one of its files was deleted
471 # was copied and one of its files was deleted
457 # from the new location in the same commit. This
472 # from the new location in the same commit. This
458 # code can't deal with that yet.
473 # code can't deal with that yet.
459 if ent.action == 'C':
474 if ent.action == 'C':
460 children = self._find_children(path, fromrev)
475 children = self._find_children(path, fromrev)
461 else:
476 else:
462 oroot = entrypath.strip('/')
477 oroot = entrypath.strip('/')
463 nroot = path.strip('/')
478 nroot = path.strip('/')
464 children = self._find_children(oroot, fromrev)
479 children = self._find_children(oroot, fromrev)
465 children = [s.replace(oroot,nroot) for s in children]
480 children = [s.replace(oroot,nroot) for s in children]
466 # Mark all [files, not directories] as deleted.
481 # Mark all [files, not directories] as deleted.
467 for child in children:
482 for child in children:
468 # Can we move a child directory and its
483 # Can we move a child directory and its
469 # parent in the same commit? (probably can). Could
484 # parent in the same commit? (probably can). Could
470 # cause problems if instead of revnum -1,
485 # cause problems if instead of revnum -1,
471 # we have to look in (copyfrom_path, revnum - 1)
486 # we have to look in (copyfrom_path, revnum - 1)
472 entrypath = get_entry_from_path("/" + child, module=old_module)
487 entrypath = get_entry_from_path("/" + child, module=old_module)
473 if entrypath:
488 if entrypath:
474 entry = self.recode(entrypath.decode(self.encoding))
489 entry = self.recode(entrypath.decode(self.encoding))
475 if entry in copies:
490 if entry in copies:
476 # deleted file within a copy
491 # deleted file within a copy
477 del copies[entry]
492 del copies[entry]
478 else:
493 else:
479 entries.append(entry)
494 entries.append(entry)
480 else:
495 else:
481 self.ui.debug('unknown path in revision %d: %s\n' % \
496 self.ui.debug('unknown path in revision %d: %s\n' % \
482 (revnum, path))
497 (revnum, path))
483 elif kind == svn.core.svn_node_dir:
498 elif kind == svn.core.svn_node_dir:
484 # Should probably synthesize normal file entries
499 # Should probably synthesize normal file entries
485 # and handle as above to clean up copy/rename handling.
500 # and handle as above to clean up copy/rename handling.
486
501
487 # If the directory just had a prop change,
502 # If the directory just had a prop change,
488 # then we shouldn't need to look for its children.
503 # then we shouldn't need to look for its children.
489 # Also this could create duplicate entries. Not sure
504 # Also this could create duplicate entries. Not sure
490 # whether this will matter. Maybe should make entries a set.
505 # whether this will matter. Maybe should make entries a set.
491 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
506 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
492 # This will fail if a directory was copied
507 # This will fail if a directory was copied
493 # from another branch and then some of its files
508 # from another branch and then some of its files
494 # were deleted in the same transaction.
509 # were deleted in the same transaction.
495 children = self._find_children(path, revnum)
510 children = self._find_children(path, revnum)
496 children.sort()
511 children.sort()
497 for child in children:
512 for child in children:
498 # Can we move a child directory and its
513 # Can we move a child directory and its
499 # parent in the same commit? (probably can). Could
514 # parent in the same commit? (probably can). Could
500 # cause problems if instead of revnum -1,
515 # cause problems if instead of revnum -1,
501 # we have to look in (copyfrom_path, revnum - 1)
516 # we have to look in (copyfrom_path, revnum - 1)
502 entrypath = get_entry_from_path("/" + child, module=self.module)
517 entrypath = get_entry_from_path("/" + child, module=self.module)
503 # print child, self.module, entrypath
518 # print child, self.module, entrypath
504 if entrypath:
519 if entrypath:
505 # Need to filter out directories here...
520 # Need to filter out directories here...
506 kind = svn.ra.check_path(self.ra, entrypath, revnum)
521 kind = svn.ra.check_path(self.ra, entrypath, revnum)
507 if kind != svn.core.svn_node_dir:
522 if kind != svn.core.svn_node_dir:
508 entries.append(self.recode(entrypath))
523 entries.append(self.recode(entrypath))
509
524
510 # Copies here (must copy all from source)
525 # Copies here (must copy all from source)
511 # Probably not a real problem for us if
526 # Probably not a real problem for us if
512 # source does not exist
527 # source does not exist
513
528
514 # Can do this with the copy command "hg copy"
529 # Can do this with the copy command "hg copy"
515 # if ent.copyfrom_path:
530 # if ent.copyfrom_path:
516 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
531 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
517 # module=self.module)
532 # module=self.module)
518 # copyto_entry = entrypath
533 # copyto_entry = entrypath
519 #
534 #
520 # print "copy directory", copyfrom_entry, 'to', copyto_entry
535 # print "copy directory", copyfrom_entry, 'to', copyto_entry
521 #
536 #
522 # copies.append((copyfrom_entry, copyto_entry))
537 # copies.append((copyfrom_entry, copyto_entry))
523
538
524 if ent.copyfrom_path:
539 if ent.copyfrom_path:
525 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
540 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
526 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
541 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
527 if copyfrom_entry:
542 if copyfrom_entry:
528 copyfrom[path] = ent
543 copyfrom[path] = ent
529 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
544 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
530
545
531 # Good, /probably/ a regular copy. Really should check
546 # Good, /probably/ a regular copy. Really should check
532 # to see whether the parent revision actually contains
547 # to see whether the parent revision actually contains
533 # the directory in question.
548 # the directory in question.
534 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
549 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
535 children.sort()
550 children.sort()
536 for child in children:
551 for child in children:
537 entrypath = get_entry_from_path("/" + child, module=self.module)
552 entrypath = get_entry_from_path("/" + child, module=self.module)
538 if entrypath:
553 if entrypath:
539 entry = entrypath.decode(self.encoding)
554 entry = entrypath.decode(self.encoding)
540 # print "COPY COPY From", copyfrom_entry, entry
555 # print "COPY COPY From", copyfrom_entry, entry
541 copyto_path = path + entry[len(copyfrom_entry):]
556 copyto_path = path + entry[len(copyfrom_entry):]
542 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
557 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
543 # print "COPY", entry, "COPY To", copyto_entry
558 # print "COPY", entry, "COPY To", copyto_entry
544 copies[self.recode(copyto_entry)] = self.recode(entry)
559 copies[self.recode(copyto_entry)] = self.recode(entry)
545 # copy from quux splort/quuxfile
560 # copy from quux splort/quuxfile
546
561
547 return (entries, copies)
562 return (entries, copies)
548
563
549 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
564 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
550 self.child_cset = None
565 self.child_cset = None
551 def parselogentry(orig_paths, revnum, author, date, message):
566 def parselogentry(orig_paths, revnum, author, date, message):
552 self.ui.debug("parsing revision %d (%d changes)\n" %
567 self.ui.debug("parsing revision %d (%d changes)\n" %
553 (revnum, len(orig_paths)))
568 (revnum, len(orig_paths)))
554
569
555 if revnum in self.modulemap:
570 if revnum in self.modulemap:
556 new_module = self.modulemap[revnum]
571 new_module = self.modulemap[revnum]
557 if new_module != self.module:
572 if new_module != self.module:
558 self.module = new_module
573 self.module = new_module
559 self.reparent(self.module)
574 self.reparent(self.module)
560
575
561 rev = self.revid(revnum)
576 rev = self.revid(revnum)
562 # branch log might return entries for a parent we already have
577 # branch log might return entries for a parent we already have
563 if (rev in self.commits or
578 if (rev in self.commits or
564 (revnum < self.lastrevs.get(self.module, 0))):
579 (revnum < self.lastrevs.get(self.module, 0))):
565 return
580 return
566
581
567 parents = []
582 parents = []
568 # check whether this revision is the start of a branch
583 # check whether this revision is the start of a branch
569 if self.module in orig_paths:
584 if self.module in orig_paths:
570 ent = orig_paths[self.module]
585 ent = orig_paths[self.module]
571 if ent.copyfrom_path:
586 if ent.copyfrom_path:
572 # ent.copyfrom_rev may not be the actual last revision
587 # ent.copyfrom_rev may not be the actual last revision
573 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
588 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
574 self.modulemap[prev] = ent.copyfrom_path
589 self.modulemap[prev] = ent.copyfrom_path
575 parents = [self.revid(prev, ent.copyfrom_path)]
590 parents = [self.revid(prev, ent.copyfrom_path)]
576 self.ui.note('found parent of branch %s at %d: %s\n' % \
591 self.ui.note('found parent of branch %s at %d: %s\n' % \
577 (self.module, prev, ent.copyfrom_path))
592 (self.module, prev, ent.copyfrom_path))
578 else:
593 else:
579 self.ui.debug("No copyfrom path, don't know what to do.\n")
594 self.ui.debug("No copyfrom path, don't know what to do.\n")
580
595
581 self.modulemap[revnum] = self.module # track backwards in time
596 self.modulemap[revnum] = self.module # track backwards in time
582
597
583 orig_paths = orig_paths.items()
598 orig_paths = orig_paths.items()
584 orig_paths.sort()
599 orig_paths.sort()
585 paths = []
600 paths = []
586 # filter out unrelated paths
601 # filter out unrelated paths
587 for path, ent in orig_paths:
602 for path, ent in orig_paths:
588 if not path.startswith(self.module):
603 if not path.startswith(self.module):
589 self.ui.debug("boring@%s: %s\n" % (revnum, path))
604 self.ui.debug("boring@%s: %s\n" % (revnum, path))
590 continue
605 continue
591 paths.append((path, ent))
606 paths.append((path, ent))
592
607
593 self.paths[rev] = (paths, parents)
608 self.paths[rev] = (paths, parents)
594
609
595 # Example SVN datetime. Includes microseconds.
610 # Example SVN datetime. Includes microseconds.
596 # ISO-8601 conformant
611 # ISO-8601 conformant
597 # '2007-01-04T17:35:00.902377Z'
612 # '2007-01-04T17:35:00.902377Z'
598 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
613 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
599
614
600 log = message and self.recode(message)
615 log = message and self.recode(message)
601 author = author and self.recode(author) or ''
616 author = author and self.recode(author) or ''
602 try:
617 try:
603 branch = self.module.split("/")[-1]
618 branch = self.module.split("/")[-1]
604 if branch == 'trunk':
619 if branch == 'trunk':
605 branch = ''
620 branch = ''
606 except IndexError:
621 except IndexError:
607 branch = None
622 branch = None
608
623
609 cset = commit(author=author,
624 cset = commit(author=author,
610 date=util.datestr(date),
625 date=util.datestr(date),
611 desc=log,
626 desc=log,
612 parents=parents,
627 parents=parents,
613 branch=branch,
628 branch=branch,
614 rev=rev.encode('utf-8'))
629 rev=rev.encode('utf-8'))
615
630
616 self.commits[rev] = cset
631 self.commits[rev] = cset
617 if self.child_cset and not self.child_cset.parents:
632 if self.child_cset and not self.child_cset.parents:
618 self.child_cset.parents = [rev]
633 self.child_cset.parents = [rev]
619 self.child_cset = cset
634 self.child_cset = cset
620
635
621 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
636 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
622 (self.module, from_revnum, to_revnum))
637 (self.module, from_revnum, to_revnum))
623
638
624 try:
639 try:
625 for entry in self.get_log([self.module], from_revnum, to_revnum):
640 for entry in self.get_log([self.module], from_revnum, to_revnum):
626 orig_paths, revnum, author, date, message = entry
641 orig_paths, revnum, author, date, message = entry
627 if self.is_blacklisted(revnum):
642 if self.is_blacklisted(revnum):
628 self.ui.note('skipping blacklisted revision %d\n' % revnum)
643 self.ui.note('skipping blacklisted revision %d\n' % revnum)
629 continue
644 continue
630 if orig_paths is None:
645 if orig_paths is None:
631 self.ui.debug('revision %d has no entries\n' % revnum)
646 self.ui.debug('revision %d has no entries\n' % revnum)
632 continue
647 continue
633 parselogentry(orig_paths, revnum, author, date, message)
648 parselogentry(orig_paths, revnum, author, date, message)
634 except SubversionException, (inst, num):
649 except SubversionException, (inst, num):
635 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
650 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
636 raise NoSuchRevision(branch=self,
651 raise NoSuchRevision(branch=self,
637 revision="Revision number %d" % to_revnum)
652 revision="Revision number %d" % to_revnum)
638 raise
653 raise
639
654
640 def _getfile(self, file, rev):
655 def _getfile(self, file, rev):
641 io = StringIO()
656 io = StringIO()
642 # TODO: ra.get_file transmits the whole file instead of diffs.
657 # TODO: ra.get_file transmits the whole file instead of diffs.
643 mode = ''
658 mode = ''
644 try:
659 try:
645 revnum = self.revnum(rev)
660 revnum = self.revnum(rev)
646 if self.module != self.modulemap[revnum]:
661 if self.module != self.modulemap[revnum]:
647 self.module = self.modulemap[revnum]
662 self.module = self.modulemap[revnum]
648 self.reparent(self.module)
663 self.reparent(self.module)
649 info = svn.ra.get_file(self.ra, file, revnum, io)
664 info = svn.ra.get_file(self.ra, file, revnum, io)
650 if isinstance(info, list):
665 if isinstance(info, list):
651 info = info[-1]
666 info = info[-1]
652 mode = ("svn:executable" in info) and 'x' or ''
667 mode = ("svn:executable" in info) and 'x' or ''
653 mode = ("svn:special" in info) and 'l' or mode
668 mode = ("svn:special" in info) and 'l' or mode
654 except SubversionException, e:
669 except SubversionException, e:
655 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
670 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
656 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
671 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
657 if e.apr_err in notfound: # File not found
672 if e.apr_err in notfound: # File not found
658 raise IOError()
673 raise IOError()
659 raise
674 raise
660 data = io.getvalue()
675 data = io.getvalue()
661 if mode == 'l':
676 if mode == 'l':
662 link_prefix = "link "
677 link_prefix = "link "
663 if data.startswith(link_prefix):
678 if data.startswith(link_prefix):
664 data = data[len(link_prefix):]
679 data = data[len(link_prefix):]
665 return data, mode
680 return data, mode
666
681
667 def _find_children(self, path, revnum):
682 def _find_children(self, path, revnum):
668 path = path.strip('/')
683 path = path.strip('/')
669 pool = Pool()
684 pool = Pool()
670 rpath = '/'.join([self.base, path]).strip('/')
685 rpath = '/'.join([self.base, path]).strip('/')
671 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
686 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
672
687
673 pre_revprop_change = '''#!/bin/sh
688 pre_revprop_change = '''#!/bin/sh
674
689
675 REPOS="$1"
690 REPOS="$1"
676 REV="$2"
691 REV="$2"
677 USER="$3"
692 USER="$3"
678 PROPNAME="$4"
693 PROPNAME="$4"
679 ACTION="$5"
694 ACTION="$5"
680
695
681 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
696 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
682 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
697 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
683 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
698 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
684
699
685 echo "Changing prohibited revision property" >&2
700 echo "Changing prohibited revision property" >&2
686 exit 1
701 exit 1
687 '''
702 '''
688
703
689 class svn_sink(converter_sink, commandline):
704 class svn_sink(converter_sink, commandline):
690 commit_re = re.compile(r'Committed revision (\d+).', re.M)
705 commit_re = re.compile(r'Committed revision (\d+).', re.M)
691
706
692 def prerun(self):
707 def prerun(self):
693 if self.wc:
708 if self.wc:
694 os.chdir(self.wc)
709 os.chdir(self.wc)
695
710
696 def postrun(self):
711 def postrun(self):
697 if self.wc:
712 if self.wc:
698 os.chdir(self.cwd)
713 os.chdir(self.cwd)
699
714
700 def join(self, name):
715 def join(self, name):
701 return os.path.join(self.wc, '.svn', name)
716 return os.path.join(self.wc, '.svn', name)
702
717
703 def revmapfile(self):
718 def revmapfile(self):
704 return self.join('hg-shamap')
719 return self.join('hg-shamap')
705
720
706 def authorfile(self):
721 def authorfile(self):
707 return self.join('hg-authormap')
722 return self.join('hg-authormap')
708
723
709 def __init__(self, ui, path):
724 def __init__(self, ui, path):
710 converter_sink.__init__(self, ui, path)
725 converter_sink.__init__(self, ui, path)
711 commandline.__init__(self, ui, 'svn')
726 commandline.__init__(self, ui, 'svn')
712 self.delete = []
727 self.delete = []
713 self.wc = None
728 self.wc = None
714 self.cwd = os.getcwd()
729 self.cwd = os.getcwd()
715
730
716 path = os.path.realpath(path)
731 path = os.path.realpath(path)
717
732
718 created = False
733 created = False
719 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
734 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
720 self.wc = path
735 self.wc = path
721 self.run0('update')
736 self.run0('update')
722 else:
737 else:
723 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
738 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
724
739
725 if os.path.isdir(os.path.dirname(path)):
740 if os.path.isdir(os.path.dirname(path)):
726 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
741 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
727 ui.status(_('initializing svn repo %r\n') %
742 ui.status(_('initializing svn repo %r\n') %
728 os.path.basename(path))
743 os.path.basename(path))
729 commandline(ui, 'svnadmin').run0('create', path)
744 commandline(ui, 'svnadmin').run0('create', path)
730 created = path
745 created = path
731 path = path.replace('\\', '/')
746 path = path.replace('\\', '/')
732 if not path.startswith('/'):
747 if not path.startswith('/'):
733 path = '/' + path
748 path = '/' + path
734 path = 'file://' + path
749 path = 'file://' + path
735
750
736 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
751 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
737 self.run0('checkout', path, wcpath)
752 self.run0('checkout', path, wcpath)
738
753
739 self.wc = wcpath
754 self.wc = wcpath
740 self.opener = util.opener(self.wc)
755 self.opener = util.opener(self.wc)
741 self.wopener = util.opener(self.wc)
756 self.wopener = util.opener(self.wc)
742 self.childmap = mapfile(ui, self.join('hg-childmap'))
757 self.childmap = mapfile(ui, self.join('hg-childmap'))
743 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
758 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
744
759
745 if created:
760 if created:
746 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
761 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
747 fp = open(hook, 'w')
762 fp = open(hook, 'w')
748 fp.write(pre_revprop_change)
763 fp.write(pre_revprop_change)
749 fp.close()
764 fp.close()
750 util.set_exec(hook, True)
765 util.set_exec(hook, True)
751
766
767 xport = transport.SvnRaTransport(url=geturl(path))
768 self.uuid = svn.ra.get_uuid(xport.ra)
769
752 def wjoin(self, *names):
770 def wjoin(self, *names):
753 return os.path.join(self.wc, *names)
771 return os.path.join(self.wc, *names)
754
772
755 def putfile(self, filename, flags, data):
773 def putfile(self, filename, flags, data):
756 if 'l' in flags:
774 if 'l' in flags:
757 self.wopener.symlink(data, filename)
775 self.wopener.symlink(data, filename)
758 else:
776 else:
759 try:
777 try:
760 if os.path.islink(self.wjoin(filename)):
778 if os.path.islink(self.wjoin(filename)):
761 os.unlink(filename)
779 os.unlink(filename)
762 except OSError:
780 except OSError:
763 pass
781 pass
764 self.wopener(filename, 'w').write(data)
782 self.wopener(filename, 'w').write(data)
765
783
766 if self.is_exec:
784 if self.is_exec:
767 was_exec = self.is_exec(self.wjoin(filename))
785 was_exec = self.is_exec(self.wjoin(filename))
768 else:
786 else:
769 # On filesystems not supporting execute-bit, there is no way
787 # On filesystems not supporting execute-bit, there is no way
770 # to know if it is set but asking subversion. Setting it
788 # to know if it is set but asking subversion. Setting it
771 # systematically is just as expensive and much simpler.
789 # systematically is just as expensive and much simpler.
772 was_exec = 'x' not in flags
790 was_exec = 'x' not in flags
773
791
774 util.set_exec(self.wjoin(filename), 'x' in flags)
792 util.set_exec(self.wjoin(filename), 'x' in flags)
775 if was_exec:
793 if was_exec:
776 if 'x' not in flags:
794 if 'x' not in flags:
777 self.run0('propdel', 'svn:executable', filename)
795 self.run0('propdel', 'svn:executable', filename)
778 else:
796 else:
779 if 'x' in flags:
797 if 'x' in flags:
780 self.run0('propset', 'svn:executable', '*', filename)
798 self.run0('propset', 'svn:executable', '*', filename)
781
799
782 def delfile(self, name):
800 def delfile(self, name):
783 self.delete.append(name)
801 self.delete.append(name)
784
802
785 def copyfile(self, source, dest):
803 def copyfile(self, source, dest):
786 # SVN's copy command pukes if the destination file exists, but
804 # SVN's copy command pukes if the destination file exists, but
787 # our copyfile method expects to record a copy that has
805 # our copyfile method expects to record a copy that has
788 # already occurred. Cross the semantic gap.
806 # already occurred. Cross the semantic gap.
789 wdest = self.wjoin(dest)
807 wdest = self.wjoin(dest)
790 exists = os.path.exists(wdest)
808 exists = os.path.exists(wdest)
791 if exists:
809 if exists:
792 fd, tempname = tempfile.mkstemp(
810 fd, tempname = tempfile.mkstemp(
793 prefix='hg-copy-', dir=os.path.dirname(wdest))
811 prefix='hg-copy-', dir=os.path.dirname(wdest))
794 os.close(fd)
812 os.close(fd)
795 os.unlink(tempname)
813 os.unlink(tempname)
796 os.rename(wdest, tempname)
814 os.rename(wdest, tempname)
797 try:
815 try:
798 self.run0('copy', source, dest)
816 self.run0('copy', source, dest)
799 finally:
817 finally:
800 if exists:
818 if exists:
801 try:
819 try:
802 os.unlink(wdest)
820 os.unlink(wdest)
803 except OSError:
821 except OSError:
804 pass
822 pass
805 os.rename(tempname, wdest)
823 os.rename(tempname, wdest)
806
824
807 def dirs_of(self, files):
825 def dirs_of(self, files):
808 dirs = set()
826 dirs = set()
809 for f in files:
827 for f in files:
810 if os.path.isdir(self.wjoin(f)):
828 if os.path.isdir(self.wjoin(f)):
811 dirs.add(f)
829 dirs.add(f)
812 for i in strutil.rfindall(f, '/'):
830 for i in strutil.rfindall(f, '/'):
813 dirs.add(f[:i])
831 dirs.add(f[:i])
814 return dirs
832 return dirs
815
833
816 def add_files(self, files):
834 def add_files(self, files):
817 add_dirs = [d for d in self.dirs_of(files)
835 add_dirs = [d for d in self.dirs_of(files)
818 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
836 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
819 if add_dirs:
837 if add_dirs:
820 add_dirs.sort()
838 add_dirs.sort()
821 self.run('add', non_recursive=True, quiet=True, *add_dirs)
839 self.run('add', non_recursive=True, quiet=True, *add_dirs)
822 if files:
840 if files:
823 self.run('add', quiet=True, *files)
841 self.run('add', quiet=True, *files)
824 return files.union(add_dirs)
842 return files.union(add_dirs)
825
843
826 def tidy_dirs(self, names):
844 def tidy_dirs(self, names):
827 dirs = list(self.dirs_of(names))
845 dirs = list(self.dirs_of(names))
828 dirs.sort(reverse=True)
846 dirs.sort(reverse=True)
829 deleted = []
847 deleted = []
830 for d in dirs:
848 for d in dirs:
831 wd = self.wjoin(d)
849 wd = self.wjoin(d)
832 if os.listdir(wd) == '.svn':
850 if os.listdir(wd) == '.svn':
833 self.run0('delete', d)
851 self.run0('delete', d)
834 deleted.append(d)
852 deleted.append(d)
835 return deleted
853 return deleted
836
854
837 def addchild(self, parent, child):
855 def addchild(self, parent, child):
838 self.childmap[parent] = child
856 self.childmap[parent] = child
839
857
858 def revid(self, rev):
859 return u"svn:%s@%s" % (self.uuid, rev)
860
840 def putcommit(self, files, parents, commit):
861 def putcommit(self, files, parents, commit):
841 for parent in parents:
862 for parent in parents:
842 try:
863 try:
843 return self.childmap[parent]
864 return self.revid(self.childmap[parent])
844 except KeyError:
865 except KeyError:
845 pass
866 pass
846 entries = set(self.delete)
867 entries = set(self.delete)
847 if self.delete:
868 if self.delete:
848 self.run0('delete', *self.delete)
869 self.run0('delete', *self.delete)
849 self.delete = []
870 self.delete = []
850 files = util.frozenset(files)
871 files = util.frozenset(files)
851 entries.update(self.add_files(files.difference(entries)))
872 entries.update(self.add_files(files.difference(entries)))
852 entries.update(self.tidy_dirs(entries))
873 entries.update(self.tidy_dirs(entries))
853 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
874 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
854 fp = os.fdopen(fd, 'w')
875 fp = os.fdopen(fd, 'w')
855 fp.write(commit.desc)
876 fp.write(commit.desc)
856 fp.close()
877 fp.close()
857 try:
878 try:
858 output = self.run0('commit',
879 output = self.run0('commit',
859 username=util.shortuser(commit.author),
880 username=util.shortuser(commit.author),
860 file=messagefile,
881 file=messagefile,
861 *list(entries))
882 *list(entries))
862 try:
883 try:
863 rev = self.commit_re.search(output).group(1)
884 rev = self.commit_re.search(output).group(1)
864 except AttributeError:
885 except AttributeError:
865 self.ui.warn(_('unexpected svn output:\n'))
886 self.ui.warn(_('unexpected svn output:\n'))
866 self.ui.warn(output)
887 self.ui.warn(output)
867 raise util.Abort(_('unable to cope with svn output'))
888 raise util.Abort(_('unable to cope with svn output'))
868 if commit.rev:
889 if commit.rev:
869 self.run('propset', 'hg:convert-rev', commit.rev,
890 self.run('propset', 'hg:convert-rev', commit.rev,
870 revprop=True, revision=rev)
891 revprop=True, revision=rev)
871 if commit.branch and commit.branch != 'default':
892 if commit.branch and commit.branch != 'default':
872 self.run('propset', 'hg:convert-branch', commit.branch,
893 self.run('propset', 'hg:convert-branch', commit.branch,
873 revprop=True, revision=rev)
894 revprop=True, revision=rev)
874 for parent in parents:
895 for parent in parents:
875 self.addchild(parent, rev)
896 self.addchild(parent, rev)
876 return rev
897 return self.revid(rev)
877 finally:
898 finally:
878 os.unlink(messagefile)
899 os.unlink(messagefile)
879
900
880 def puttags(self, tags):
901 def puttags(self, tags):
881 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
902 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now