|
1 # hg.py - hg backend for convert extension |
|
2 # |
|
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others |
|
4 # |
|
5 # This software may be used and distributed according to the terms of the |
|
6 # GNU General Public License version 2 or any later version. |
|
7 |
|
8 # Notes for hg->hg conversion: |
|
9 # |
|
10 # * Old versions of Mercurial didn't trim the whitespace from the ends |
|
11 # of commit messages, but new versions do. Changesets created by |
|
12 # those older versions, then converted, may thus have different |
|
13 # hashes for changesets that are otherwise identical. |
|
14 # |
|
15 # * Using "--config convert.hg.saverev=true" will make the source |
|
16 # identifier to be stored in the converted revision. This will cause |
|
17 # the converted revision to have a different identity than the |
|
18 # source. |
|
19 |
|
20 |
|
21 import os, time, cStringIO |
|
22 from mercurial.i18n import _ |
|
23 from mercurial.node import bin, hex, nullid |
|
24 from mercurial import hg, util, context, error |
|
25 |
|
26 from common import NoRepo, commit, converter_source, converter_sink |
|
27 |
|
28 class mercurial_sink(converter_sink): |
|
29 def __init__(self, ui, path): |
|
30 converter_sink.__init__(self, ui, path) |
|
31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True) |
|
32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False) |
|
33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default') |
|
34 self.lastbranch = None |
|
35 if os.path.isdir(path) and len(os.listdir(path)) > 0: |
|
36 try: |
|
37 self.repo = hg.repository(self.ui, path) |
|
38 if not self.repo.local(): |
|
39 raise NoRepo(_('%s is not a local Mercurial repository') |
|
40 % path) |
|
41 except error.RepoError, err: |
|
42 ui.traceback() |
|
43 raise NoRepo(err.args[0]) |
|
44 else: |
|
45 try: |
|
46 ui.status(_('initializing destination %s repository\n') % path) |
|
47 self.repo = hg.repository(self.ui, path, create=True) |
|
48 if not self.repo.local(): |
|
49 raise NoRepo(_('%s is not a local Mercurial repository') |
|
50 % path) |
|
51 self.created.append(path) |
|
52 except error.RepoError: |
|
53 ui.traceback() |
|
54 raise NoRepo(_("could not create hg repository %s as sink") |
|
55 % path) |
|
56 self.lock = None |
|
57 self.wlock = None |
|
58 self.filemapmode = False |
|
59 |
|
60 def before(self): |
|
61 self.ui.debug('run hg sink pre-conversion action\n') |
|
62 self.wlock = self.repo.wlock() |
|
63 self.lock = self.repo.lock() |
|
64 |
|
65 def after(self): |
|
66 self.ui.debug('run hg sink post-conversion action\n') |
|
67 if self.lock: |
|
68 self.lock.release() |
|
69 if self.wlock: |
|
70 self.wlock.release() |
|
71 |
|
72 def revmapfile(self): |
|
73 return os.path.join(self.path, ".hg", "shamap") |
|
74 |
|
75 def authorfile(self): |
|
76 return os.path.join(self.path, ".hg", "authormap") |
|
77 |
|
78 def getheads(self): |
|
79 h = self.repo.changelog.heads() |
|
80 return [hex(x) for x in h] |
|
81 |
|
82 def setbranch(self, branch, pbranches): |
|
83 if not self.clonebranches: |
|
84 return |
|
85 |
|
86 setbranch = (branch != self.lastbranch) |
|
87 self.lastbranch = branch |
|
88 if not branch: |
|
89 branch = 'default' |
|
90 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches] |
|
91 pbranch = pbranches and pbranches[0][1] or 'default' |
|
92 |
|
93 branchpath = os.path.join(self.path, branch) |
|
94 if setbranch: |
|
95 self.after() |
|
96 try: |
|
97 self.repo = hg.repository(self.ui, branchpath) |
|
98 except: |
|
99 self.repo = hg.repository(self.ui, branchpath, create=True) |
|
100 self.before() |
|
101 |
|
102 # pbranches may bring revisions from other branches (merge parents) |
|
103 # Make sure we have them, or pull them. |
|
104 missings = {} |
|
105 for b in pbranches: |
|
106 try: |
|
107 self.repo.lookup(b[0]) |
|
108 except: |
|
109 missings.setdefault(b[1], []).append(b[0]) |
|
110 |
|
111 if missings: |
|
112 self.after() |
|
113 for pbranch, heads in missings.iteritems(): |
|
114 pbranchpath = os.path.join(self.path, pbranch) |
|
115 prepo = hg.repository(self.ui, pbranchpath) |
|
116 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch)) |
|
117 self.repo.pull(prepo, [prepo.lookup(h) for h in heads]) |
|
118 self.before() |
|
119 |
|
120 def _rewritetags(self, source, revmap, data): |
|
121 fp = cStringIO.StringIO() |
|
122 for line in data.splitlines(): |
|
123 s = line.split(' ', 1) |
|
124 if len(s) != 2: |
|
125 continue |
|
126 revid = revmap.get(source.lookuprev(s[0])) |
|
127 if not revid: |
|
128 continue |
|
129 fp.write('%s %s\n' % (revid, s[1])) |
|
130 return fp.getvalue() |
|
131 |
|
132 def putcommit(self, files, copies, parents, commit, source, revmap): |
|
133 |
|
134 files = dict(files) |
|
135 def getfilectx(repo, memctx, f): |
|
136 v = files[f] |
|
137 data, mode = source.getfile(f, v) |
|
138 if f == '.hgtags': |
|
139 data = self._rewritetags(source, revmap, data) |
|
140 return context.memfilectx(f, data, 'l' in mode, 'x' in mode, |
|
141 copies.get(f)) |
|
142 |
|
143 pl = [] |
|
144 for p in parents: |
|
145 if p not in pl: |
|
146 pl.append(p) |
|
147 parents = pl |
|
148 nparents = len(parents) |
|
149 if self.filemapmode and nparents == 1: |
|
150 m1node = self.repo.changelog.read(bin(parents[0]))[0] |
|
151 parent = parents[0] |
|
152 |
|
153 if len(parents) < 2: |
|
154 parents.append(nullid) |
|
155 if len(parents) < 2: |
|
156 parents.append(nullid) |
|
157 p2 = parents.pop(0) |
|
158 |
|
159 text = commit.desc |
|
160 extra = commit.extra.copy() |
|
161 if self.branchnames and commit.branch: |
|
162 extra['branch'] = commit.branch |
|
163 if commit.rev: |
|
164 extra['convert_revision'] = commit.rev |
|
165 |
|
166 while parents: |
|
167 p1 = p2 |
|
168 p2 = parents.pop(0) |
|
169 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(), |
|
170 getfilectx, commit.author, commit.date, extra) |
|
171 self.repo.commitctx(ctx) |
|
172 text = "(octopus merge fixup)\n" |
|
173 p2 = hex(self.repo.changelog.tip()) |
|
174 |
|
175 if self.filemapmode and nparents == 1: |
|
176 man = self.repo.manifest |
|
177 mnode = self.repo.changelog.read(bin(p2))[0] |
|
178 closed = 'close' in commit.extra |
|
179 if not closed and not man.cmp(m1node, man.revision(mnode)): |
|
180 self.ui.status(_("filtering out empty revision\n")) |
|
181 self.repo.rollback() |
|
182 return parent |
|
183 return p2 |
|
184 |
|
185 def puttags(self, tags): |
|
186 try: |
|
187 parentctx = self.repo[self.tagsbranch] |
|
188 tagparent = parentctx.node() |
|
189 except error.RepoError: |
|
190 parentctx = None |
|
191 tagparent = nullid |
|
192 |
|
193 try: |
|
194 oldlines = sorted(parentctx['.hgtags'].data().splitlines(True)) |
|
195 except: |
|
196 oldlines = [] |
|
197 |
|
198 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags]) |
|
199 if newlines == oldlines: |
|
200 return None, None |
|
201 data = "".join(newlines) |
|
202 def getfilectx(repo, memctx, f): |
|
203 return context.memfilectx(f, data, False, False, None) |
|
204 |
|
205 self.ui.status(_("updating tags\n")) |
|
206 date = "%s 0" % int(time.mktime(time.gmtime())) |
|
207 extra = {'branch': self.tagsbranch} |
|
208 ctx = context.memctx(self.repo, (tagparent, None), "update tags", |
|
209 [".hgtags"], getfilectx, "convert-repo", date, |
|
210 extra) |
|
211 self.repo.commitctx(ctx) |
|
212 return hex(self.repo.changelog.tip()), hex(tagparent) |
|
213 |
|
214 def setfilemapmode(self, active): |
|
215 self.filemapmode = active |
|
216 |
|
217 class mercurial_source(converter_source): |
|
218 def __init__(self, ui, path, rev=None): |
|
219 converter_source.__init__(self, ui, path, rev) |
|
220 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False) |
|
221 self.ignored = set() |
|
222 self.saverev = ui.configbool('convert', 'hg.saverev', False) |
|
223 try: |
|
224 self.repo = hg.repository(self.ui, path) |
|
225 # try to provoke an exception if this isn't really a hg |
|
226 # repo, but some other bogus compatible-looking url |
|
227 if not self.repo.local(): |
|
228 raise error.RepoError() |
|
229 except error.RepoError: |
|
230 ui.traceback() |
|
231 raise NoRepo(_("%s is not a local Mercurial repository") % path) |
|
232 self.lastrev = None |
|
233 self.lastctx = None |
|
234 self._changescache = None |
|
235 self.convertfp = None |
|
236 # Restrict converted revisions to startrev descendants |
|
237 startnode = ui.config('convert', 'hg.startrev') |
|
238 if startnode is not None: |
|
239 try: |
|
240 startnode = self.repo.lookup(startnode) |
|
241 except error.RepoError: |
|
242 raise util.Abort(_('%s is not a valid start revision') |
|
243 % startnode) |
|
244 startrev = self.repo.changelog.rev(startnode) |
|
245 children = {startnode: 1} |
|
246 for rev in self.repo.changelog.descendants(startrev): |
|
247 children[self.repo.changelog.node(rev)] = 1 |
|
248 self.keep = children.__contains__ |
|
249 else: |
|
250 self.keep = util.always |
|
251 |
|
252 def changectx(self, rev): |
|
253 if self.lastrev != rev: |
|
254 self.lastctx = self.repo[rev] |
|
255 self.lastrev = rev |
|
256 return self.lastctx |
|
257 |
|
258 def parents(self, ctx): |
|
259 return [p for p in ctx.parents() if p and self.keep(p.node())] |
|
260 |
|
261 def getheads(self): |
|
262 if self.rev: |
|
263 heads = [self.repo[self.rev].node()] |
|
264 else: |
|
265 heads = self.repo.heads() |
|
266 return [hex(h) for h in heads if self.keep(h)] |
|
267 |
|
268 def getfile(self, name, rev): |
|
269 try: |
|
270 fctx = self.changectx(rev)[name] |
|
271 return fctx.data(), fctx.flags() |
|
272 except error.LookupError, err: |
|
273 raise IOError(err) |
|
274 |
|
275 def getchanges(self, rev): |
|
276 ctx = self.changectx(rev) |
|
277 parents = self.parents(ctx) |
|
278 if not parents: |
|
279 files = sorted(ctx.manifest()) |
|
280 if self.ignoreerrors: |
|
281 # calling getcopies() is a simple way to detect missing |
|
282 # revlogs and populate self.ignored |
|
283 self.getcopies(ctx, parents, files) |
|
284 return [(f, rev) for f in files if f not in self.ignored], {} |
|
285 if self._changescache and self._changescache[0] == rev: |
|
286 m, a, r = self._changescache[1] |
|
287 else: |
|
288 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3] |
|
289 # getcopies() detects missing revlogs early, run it before |
|
290 # filtering the changes. |
|
291 copies = self.getcopies(ctx, parents, m + a) |
|
292 changes = [(name, rev) for name in m + a + r |
|
293 if name not in self.ignored] |
|
294 return sorted(changes), copies |
|
295 |
|
296 def getcopies(self, ctx, parents, files): |
|
297 copies = {} |
|
298 for name in files: |
|
299 if name in self.ignored: |
|
300 continue |
|
301 try: |
|
302 copysource, copynode = ctx.filectx(name).renamed() |
|
303 if copysource in self.ignored or not self.keep(copynode): |
|
304 continue |
|
305 # Ignore copy sources not in parent revisions |
|
306 found = False |
|
307 for p in parents: |
|
308 if copysource in p: |
|
309 found = True |
|
310 break |
|
311 if not found: |
|
312 continue |
|
313 copies[name] = copysource |
|
314 except TypeError: |
|
315 pass |
|
316 except error.LookupError, e: |
|
317 if not self.ignoreerrors: |
|
318 raise |
|
319 self.ignored.add(name) |
|
320 self.ui.warn(_('ignoring: %s\n') % e) |
|
321 return copies |
|
322 |
|
323 def getcommit(self, rev): |
|
324 ctx = self.changectx(rev) |
|
325 parents = [p.hex() for p in self.parents(ctx)] |
|
326 if self.saverev: |
|
327 crev = rev |
|
328 else: |
|
329 crev = None |
|
330 return commit(author=ctx.user(), date=util.datestr(ctx.date()), |
|
331 desc=ctx.description(), rev=crev, parents=parents, |
|
332 branch=ctx.branch(), extra=ctx.extra(), |
|
333 sortkey=ctx.rev()) |
|
334 |
|
335 def gettags(self): |
|
336 tags = [t for t in self.repo.tagslist() if t[0] != 'tip'] |
|
337 return dict([(name, hex(node)) for name, node in tags |
|
338 if self.keep(node)]) |
|
339 |
|
340 def getchangedfiles(self, rev, i): |
|
341 ctx = self.changectx(rev) |
|
342 parents = self.parents(ctx) |
|
343 if not parents and i is None: |
|
344 i = 0 |
|
345 changes = [], ctx.manifest().keys(), [] |
|
346 else: |
|
347 i = i or 0 |
|
348 changes = self.repo.status(parents[i].node(), ctx.node())[:3] |
|
349 changes = [[f for f in l if f not in self.ignored] for l in changes] |
|
350 |
|
351 if i == 0: |
|
352 self._changescache = (rev, changes) |
|
353 |
|
354 return changes[0] + changes[1] + changes[2] |
|
355 |
|
356 def converted(self, rev, destrev): |
|
357 if self.convertfp is None: |
|
358 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'), |
|
359 'a') |
|
360 self.convertfp.write('%s %s\n' % (destrev, rev)) |
|
361 self.convertfp.flush() |
|
362 |
|
363 def before(self): |
|
364 self.ui.debug('run hg source pre-conversion action\n') |
|
365 |
|
366 def after(self): |
|
367 self.ui.debug('run hg source post-conversion action\n') |
|
368 |
|
369 def hasnativeorder(self): |
|
370 return True |
|
371 |
|
372 def lookuprev(self, rev): |
|
373 try: |
|
374 return hex(self.repo.lookup(rev)) |
|
375 except error.RepoError: |
|
376 return None |