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