##// END OF EJS Templates
fix 'convert' with single commit repositories...
Hollis Blanchard -
r4719:1069205a default
parent child Browse files
Show More
@@ -1,313 +1,318 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from common import NoRepo
9 9 from cvs import convert_cvs
10 10 from git import convert_git
11 11 from hg import convert_mercurial
12 12
13 13 import os
14 14 from mercurial import hg, ui, util, commands
15 15
16 16 commands.norepo += " convert"
17 17
18 18 converters = [convert_cvs, convert_git, convert_mercurial]
19 19
20 20 def converter(ui, path):
21 21 if not os.path.isdir(path):
22 22 raise util.Abort("%s: not a directory" % path)
23 23 for c in converters:
24 24 try:
25 25 return c(ui, path)
26 26 except NoRepo:
27 27 pass
28 28 raise util.Abort("%s: unknown repository type" % path)
29 29
30 30 class convert(object):
31 31 def __init__(self, ui, source, dest, mapfile, opts):
32 32
33 33 self.source = source
34 34 self.dest = dest
35 35 self.ui = ui
36 36 self.opts = opts
37 37 self.commitcache = {}
38 38 self.mapfile = mapfile
39 39 self.mapfilefd = None
40 40 self.authors = {}
41 41 self.authorfile = None
42 42
43 43 self.map = {}
44 44 try:
45 45 origmapfile = open(self.mapfile, 'r')
46 46 for l in origmapfile:
47 47 sv, dv = l[:-1].split()
48 48 self.map[sv] = dv
49 49 origmapfile.close()
50 50 except IOError:
51 51 pass
52 52
53 53 # Read first the dst author map if any
54 54 authorfile = self.dest.authorfile()
55 55 if authorfile and os.path.exists(authorfile):
56 56 self.readauthormap(authorfile)
57 57 # Extend/Override with new author map if necessary
58 58 if opts.get('authors'):
59 59 self.readauthormap(opts.get('authors'))
60 60 self.authorfile = self.dest.authorfile()
61 61
62 62 def walktree(self, heads):
63 '''Return a mapping that identifies the uncommitted parents of every
64 uncommitted changeset.'''
63 65 visit = heads
64 66 known = {}
65 67 parents = {}
66 68 while visit:
67 69 n = visit.pop(0)
68 70 if n in known or n in self.map: continue
69 71 known[n] = 1
70 72 self.commitcache[n] = self.source.getcommit(n)
71 73 cp = self.commitcache[n].parents
74 parents[n] = []
72 75 for p in cp:
73 parents.setdefault(n, []).append(p)
76 parents[n].append(p)
74 77 visit.append(p)
75 78
76 79 return parents
77 80
78 81 def toposort(self, parents):
82 '''Return an ordering such that every uncommitted changeset is
83 preceeded by all its uncommitted ancestors.'''
79 84 visit = parents.keys()
80 85 seen = {}
81 86 children = {}
82 87
83 88 while visit:
84 89 n = visit.pop(0)
85 90 if n in seen: continue
86 91 seen[n] = 1
87 pc = 0
88 if n in parents:
89 for p in parents[n]:
90 if p not in self.map: pc += 1
92 # Ensure that nodes without parents are present in the 'children'
93 # mapping.
94 children.setdefault(n, [])
95 for p in parents[n]:
96 if not p in self.map:
91 97 visit.append(p)
92 children.setdefault(p, []).append(n)
93 if not pc: root = n
98 children.setdefault(p, []).append(n)
94 99
95 100 s = []
96 101 removed = {}
97 102 visit = children.keys()
98 103 while visit:
99 104 n = visit.pop(0)
100 105 if n in removed: continue
101 106 dep = 0
102 107 if n in parents:
103 108 for p in parents[n]:
104 109 if p in self.map: continue
105 110 if p not in removed:
106 111 # we're still dependent
107 112 visit.append(n)
108 113 dep = 1
109 114 break
110 115
111 116 if not dep:
112 117 # all n's parents are in the list
113 118 removed[n] = 1
114 119 if n not in self.map:
115 120 s.append(n)
116 121 if n in children:
117 122 for c in children[n]:
118 123 visit.insert(0, c)
119 124
120 125 if self.opts.get('datesort'):
121 126 depth = {}
122 127 for n in s:
123 128 depth[n] = 0
124 129 pl = [p for p in self.commitcache[n].parents
125 130 if p not in self.map]
126 131 if pl:
127 132 depth[n] = max([depth[p] for p in pl]) + 1
128 133
129 134 s = [(depth[n], self.commitcache[n].date, n) for n in s]
130 135 s.sort()
131 136 s = [e[2] for e in s]
132 137
133 138 return s
134 139
135 140 def mapentry(self, src, dst):
136 141 if self.mapfilefd is None:
137 142 try:
138 143 self.mapfilefd = open(self.mapfile, "a")
139 144 except IOError, (errno, strerror):
140 145 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror))
141 146 self.map[src] = dst
142 147 self.mapfilefd.write("%s %s\n" % (src, dst))
143 148 self.mapfilefd.flush()
144 149
145 150 def writeauthormap(self):
146 151 authorfile = self.authorfile
147 152 if authorfile:
148 153 self.ui.status('Writing author map file %s\n' % authorfile)
149 154 ofile = open(authorfile, 'w+')
150 155 for author in self.authors:
151 156 ofile.write("%s=%s\n" % (author, self.authors[author]))
152 157 ofile.close()
153 158
154 159 def readauthormap(self, authorfile):
155 160 afile = open(authorfile, 'r')
156 161 for line in afile:
157 162 try:
158 163 srcauthor = line.split('=')[0].strip()
159 164 dstauthor = line.split('=')[1].strip()
160 165 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
161 166 self.ui.status(
162 167 'Overriding mapping for author %s, was %s, will be %s\n'
163 168 % (srcauthor, self.authors[srcauthor], dstauthor))
164 169 else:
165 170 self.ui.debug('Mapping author %s to %s\n'
166 171 % (srcauthor, dstauthor))
167 172 self.authors[srcauthor] = dstauthor
168 173 except IndexError:
169 174 self.ui.warn(
170 175 'Ignoring bad line in author file map %s: %s\n'
171 176 % (authorfile, line))
172 177 afile.close()
173 178
174 179 def copy(self, rev):
175 180 c = self.commitcache[rev]
176 181 files = self.source.getchanges(rev)
177 182
178 183 for f, v in files:
179 184 try:
180 185 data = self.source.getfile(f, v)
181 186 except IOError, inst:
182 187 self.dest.delfile(f)
183 188 else:
184 189 e = self.source.getmode(f, v)
185 190 self.dest.putfile(f, e, data)
186 191
187 192 r = [self.map[v] for v in c.parents]
188 193 f = [f for f, v in files]
189 194 newnode = self.dest.putcommit(f, r, c)
190 195 self.mapentry(rev, newnode)
191 196
192 197 def convert(self):
193 198 try:
194 199 self.ui.status("scanning source...\n")
195 200 heads = self.source.getheads()
196 201 parents = self.walktree(heads)
197 202 self.ui.status("sorting...\n")
198 203 t = self.toposort(parents)
199 204 num = len(t)
200 205 c = None
201 206
202 207 self.ui.status("converting...\n")
203 208 for c in t:
204 209 num -= 1
205 210 desc = self.commitcache[c].desc
206 211 if "\n" in desc:
207 212 desc = desc.splitlines()[0]
208 213 author = self.commitcache[c].author
209 214 author = self.authors.get(author, author)
210 215 self.commitcache[c].author = author
211 216 self.ui.status("%d %s\n" % (num, desc))
212 217 self.copy(c)
213 218
214 219 tags = self.source.gettags()
215 220 ctags = {}
216 221 for k in tags:
217 222 v = tags[k]
218 223 if v in self.map:
219 224 ctags[k] = self.map[v]
220 225
221 226 if c and ctags:
222 227 nrev = self.dest.puttags(ctags)
223 228 # write another hash correspondence to override the previous
224 229 # one so we don't end up with extra tag heads
225 230 if nrev:
226 231 self.mapentry(c, nrev)
227 232
228 233 self.writeauthormap()
229 234 finally:
230 235 self.cleanup()
231 236
232 237 def cleanup(self):
233 238 if self.mapfilefd:
234 239 self.mapfilefd.close()
235 240
236 241 def _convert(ui, src, dest=None, mapfile=None, **opts):
237 242 '''Convert a foreign SCM repository to a Mercurial one.
238 243
239 244 Accepted source formats:
240 245 - GIT
241 246 - CVS
242 247
243 248 Accepted destination formats:
244 249 - Mercurial
245 250
246 251 If destination isn't given, a new Mercurial repo named <src>-hg will
247 252 be created. If <mapfile> isn't given, it will be put in a default
248 253 location (<dest>/.hg/shamap by default)
249 254
250 255 The <mapfile> is a simple text file that maps each source commit ID to
251 256 the destination ID for that revision, like so:
252 257 <source ID> <destination ID>
253 258
254 259 If the file doesn't exist, it's automatically created. It's updated
255 260 on each commit copied, so convert-repo can be interrupted and can
256 261 be run repeatedly to copy new commits.
257 262
258 263 The [username mapping] file is a simple text file that maps each source
259 264 commit author to a destination commit author. It is handy for source SCMs
260 265 that use unix logins to identify authors (eg: CVS). One line per author
261 266 mapping and the line format is:
262 267 srcauthor=whatever string you want
263 268 '''
264 269
265 270 srcc = converter(ui, src)
266 271 if not hasattr(srcc, "getcommit"):
267 272 raise util.Abort("%s: can't read from this repo type" % src)
268 273
269 274 if not dest:
270 275 dest = src + "-hg"
271 276 ui.status("assuming destination %s\n" % dest)
272 277
273 278 # Try to be smart and initalize things when required
274 279 if os.path.isdir(dest):
275 280 if len(os.listdir(dest)) > 0:
276 281 try:
277 282 hg.repository(ui, dest)
278 283 ui.status("destination %s is a Mercurial repository\n" % dest)
279 284 except hg.RepoError:
280 285 raise util.Abort(
281 286 "destination directory %s is not empty.\n"
282 287 "Please specify an empty directory to be initialized\n"
283 288 "or an already initialized mercurial repository"
284 289 % dest)
285 290 else:
286 291 ui.status("initializing destination %s repository\n" % dest)
287 292 hg.repository(ui, dest, create=True)
288 293 elif os.path.exists(dest):
289 294 raise util.Abort("destination %s exists and is not a directory" % dest)
290 295 else:
291 296 ui.status("initializing destination %s repository\n" % dest)
292 297 hg.repository(ui, dest, create=True)
293 298
294 299 destc = converter(ui, dest)
295 300 if not hasattr(destc, "putcommit"):
296 301 raise util.Abort("%s: can't write to this repo type" % src)
297 302
298 303 if not mapfile:
299 304 try:
300 305 mapfile = destc.mapfile()
301 306 except:
302 307 mapfile = os.path.join(destc, "map")
303 308
304 309 c = convert(ui, srcc, destc, mapfile, opts)
305 310 c.convert()
306 311
307 312 cmdtable = {
308 313 "convert":
309 314 (_convert,
310 315 [('A', 'authors', '', 'username mapping filename'),
311 316 ('', 'datesort', None, 'try to sort changesets by date')],
312 317 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
313 318 }
General Comments 0
You need to be logged in to leave comments. Login now