##// END OF EJS Templates
py3: use pycompat.byteskwargs in hgext/convert/...
Pulkit Goyal -
r36347:93943eef default
parent child Browse files
Show More
@@ -1,493 +1,495 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import base64
9 import base64
10 import datetime
10 import datetime
11 import errno
11 import errno
12 import os
12 import os
13 import re
13 import re
14 import subprocess
14 import subprocess
15
15
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial import (
17 from mercurial import (
18 encoding,
18 encoding,
19 error,
19 error,
20 phases,
20 phases,
21 pycompat,
21 util,
22 util,
22 )
23 )
23
24
24 pickle = util.pickle
25 pickle = util.pickle
25 propertycache = util.propertycache
26 propertycache = util.propertycache
26
27
27 def encodeargs(args):
28 def encodeargs(args):
28 def encodearg(s):
29 def encodearg(s):
29 lines = base64.encodestring(s)
30 lines = base64.encodestring(s)
30 lines = [l.splitlines()[0] for l in lines]
31 lines = [l.splitlines()[0] for l in lines]
31 return ''.join(lines)
32 return ''.join(lines)
32
33
33 s = pickle.dumps(args)
34 s = pickle.dumps(args)
34 return encodearg(s)
35 return encodearg(s)
35
36
36 def decodeargs(s):
37 def decodeargs(s):
37 s = base64.decodestring(s)
38 s = base64.decodestring(s)
38 return pickle.loads(s)
39 return pickle.loads(s)
39
40
40 class MissingTool(Exception):
41 class MissingTool(Exception):
41 pass
42 pass
42
43
43 def checktool(exe, name=None, abort=True):
44 def checktool(exe, name=None, abort=True):
44 name = name or exe
45 name = name or exe
45 if not util.findexe(exe):
46 if not util.findexe(exe):
46 if abort:
47 if abort:
47 exc = error.Abort
48 exc = error.Abort
48 else:
49 else:
49 exc = MissingTool
50 exc = MissingTool
50 raise exc(_('cannot find required "%s" tool') % name)
51 raise exc(_('cannot find required "%s" tool') % name)
51
52
52 class NoRepo(Exception):
53 class NoRepo(Exception):
53 pass
54 pass
54
55
55 SKIPREV = 'SKIP'
56 SKIPREV = 'SKIP'
56
57
57 class commit(object):
58 class commit(object):
58 def __init__(self, author, date, desc, parents, branch=None, rev=None,
59 def __init__(self, author, date, desc, parents, branch=None, rev=None,
59 extra=None, sortkey=None, saverev=True, phase=phases.draft,
60 extra=None, sortkey=None, saverev=True, phase=phases.draft,
60 optparents=None):
61 optparents=None):
61 self.author = author or 'unknown'
62 self.author = author or 'unknown'
62 self.date = date or '0 0'
63 self.date = date or '0 0'
63 self.desc = desc
64 self.desc = desc
64 self.parents = parents # will be converted and used as parents
65 self.parents = parents # will be converted and used as parents
65 self.optparents = optparents or [] # will be used if already converted
66 self.optparents = optparents or [] # will be used if already converted
66 self.branch = branch
67 self.branch = branch
67 self.rev = rev
68 self.rev = rev
68 self.extra = extra or {}
69 self.extra = extra or {}
69 self.sortkey = sortkey
70 self.sortkey = sortkey
70 self.saverev = saverev
71 self.saverev = saverev
71 self.phase = phase
72 self.phase = phase
72
73
73 class converter_source(object):
74 class converter_source(object):
74 """Conversion source interface"""
75 """Conversion source interface"""
75
76
76 def __init__(self, ui, repotype, path=None, revs=None):
77 def __init__(self, ui, repotype, path=None, revs=None):
77 """Initialize conversion source (or raise NoRepo("message")
78 """Initialize conversion source (or raise NoRepo("message")
78 exception if path is not a valid repository)"""
79 exception if path is not a valid repository)"""
79 self.ui = ui
80 self.ui = ui
80 self.path = path
81 self.path = path
81 self.revs = revs
82 self.revs = revs
82 self.repotype = repotype
83 self.repotype = repotype
83
84
84 self.encoding = 'utf-8'
85 self.encoding = 'utf-8'
85
86
86 def checkhexformat(self, revstr, mapname='splicemap'):
87 def checkhexformat(self, revstr, mapname='splicemap'):
87 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
88 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
88 such format for their revision numbering
89 such format for their revision numbering
89 """
90 """
90 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
91 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
91 raise error.Abort(_('%s entry %s is not a valid revision'
92 raise error.Abort(_('%s entry %s is not a valid revision'
92 ' identifier') % (mapname, revstr))
93 ' identifier') % (mapname, revstr))
93
94
94 def before(self):
95 def before(self):
95 pass
96 pass
96
97
97 def after(self):
98 def after(self):
98 pass
99 pass
99
100
100 def targetfilebelongstosource(self, targetfilename):
101 def targetfilebelongstosource(self, targetfilename):
101 """Returns true if the given targetfile belongs to the source repo. This
102 """Returns true if the given targetfile belongs to the source repo. This
102 is useful when only a subdirectory of the target belongs to the source
103 is useful when only a subdirectory of the target belongs to the source
103 repo."""
104 repo."""
104 # For normal full repo converts, this is always True.
105 # For normal full repo converts, this is always True.
105 return True
106 return True
106
107
107 def setrevmap(self, revmap):
108 def setrevmap(self, revmap):
108 """set the map of already-converted revisions"""
109 """set the map of already-converted revisions"""
109
110
110 def getheads(self):
111 def getheads(self):
111 """Return a list of this repository's heads"""
112 """Return a list of this repository's heads"""
112 raise NotImplementedError
113 raise NotImplementedError
113
114
114 def getfile(self, name, rev):
115 def getfile(self, name, rev):
115 """Return a pair (data, mode) where data is the file content
116 """Return a pair (data, mode) where data is the file content
116 as a string and mode one of '', 'x' or 'l'. rev is the
117 as a string and mode one of '', 'x' or 'l'. rev is the
117 identifier returned by a previous call to getchanges().
118 identifier returned by a previous call to getchanges().
118 Data is None if file is missing/deleted in rev.
119 Data is None if file is missing/deleted in rev.
119 """
120 """
120 raise NotImplementedError
121 raise NotImplementedError
121
122
122 def getchanges(self, version, full):
123 def getchanges(self, version, full):
123 """Returns a tuple of (files, copies, cleanp2).
124 """Returns a tuple of (files, copies, cleanp2).
124
125
125 files is a sorted list of (filename, id) tuples for all files
126 files is a sorted list of (filename, id) tuples for all files
126 changed between version and its first parent returned by
127 changed between version and its first parent returned by
127 getcommit(). If full, all files in that revision is returned.
128 getcommit(). If full, all files in that revision is returned.
128 id is the source revision id of the file.
129 id is the source revision id of the file.
129
130
130 copies is a dictionary of dest: source
131 copies is a dictionary of dest: source
131
132
132 cleanp2 is the set of files filenames that are clean against p2.
133 cleanp2 is the set of files filenames that are clean against p2.
133 (Files that are clean against p1 are already not in files (unless
134 (Files that are clean against p1 are already not in files (unless
134 full). This makes it possible to handle p2 clean files similarly.)
135 full). This makes it possible to handle p2 clean files similarly.)
135 """
136 """
136 raise NotImplementedError
137 raise NotImplementedError
137
138
138 def getcommit(self, version):
139 def getcommit(self, version):
139 """Return the commit object for version"""
140 """Return the commit object for version"""
140 raise NotImplementedError
141 raise NotImplementedError
141
142
142 def numcommits(self):
143 def numcommits(self):
143 """Return the number of commits in this source.
144 """Return the number of commits in this source.
144
145
145 If unknown, return None.
146 If unknown, return None.
146 """
147 """
147 return None
148 return None
148
149
149 def gettags(self):
150 def gettags(self):
150 """Return the tags as a dictionary of name: revision
151 """Return the tags as a dictionary of name: revision
151
152
152 Tag names must be UTF-8 strings.
153 Tag names must be UTF-8 strings.
153 """
154 """
154 raise NotImplementedError
155 raise NotImplementedError
155
156
156 def recode(self, s, encoding=None):
157 def recode(self, s, encoding=None):
157 if not encoding:
158 if not encoding:
158 encoding = self.encoding or 'utf-8'
159 encoding = self.encoding or 'utf-8'
159
160
160 if isinstance(s, unicode):
161 if isinstance(s, unicode):
161 return s.encode("utf-8")
162 return s.encode("utf-8")
162 try:
163 try:
163 return s.decode(encoding).encode("utf-8")
164 return s.decode(encoding).encode("utf-8")
164 except UnicodeError:
165 except UnicodeError:
165 try:
166 try:
166 return s.decode("latin-1").encode("utf-8")
167 return s.decode("latin-1").encode("utf-8")
167 except UnicodeError:
168 except UnicodeError:
168 return s.decode(encoding, "replace").encode("utf-8")
169 return s.decode(encoding, "replace").encode("utf-8")
169
170
170 def getchangedfiles(self, rev, i):
171 def getchangedfiles(self, rev, i):
171 """Return the files changed by rev compared to parent[i].
172 """Return the files changed by rev compared to parent[i].
172
173
173 i is an index selecting one of the parents of rev. The return
174 i is an index selecting one of the parents of rev. The return
174 value should be the list of files that are different in rev and
175 value should be the list of files that are different in rev and
175 this parent.
176 this parent.
176
177
177 If rev has no parents, i is None.
178 If rev has no parents, i is None.
178
179
179 This function is only needed to support --filemap
180 This function is only needed to support --filemap
180 """
181 """
181 raise NotImplementedError
182 raise NotImplementedError
182
183
183 def converted(self, rev, sinkrev):
184 def converted(self, rev, sinkrev):
184 '''Notify the source that a revision has been converted.'''
185 '''Notify the source that a revision has been converted.'''
185
186
186 def hasnativeorder(self):
187 def hasnativeorder(self):
187 """Return true if this source has a meaningful, native revision
188 """Return true if this source has a meaningful, native revision
188 order. For instance, Mercurial revisions are store sequentially
189 order. For instance, Mercurial revisions are store sequentially
189 while there is no such global ordering with Darcs.
190 while there is no such global ordering with Darcs.
190 """
191 """
191 return False
192 return False
192
193
193 def hasnativeclose(self):
194 def hasnativeclose(self):
194 """Return true if this source has ability to close branch.
195 """Return true if this source has ability to close branch.
195 """
196 """
196 return False
197 return False
197
198
198 def lookuprev(self, rev):
199 def lookuprev(self, rev):
199 """If rev is a meaningful revision reference in source, return
200 """If rev is a meaningful revision reference in source, return
200 the referenced identifier in the same format used by getcommit().
201 the referenced identifier in the same format used by getcommit().
201 return None otherwise.
202 return None otherwise.
202 """
203 """
203 return None
204 return None
204
205
205 def getbookmarks(self):
206 def getbookmarks(self):
206 """Return the bookmarks as a dictionary of name: revision
207 """Return the bookmarks as a dictionary of name: revision
207
208
208 Bookmark names are to be UTF-8 strings.
209 Bookmark names are to be UTF-8 strings.
209 """
210 """
210 return {}
211 return {}
211
212
212 def checkrevformat(self, revstr, mapname='splicemap'):
213 def checkrevformat(self, revstr, mapname='splicemap'):
213 """revstr is a string that describes a revision in the given
214 """revstr is a string that describes a revision in the given
214 source control system. Return true if revstr has correct
215 source control system. Return true if revstr has correct
215 format.
216 format.
216 """
217 """
217 return True
218 return True
218
219
219 class converter_sink(object):
220 class converter_sink(object):
220 """Conversion sink (target) interface"""
221 """Conversion sink (target) interface"""
221
222
222 def __init__(self, ui, repotype, path):
223 def __init__(self, ui, repotype, path):
223 """Initialize conversion sink (or raise NoRepo("message")
224 """Initialize conversion sink (or raise NoRepo("message")
224 exception if path is not a valid repository)
225 exception if path is not a valid repository)
225
226
226 created is a list of paths to remove if a fatal error occurs
227 created is a list of paths to remove if a fatal error occurs
227 later"""
228 later"""
228 self.ui = ui
229 self.ui = ui
229 self.path = path
230 self.path = path
230 self.created = []
231 self.created = []
231 self.repotype = repotype
232 self.repotype = repotype
232
233
233 def revmapfile(self):
234 def revmapfile(self):
234 """Path to a file that will contain lines
235 """Path to a file that will contain lines
235 source_rev_id sink_rev_id
236 source_rev_id sink_rev_id
236 mapping equivalent revision identifiers for each system."""
237 mapping equivalent revision identifiers for each system."""
237 raise NotImplementedError
238 raise NotImplementedError
238
239
239 def authorfile(self):
240 def authorfile(self):
240 """Path to a file that will contain lines
241 """Path to a file that will contain lines
241 srcauthor=dstauthor
242 srcauthor=dstauthor
242 mapping equivalent authors identifiers for each system."""
243 mapping equivalent authors identifiers for each system."""
243 return None
244 return None
244
245
245 def putcommit(self, files, copies, parents, commit, source, revmap, full,
246 def putcommit(self, files, copies, parents, commit, source, revmap, full,
246 cleanp2):
247 cleanp2):
247 """Create a revision with all changed files listed in 'files'
248 """Create a revision with all changed files listed in 'files'
248 and having listed parents. 'commit' is a commit object
249 and having listed parents. 'commit' is a commit object
249 containing at a minimum the author, date, and message for this
250 containing at a minimum the author, date, and message for this
250 changeset. 'files' is a list of (path, version) tuples,
251 changeset. 'files' is a list of (path, version) tuples,
251 'copies' is a dictionary mapping destinations to sources,
252 'copies' is a dictionary mapping destinations to sources,
252 'source' is the source repository, and 'revmap' is a mapfile
253 'source' is the source repository, and 'revmap' is a mapfile
253 of source revisions to converted revisions. Only getfile() and
254 of source revisions to converted revisions. Only getfile() and
254 lookuprev() should be called on 'source'. 'full' means that 'files'
255 lookuprev() should be called on 'source'. 'full' means that 'files'
255 is complete and all other files should be removed.
256 is complete and all other files should be removed.
256 'cleanp2' is a set of the filenames that are unchanged from p2
257 'cleanp2' is a set of the filenames that are unchanged from p2
257 (only in the common merge case where there two parents).
258 (only in the common merge case where there two parents).
258
259
259 Note that the sink repository is not told to update itself to
260 Note that the sink repository is not told to update itself to
260 a particular revision (or even what that revision would be)
261 a particular revision (or even what that revision would be)
261 before it receives the file data.
262 before it receives the file data.
262 """
263 """
263 raise NotImplementedError
264 raise NotImplementedError
264
265
265 def puttags(self, tags):
266 def puttags(self, tags):
266 """Put tags into sink.
267 """Put tags into sink.
267
268
268 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
269 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
269 Return a pair (tag_revision, tag_parent_revision), or (None, None)
270 Return a pair (tag_revision, tag_parent_revision), or (None, None)
270 if nothing was changed.
271 if nothing was changed.
271 """
272 """
272 raise NotImplementedError
273 raise NotImplementedError
273
274
274 def setbranch(self, branch, pbranches):
275 def setbranch(self, branch, pbranches):
275 """Set the current branch name. Called before the first putcommit
276 """Set the current branch name. Called before the first putcommit
276 on the branch.
277 on the branch.
277 branch: branch name for subsequent commits
278 branch: branch name for subsequent commits
278 pbranches: (converted parent revision, parent branch) tuples"""
279 pbranches: (converted parent revision, parent branch) tuples"""
279
280
280 def setfilemapmode(self, active):
281 def setfilemapmode(self, active):
281 """Tell the destination that we're using a filemap
282 """Tell the destination that we're using a filemap
282
283
283 Some converter_sources (svn in particular) can claim that a file
284 Some converter_sources (svn in particular) can claim that a file
284 was changed in a revision, even if there was no change. This method
285 was changed in a revision, even if there was no change. This method
285 tells the destination that we're using a filemap and that it should
286 tells the destination that we're using a filemap and that it should
286 filter empty revisions.
287 filter empty revisions.
287 """
288 """
288
289
289 def before(self):
290 def before(self):
290 pass
291 pass
291
292
292 def after(self):
293 def after(self):
293 pass
294 pass
294
295
295 def putbookmarks(self, bookmarks):
296 def putbookmarks(self, bookmarks):
296 """Put bookmarks into sink.
297 """Put bookmarks into sink.
297
298
298 bookmarks: {bookmarkname: sink_rev_id, ...}
299 bookmarks: {bookmarkname: sink_rev_id, ...}
299 where bookmarkname is an UTF-8 string.
300 where bookmarkname is an UTF-8 string.
300 """
301 """
301
302
302 def hascommitfrommap(self, rev):
303 def hascommitfrommap(self, rev):
303 """Return False if a rev mentioned in a filemap is known to not be
304 """Return False if a rev mentioned in a filemap is known to not be
304 present."""
305 present."""
305 raise NotImplementedError
306 raise NotImplementedError
306
307
307 def hascommitforsplicemap(self, rev):
308 def hascommitforsplicemap(self, rev):
308 """This method is for the special needs for splicemap handling and not
309 """This method is for the special needs for splicemap handling and not
309 for general use. Returns True if the sink contains rev, aborts on some
310 for general use. Returns True if the sink contains rev, aborts on some
310 special cases."""
311 special cases."""
311 raise NotImplementedError
312 raise NotImplementedError
312
313
313 class commandline(object):
314 class commandline(object):
314 def __init__(self, ui, command):
315 def __init__(self, ui, command):
315 self.ui = ui
316 self.ui = ui
316 self.command = command
317 self.command = command
317
318
318 def prerun(self):
319 def prerun(self):
319 pass
320 pass
320
321
321 def postrun(self):
322 def postrun(self):
322 pass
323 pass
323
324
324 def _cmdline(self, cmd, *args, **kwargs):
325 def _cmdline(self, cmd, *args, **kwargs):
326 kwargs = pycompat.byteskwargs(kwargs)
325 cmdline = [self.command, cmd] + list(args)
327 cmdline = [self.command, cmd] + list(args)
326 for k, v in kwargs.iteritems():
328 for k, v in kwargs.iteritems():
327 if len(k) == 1:
329 if len(k) == 1:
328 cmdline.append('-' + k)
330 cmdline.append('-' + k)
329 else:
331 else:
330 cmdline.append('--' + k.replace('_', '-'))
332 cmdline.append('--' + k.replace('_', '-'))
331 try:
333 try:
332 if len(k) == 1:
334 if len(k) == 1:
333 cmdline.append('' + v)
335 cmdline.append('' + v)
334 else:
336 else:
335 cmdline[-1] += '=' + v
337 cmdline[-1] += '=' + v
336 except TypeError:
338 except TypeError:
337 pass
339 pass
338 cmdline = [util.shellquote(arg) for arg in cmdline]
340 cmdline = [util.shellquote(arg) for arg in cmdline]
339 if not self.ui.debugflag:
341 if not self.ui.debugflag:
340 cmdline += ['2>', os.devnull]
342 cmdline += ['2>', os.devnull]
341 cmdline = ' '.join(cmdline)
343 cmdline = ' '.join(cmdline)
342 return cmdline
344 return cmdline
343
345
344 def _run(self, cmd, *args, **kwargs):
346 def _run(self, cmd, *args, **kwargs):
345 def popen(cmdline):
347 def popen(cmdline):
346 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
348 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
347 close_fds=util.closefds,
349 close_fds=util.closefds,
348 stdout=subprocess.PIPE)
350 stdout=subprocess.PIPE)
349 return p
351 return p
350 return self._dorun(popen, cmd, *args, **kwargs)
352 return self._dorun(popen, cmd, *args, **kwargs)
351
353
352 def _run2(self, cmd, *args, **kwargs):
354 def _run2(self, cmd, *args, **kwargs):
353 return self._dorun(util.popen2, cmd, *args, **kwargs)
355 return self._dorun(util.popen2, cmd, *args, **kwargs)
354
356
355 def _run3(self, cmd, *args, **kwargs):
357 def _run3(self, cmd, *args, **kwargs):
356 return self._dorun(util.popen3, cmd, *args, **kwargs)
358 return self._dorun(util.popen3, cmd, *args, **kwargs)
357
359
358 def _dorun(self, openfunc, cmd, *args, **kwargs):
360 def _dorun(self, openfunc, cmd, *args, **kwargs):
359 cmdline = self._cmdline(cmd, *args, **kwargs)
361 cmdline = self._cmdline(cmd, *args, **kwargs)
360 self.ui.debug('running: %s\n' % (cmdline,))
362 self.ui.debug('running: %s\n' % (cmdline,))
361 self.prerun()
363 self.prerun()
362 try:
364 try:
363 return openfunc(cmdline)
365 return openfunc(cmdline)
364 finally:
366 finally:
365 self.postrun()
367 self.postrun()
366
368
367 def run(self, cmd, *args, **kwargs):
369 def run(self, cmd, *args, **kwargs):
368 p = self._run(cmd, *args, **kwargs)
370 p = self._run(cmd, *args, **kwargs)
369 output = p.communicate()[0]
371 output = p.communicate()[0]
370 self.ui.debug(output)
372 self.ui.debug(output)
371 return output, p.returncode
373 return output, p.returncode
372
374
373 def runlines(self, cmd, *args, **kwargs):
375 def runlines(self, cmd, *args, **kwargs):
374 p = self._run(cmd, *args, **kwargs)
376 p = self._run(cmd, *args, **kwargs)
375 output = p.stdout.readlines()
377 output = p.stdout.readlines()
376 p.wait()
378 p.wait()
377 self.ui.debug(''.join(output))
379 self.ui.debug(''.join(output))
378 return output, p.returncode
380 return output, p.returncode
379
381
380 def checkexit(self, status, output=''):
382 def checkexit(self, status, output=''):
381 if status:
383 if status:
382 if output:
384 if output:
383 self.ui.warn(_('%s error:\n') % self.command)
385 self.ui.warn(_('%s error:\n') % self.command)
384 self.ui.warn(output)
386 self.ui.warn(output)
385 msg = util.explainexit(status)[0]
387 msg = util.explainexit(status)[0]
386 raise error.Abort('%s %s' % (self.command, msg))
388 raise error.Abort('%s %s' % (self.command, msg))
387
389
388 def run0(self, cmd, *args, **kwargs):
390 def run0(self, cmd, *args, **kwargs):
389 output, status = self.run(cmd, *args, **kwargs)
391 output, status = self.run(cmd, *args, **kwargs)
390 self.checkexit(status, output)
392 self.checkexit(status, output)
391 return output
393 return output
392
394
393 def runlines0(self, cmd, *args, **kwargs):
395 def runlines0(self, cmd, *args, **kwargs):
394 output, status = self.runlines(cmd, *args, **kwargs)
396 output, status = self.runlines(cmd, *args, **kwargs)
395 self.checkexit(status, ''.join(output))
397 self.checkexit(status, ''.join(output))
396 return output
398 return output
397
399
398 @propertycache
400 @propertycache
399 def argmax(self):
401 def argmax(self):
400 # POSIX requires at least 4096 bytes for ARG_MAX
402 # POSIX requires at least 4096 bytes for ARG_MAX
401 argmax = 4096
403 argmax = 4096
402 try:
404 try:
403 argmax = os.sysconf("SC_ARG_MAX")
405 argmax = os.sysconf("SC_ARG_MAX")
404 except (AttributeError, ValueError):
406 except (AttributeError, ValueError):
405 pass
407 pass
406
408
407 # Windows shells impose their own limits on command line length,
409 # Windows shells impose their own limits on command line length,
408 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
410 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
409 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
411 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
410 # details about cmd.exe limitations.
412 # details about cmd.exe limitations.
411
413
412 # Since ARG_MAX is for command line _and_ environment, lower our limit
414 # Since ARG_MAX is for command line _and_ environment, lower our limit
413 # (and make happy Windows shells while doing this).
415 # (and make happy Windows shells while doing this).
414 return argmax // 2 - 1
416 return argmax // 2 - 1
415
417
416 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
418 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
417 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
419 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
418 limit = self.argmax - cmdlen
420 limit = self.argmax - cmdlen
419 bytes = 0
421 bytes = 0
420 fl = []
422 fl = []
421 for fn in arglist:
423 for fn in arglist:
422 b = len(fn) + 3
424 b = len(fn) + 3
423 if bytes + b < limit or len(fl) == 0:
425 if bytes + b < limit or len(fl) == 0:
424 fl.append(fn)
426 fl.append(fn)
425 bytes += b
427 bytes += b
426 else:
428 else:
427 yield fl
429 yield fl
428 fl = [fn]
430 fl = [fn]
429 bytes = b
431 bytes = b
430 if fl:
432 if fl:
431 yield fl
433 yield fl
432
434
433 def xargs(self, arglist, cmd, *args, **kwargs):
435 def xargs(self, arglist, cmd, *args, **kwargs):
434 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
436 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
435 self.run0(cmd, *(list(args) + l), **kwargs)
437 self.run0(cmd, *(list(args) + l), **kwargs)
436
438
437 class mapfile(dict):
439 class mapfile(dict):
438 def __init__(self, ui, path):
440 def __init__(self, ui, path):
439 super(mapfile, self).__init__()
441 super(mapfile, self).__init__()
440 self.ui = ui
442 self.ui = ui
441 self.path = path
443 self.path = path
442 self.fp = None
444 self.fp = None
443 self.order = []
445 self.order = []
444 self._read()
446 self._read()
445
447
446 def _read(self):
448 def _read(self):
447 if not self.path:
449 if not self.path:
448 return
450 return
449 try:
451 try:
450 fp = open(self.path, 'rb')
452 fp = open(self.path, 'rb')
451 except IOError as err:
453 except IOError as err:
452 if err.errno != errno.ENOENT:
454 if err.errno != errno.ENOENT:
453 raise
455 raise
454 return
456 return
455 for i, line in enumerate(util.iterfile(fp)):
457 for i, line in enumerate(util.iterfile(fp)):
456 line = line.splitlines()[0].rstrip()
458 line = line.splitlines()[0].rstrip()
457 if not line:
459 if not line:
458 # Ignore blank lines
460 # Ignore blank lines
459 continue
461 continue
460 try:
462 try:
461 key, value = line.rsplit(' ', 1)
463 key, value = line.rsplit(' ', 1)
462 except ValueError:
464 except ValueError:
463 raise error.Abort(
465 raise error.Abort(
464 _('syntax error in %s(%d): key/value pair expected')
466 _('syntax error in %s(%d): key/value pair expected')
465 % (self.path, i + 1))
467 % (self.path, i + 1))
466 if key not in self:
468 if key not in self:
467 self.order.append(key)
469 self.order.append(key)
468 super(mapfile, self).__setitem__(key, value)
470 super(mapfile, self).__setitem__(key, value)
469 fp.close()
471 fp.close()
470
472
471 def __setitem__(self, key, value):
473 def __setitem__(self, key, value):
472 if self.fp is None:
474 if self.fp is None:
473 try:
475 try:
474 self.fp = open(self.path, 'ab')
476 self.fp = open(self.path, 'ab')
475 except IOError as err:
477 except IOError as err:
476 raise error.Abort(
478 raise error.Abort(
477 _('could not open map file %r: %s') %
479 _('could not open map file %r: %s') %
478 (self.path, encoding.strtolocal(err.strerror)))
480 (self.path, encoding.strtolocal(err.strerror)))
479 self.fp.write(util.tonativeeol('%s %s\n' % (key, value)))
481 self.fp.write(util.tonativeeol('%s %s\n' % (key, value)))
480 self.fp.flush()
482 self.fp.flush()
481 super(mapfile, self).__setitem__(key, value)
483 super(mapfile, self).__setitem__(key, value)
482
484
483 def close(self):
485 def close(self):
484 if self.fp:
486 if self.fp:
485 self.fp.close()
487 self.fp.close()
486 self.fp = None
488 self.fp = None
487
489
488 def makedatetimestamp(t):
490 def makedatetimestamp(t):
489 """Like util.makedate() but for time t instead of current time"""
491 """Like util.makedate() but for time t instead of current time"""
490 delta = (datetime.datetime.utcfromtimestamp(t) -
492 delta = (datetime.datetime.utcfromtimestamp(t) -
491 datetime.datetime.fromtimestamp(t))
493 datetime.datetime.fromtimestamp(t))
492 tz = delta.days * 86400 + delta.seconds
494 tz = delta.days * 86400 + delta.seconds
493 return t, tz
495 return t, tz
@@ -1,617 +1,618 b''
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
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 of the
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.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import collections
9 import collections
10 import os
10 import os
11 import shlex
11 import shlex
12 import shutil
12 import shutil
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import (
15 from mercurial import (
16 encoding,
16 encoding,
17 error,
17 error,
18 hg,
18 hg,
19 pycompat,
19 pycompat,
20 scmutil,
20 scmutil,
21 util,
21 util,
22 )
22 )
23
23
24 from . import (
24 from . import (
25 bzr,
25 bzr,
26 common,
26 common,
27 cvs,
27 cvs,
28 darcs,
28 darcs,
29 filemap,
29 filemap,
30 git,
30 git,
31 gnuarch,
31 gnuarch,
32 hg as hgconvert,
32 hg as hgconvert,
33 monotone,
33 monotone,
34 p4,
34 p4,
35 subversion,
35 subversion,
36 )
36 )
37
37
38 mapfile = common.mapfile
38 mapfile = common.mapfile
39 MissingTool = common.MissingTool
39 MissingTool = common.MissingTool
40 NoRepo = common.NoRepo
40 NoRepo = common.NoRepo
41 SKIPREV = common.SKIPREV
41 SKIPREV = common.SKIPREV
42
42
43 bzr_source = bzr.bzr_source
43 bzr_source = bzr.bzr_source
44 convert_cvs = cvs.convert_cvs
44 convert_cvs = cvs.convert_cvs
45 convert_git = git.convert_git
45 convert_git = git.convert_git
46 darcs_source = darcs.darcs_source
46 darcs_source = darcs.darcs_source
47 gnuarch_source = gnuarch.gnuarch_source
47 gnuarch_source = gnuarch.gnuarch_source
48 mercurial_sink = hgconvert.mercurial_sink
48 mercurial_sink = hgconvert.mercurial_sink
49 mercurial_source = hgconvert.mercurial_source
49 mercurial_source = hgconvert.mercurial_source
50 monotone_source = monotone.monotone_source
50 monotone_source = monotone.monotone_source
51 p4_source = p4.p4_source
51 p4_source = p4.p4_source
52 svn_sink = subversion.svn_sink
52 svn_sink = subversion.svn_sink
53 svn_source = subversion.svn_source
53 svn_source = subversion.svn_source
54
54
55 orig_encoding = 'ascii'
55 orig_encoding = 'ascii'
56
56
57 def recode(s):
57 def recode(s):
58 if isinstance(s, unicode):
58 if isinstance(s, unicode):
59 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
59 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
60 else:
60 else:
61 return s.decode('utf-8').encode(
61 return s.decode('utf-8').encode(
62 pycompat.sysstr(orig_encoding), 'replace')
62 pycompat.sysstr(orig_encoding), 'replace')
63
63
64 def mapbranch(branch, branchmap):
64 def mapbranch(branch, branchmap):
65 '''
65 '''
66 >>> bmap = {b'default': b'branch1'}
66 >>> bmap = {b'default': b'branch1'}
67 >>> for i in [b'', None]:
67 >>> for i in [b'', None]:
68 ... mapbranch(i, bmap)
68 ... mapbranch(i, bmap)
69 'branch1'
69 'branch1'
70 'branch1'
70 'branch1'
71 >>> bmap = {b'None': b'branch2'}
71 >>> bmap = {b'None': b'branch2'}
72 >>> for i in [b'', None]:
72 >>> for i in [b'', None]:
73 ... mapbranch(i, bmap)
73 ... mapbranch(i, bmap)
74 'branch2'
74 'branch2'
75 'branch2'
75 'branch2'
76 >>> bmap = {b'None': b'branch3', b'default': b'branch4'}
76 >>> bmap = {b'None': b'branch3', b'default': b'branch4'}
77 >>> for i in [b'None', b'', None, b'default', b'branch5']:
77 >>> for i in [b'None', b'', None, b'default', b'branch5']:
78 ... mapbranch(i, bmap)
78 ... mapbranch(i, bmap)
79 'branch3'
79 'branch3'
80 'branch4'
80 'branch4'
81 'branch4'
81 'branch4'
82 'branch4'
82 'branch4'
83 'branch5'
83 'branch5'
84 '''
84 '''
85 # If branch is None or empty, this commit is coming from the source
85 # If branch is None or empty, this commit is coming from the source
86 # repository's default branch and destined for the default branch in the
86 # repository's default branch and destined for the default branch in the
87 # destination repository. For such commits, using a literal "default"
87 # destination repository. For such commits, using a literal "default"
88 # in branchmap below allows the user to map "default" to an alternate
88 # in branchmap below allows the user to map "default" to an alternate
89 # default branch in the destination repository.
89 # default branch in the destination repository.
90 branch = branchmap.get(branch or 'default', branch)
90 branch = branchmap.get(branch or 'default', branch)
91 # At some point we used "None" literal to denote the default branch,
91 # At some point we used "None" literal to denote the default branch,
92 # attempt to use that for backward compatibility.
92 # attempt to use that for backward compatibility.
93 if (not branch):
93 if (not branch):
94 branch = branchmap.get('None', branch)
94 branch = branchmap.get('None', branch)
95 return branch
95 return branch
96
96
97 source_converters = [
97 source_converters = [
98 ('cvs', convert_cvs, 'branchsort'),
98 ('cvs', convert_cvs, 'branchsort'),
99 ('git', convert_git, 'branchsort'),
99 ('git', convert_git, 'branchsort'),
100 ('svn', svn_source, 'branchsort'),
100 ('svn', svn_source, 'branchsort'),
101 ('hg', mercurial_source, 'sourcesort'),
101 ('hg', mercurial_source, 'sourcesort'),
102 ('darcs', darcs_source, 'branchsort'),
102 ('darcs', darcs_source, 'branchsort'),
103 ('mtn', monotone_source, 'branchsort'),
103 ('mtn', monotone_source, 'branchsort'),
104 ('gnuarch', gnuarch_source, 'branchsort'),
104 ('gnuarch', gnuarch_source, 'branchsort'),
105 ('bzr', bzr_source, 'branchsort'),
105 ('bzr', bzr_source, 'branchsort'),
106 ('p4', p4_source, 'branchsort'),
106 ('p4', p4_source, 'branchsort'),
107 ]
107 ]
108
108
109 sink_converters = [
109 sink_converters = [
110 ('hg', mercurial_sink),
110 ('hg', mercurial_sink),
111 ('svn', svn_sink),
111 ('svn', svn_sink),
112 ]
112 ]
113
113
114 def convertsource(ui, path, type, revs):
114 def convertsource(ui, path, type, revs):
115 exceptions = []
115 exceptions = []
116 if type and type not in [s[0] for s in source_converters]:
116 if type and type not in [s[0] for s in source_converters]:
117 raise error.Abort(_('%s: invalid source repository type') % type)
117 raise error.Abort(_('%s: invalid source repository type') % type)
118 for name, source, sortmode in source_converters:
118 for name, source, sortmode in source_converters:
119 try:
119 try:
120 if not type or name == type:
120 if not type or name == type:
121 return source(ui, name, path, revs), sortmode
121 return source(ui, name, path, revs), sortmode
122 except (NoRepo, MissingTool) as inst:
122 except (NoRepo, MissingTool) as inst:
123 exceptions.append(inst)
123 exceptions.append(inst)
124 if not ui.quiet:
124 if not ui.quiet:
125 for inst in exceptions:
125 for inst in exceptions:
126 ui.write("%s\n" % inst)
126 ui.write("%s\n" % inst)
127 raise error.Abort(_('%s: missing or unsupported repository') % path)
127 raise error.Abort(_('%s: missing or unsupported repository') % path)
128
128
129 def convertsink(ui, path, type):
129 def convertsink(ui, path, type):
130 if type and type not in [s[0] for s in sink_converters]:
130 if type and type not in [s[0] for s in sink_converters]:
131 raise error.Abort(_('%s: invalid destination repository type') % type)
131 raise error.Abort(_('%s: invalid destination repository type') % type)
132 for name, sink in sink_converters:
132 for name, sink in sink_converters:
133 try:
133 try:
134 if not type or name == type:
134 if not type or name == type:
135 return sink(ui, name, path)
135 return sink(ui, name, path)
136 except NoRepo as inst:
136 except NoRepo as inst:
137 ui.note(_("convert: %s\n") % inst)
137 ui.note(_("convert: %s\n") % inst)
138 except MissingTool as inst:
138 except MissingTool as inst:
139 raise error.Abort('%s\n' % inst)
139 raise error.Abort('%s\n' % inst)
140 raise error.Abort(_('%s: unknown repository type') % path)
140 raise error.Abort(_('%s: unknown repository type') % path)
141
141
142 class progresssource(object):
142 class progresssource(object):
143 def __init__(self, ui, source, filecount):
143 def __init__(self, ui, source, filecount):
144 self.ui = ui
144 self.ui = ui
145 self.source = source
145 self.source = source
146 self.filecount = filecount
146 self.filecount = filecount
147 self.retrieved = 0
147 self.retrieved = 0
148
148
149 def getfile(self, file, rev):
149 def getfile(self, file, rev):
150 self.retrieved += 1
150 self.retrieved += 1
151 self.ui.progress(_('getting files'), self.retrieved,
151 self.ui.progress(_('getting files'), self.retrieved,
152 item=file, total=self.filecount, unit=_('files'))
152 item=file, total=self.filecount, unit=_('files'))
153 return self.source.getfile(file, rev)
153 return self.source.getfile(file, rev)
154
154
155 def targetfilebelongstosource(self, targetfilename):
155 def targetfilebelongstosource(self, targetfilename):
156 return self.source.targetfilebelongstosource(targetfilename)
156 return self.source.targetfilebelongstosource(targetfilename)
157
157
158 def lookuprev(self, rev):
158 def lookuprev(self, rev):
159 return self.source.lookuprev(rev)
159 return self.source.lookuprev(rev)
160
160
161 def close(self):
161 def close(self):
162 self.ui.progress(_('getting files'), None)
162 self.ui.progress(_('getting files'), None)
163
163
164 class converter(object):
164 class converter(object):
165 def __init__(self, ui, source, dest, revmapfile, opts):
165 def __init__(self, ui, source, dest, revmapfile, opts):
166
166
167 self.source = source
167 self.source = source
168 self.dest = dest
168 self.dest = dest
169 self.ui = ui
169 self.ui = ui
170 self.opts = opts
170 self.opts = opts
171 self.commitcache = {}
171 self.commitcache = {}
172 self.authors = {}
172 self.authors = {}
173 self.authorfile = None
173 self.authorfile = None
174
174
175 # Record converted revisions persistently: maps source revision
175 # Record converted revisions persistently: maps source revision
176 # ID to target revision ID (both strings). (This is how
176 # ID to target revision ID (both strings). (This is how
177 # incremental conversions work.)
177 # incremental conversions work.)
178 self.map = mapfile(ui, revmapfile)
178 self.map = mapfile(ui, revmapfile)
179
179
180 # Read first the dst author map if any
180 # Read first the dst author map if any
181 authorfile = self.dest.authorfile()
181 authorfile = self.dest.authorfile()
182 if authorfile and os.path.exists(authorfile):
182 if authorfile and os.path.exists(authorfile):
183 self.readauthormap(authorfile)
183 self.readauthormap(authorfile)
184 # Extend/Override with new author map if necessary
184 # Extend/Override with new author map if necessary
185 if opts.get('authormap'):
185 if opts.get('authormap'):
186 self.readauthormap(opts.get('authormap'))
186 self.readauthormap(opts.get('authormap'))
187 self.authorfile = self.dest.authorfile()
187 self.authorfile = self.dest.authorfile()
188
188
189 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
189 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
190 self.branchmap = mapfile(ui, opts.get('branchmap'))
190 self.branchmap = mapfile(ui, opts.get('branchmap'))
191
191
192 def parsesplicemap(self, path):
192 def parsesplicemap(self, path):
193 """ check and validate the splicemap format and
193 """ check and validate the splicemap format and
194 return a child/parents dictionary.
194 return a child/parents dictionary.
195 Format checking has two parts.
195 Format checking has two parts.
196 1. generic format which is same across all source types
196 1. generic format which is same across all source types
197 2. specific format checking which may be different for
197 2. specific format checking which may be different for
198 different source type. This logic is implemented in
198 different source type. This logic is implemented in
199 checkrevformat function in source files like
199 checkrevformat function in source files like
200 hg.py, subversion.py etc.
200 hg.py, subversion.py etc.
201 """
201 """
202
202
203 if not path:
203 if not path:
204 return {}
204 return {}
205 m = {}
205 m = {}
206 try:
206 try:
207 fp = open(path, 'rb')
207 fp = open(path, 'rb')
208 for i, line in enumerate(util.iterfile(fp)):
208 for i, line in enumerate(util.iterfile(fp)):
209 line = line.splitlines()[0].rstrip()
209 line = line.splitlines()[0].rstrip()
210 if not line:
210 if not line:
211 # Ignore blank lines
211 # Ignore blank lines
212 continue
212 continue
213 # split line
213 # split line
214 lex = shlex.shlex(line, posix=True)
214 lex = shlex.shlex(line, posix=True)
215 lex.whitespace_split = True
215 lex.whitespace_split = True
216 lex.whitespace += ','
216 lex.whitespace += ','
217 line = list(lex)
217 line = list(lex)
218 # check number of parents
218 # check number of parents
219 if not (2 <= len(line) <= 3):
219 if not (2 <= len(line) <= 3):
220 raise error.Abort(_('syntax error in %s(%d): child parent1'
220 raise error.Abort(_('syntax error in %s(%d): child parent1'
221 '[,parent2] expected') % (path, i + 1))
221 '[,parent2] expected') % (path, i + 1))
222 for part in line:
222 for part in line:
223 self.source.checkrevformat(part)
223 self.source.checkrevformat(part)
224 child, p1, p2 = line[0], line[1:2], line[2:]
224 child, p1, p2 = line[0], line[1:2], line[2:]
225 if p1 == p2:
225 if p1 == p2:
226 m[child] = p1
226 m[child] = p1
227 else:
227 else:
228 m[child] = p1 + p2
228 m[child] = p1 + p2
229 # if file does not exist or error reading, exit
229 # if file does not exist or error reading, exit
230 except IOError:
230 except IOError:
231 raise error.Abort(_('splicemap file not found or error reading %s:')
231 raise error.Abort(_('splicemap file not found or error reading %s:')
232 % path)
232 % path)
233 return m
233 return m
234
234
235
235
236 def walktree(self, heads):
236 def walktree(self, heads):
237 '''Return a mapping that identifies the uncommitted parents of every
237 '''Return a mapping that identifies the uncommitted parents of every
238 uncommitted changeset.'''
238 uncommitted changeset.'''
239 visit = heads
239 visit = heads
240 known = set()
240 known = set()
241 parents = {}
241 parents = {}
242 numcommits = self.source.numcommits()
242 numcommits = self.source.numcommits()
243 while visit:
243 while visit:
244 n = visit.pop(0)
244 n = visit.pop(0)
245 if n in known:
245 if n in known:
246 continue
246 continue
247 if n in self.map:
247 if n in self.map:
248 m = self.map[n]
248 m = self.map[n]
249 if m == SKIPREV or self.dest.hascommitfrommap(m):
249 if m == SKIPREV or self.dest.hascommitfrommap(m):
250 continue
250 continue
251 known.add(n)
251 known.add(n)
252 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
252 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
253 total=numcommits)
253 total=numcommits)
254 commit = self.cachecommit(n)
254 commit = self.cachecommit(n)
255 parents[n] = []
255 parents[n] = []
256 for p in commit.parents:
256 for p in commit.parents:
257 parents[n].append(p)
257 parents[n].append(p)
258 visit.append(p)
258 visit.append(p)
259 self.ui.progress(_('scanning'), None)
259 self.ui.progress(_('scanning'), None)
260
260
261 return parents
261 return parents
262
262
263 def mergesplicemap(self, parents, splicemap):
263 def mergesplicemap(self, parents, splicemap):
264 """A splicemap redefines child/parent relationships. Check the
264 """A splicemap redefines child/parent relationships. Check the
265 map contains valid revision identifiers and merge the new
265 map contains valid revision identifiers and merge the new
266 links in the source graph.
266 links in the source graph.
267 """
267 """
268 for c in sorted(splicemap):
268 for c in sorted(splicemap):
269 if c not in parents:
269 if c not in parents:
270 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
270 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
271 # Could be in source but not converted during this run
271 # Could be in source but not converted during this run
272 self.ui.warn(_('splice map revision %s is not being '
272 self.ui.warn(_('splice map revision %s is not being '
273 'converted, ignoring\n') % c)
273 'converted, ignoring\n') % c)
274 continue
274 continue
275 pc = []
275 pc = []
276 for p in splicemap[c]:
276 for p in splicemap[c]:
277 # We do not have to wait for nodes already in dest.
277 # We do not have to wait for nodes already in dest.
278 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
278 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
279 continue
279 continue
280 # Parent is not in dest and not being converted, not good
280 # Parent is not in dest and not being converted, not good
281 if p not in parents:
281 if p not in parents:
282 raise error.Abort(_('unknown splice map parent: %s') % p)
282 raise error.Abort(_('unknown splice map parent: %s') % p)
283 pc.append(p)
283 pc.append(p)
284 parents[c] = pc
284 parents[c] = pc
285
285
286 def toposort(self, parents, sortmode):
286 def toposort(self, parents, sortmode):
287 '''Return an ordering such that every uncommitted changeset is
287 '''Return an ordering such that every uncommitted changeset is
288 preceded by all its uncommitted ancestors.'''
288 preceded by all its uncommitted ancestors.'''
289
289
290 def mapchildren(parents):
290 def mapchildren(parents):
291 """Return a (children, roots) tuple where 'children' maps parent
291 """Return a (children, roots) tuple where 'children' maps parent
292 revision identifiers to children ones, and 'roots' is the list of
292 revision identifiers to children ones, and 'roots' is the list of
293 revisions without parents. 'parents' must be a mapping of revision
293 revisions without parents. 'parents' must be a mapping of revision
294 identifier to its parents ones.
294 identifier to its parents ones.
295 """
295 """
296 visit = collections.deque(sorted(parents))
296 visit = collections.deque(sorted(parents))
297 seen = set()
297 seen = set()
298 children = {}
298 children = {}
299 roots = []
299 roots = []
300
300
301 while visit:
301 while visit:
302 n = visit.popleft()
302 n = visit.popleft()
303 if n in seen:
303 if n in seen:
304 continue
304 continue
305 seen.add(n)
305 seen.add(n)
306 # Ensure that nodes without parents are present in the
306 # Ensure that nodes without parents are present in the
307 # 'children' mapping.
307 # 'children' mapping.
308 children.setdefault(n, [])
308 children.setdefault(n, [])
309 hasparent = False
309 hasparent = False
310 for p in parents[n]:
310 for p in parents[n]:
311 if p not in self.map:
311 if p not in self.map:
312 visit.append(p)
312 visit.append(p)
313 hasparent = True
313 hasparent = True
314 children.setdefault(p, []).append(n)
314 children.setdefault(p, []).append(n)
315 if not hasparent:
315 if not hasparent:
316 roots.append(n)
316 roots.append(n)
317
317
318 return children, roots
318 return children, roots
319
319
320 # Sort functions are supposed to take a list of revisions which
320 # Sort functions are supposed to take a list of revisions which
321 # can be converted immediately and pick one
321 # can be converted immediately and pick one
322
322
323 def makebranchsorter():
323 def makebranchsorter():
324 """If the previously converted revision has a child in the
324 """If the previously converted revision has a child in the
325 eligible revisions list, pick it. Return the list head
325 eligible revisions list, pick it. Return the list head
326 otherwise. Branch sort attempts to minimize branch
326 otherwise. Branch sort attempts to minimize branch
327 switching, which is harmful for Mercurial backend
327 switching, which is harmful for Mercurial backend
328 compression.
328 compression.
329 """
329 """
330 prev = [None]
330 prev = [None]
331 def picknext(nodes):
331 def picknext(nodes):
332 next = nodes[0]
332 next = nodes[0]
333 for n in nodes:
333 for n in nodes:
334 if prev[0] in parents[n]:
334 if prev[0] in parents[n]:
335 next = n
335 next = n
336 break
336 break
337 prev[0] = next
337 prev[0] = next
338 return next
338 return next
339 return picknext
339 return picknext
340
340
341 def makesourcesorter():
341 def makesourcesorter():
342 """Source specific sort."""
342 """Source specific sort."""
343 keyfn = lambda n: self.commitcache[n].sortkey
343 keyfn = lambda n: self.commitcache[n].sortkey
344 def picknext(nodes):
344 def picknext(nodes):
345 return sorted(nodes, key=keyfn)[0]
345 return sorted(nodes, key=keyfn)[0]
346 return picknext
346 return picknext
347
347
348 def makeclosesorter():
348 def makeclosesorter():
349 """Close order sort."""
349 """Close order sort."""
350 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
350 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
351 self.commitcache[n].sortkey)
351 self.commitcache[n].sortkey)
352 def picknext(nodes):
352 def picknext(nodes):
353 return sorted(nodes, key=keyfn)[0]
353 return sorted(nodes, key=keyfn)[0]
354 return picknext
354 return picknext
355
355
356 def makedatesorter():
356 def makedatesorter():
357 """Sort revisions by date."""
357 """Sort revisions by date."""
358 dates = {}
358 dates = {}
359 def getdate(n):
359 def getdate(n):
360 if n not in dates:
360 if n not in dates:
361 dates[n] = util.parsedate(self.commitcache[n].date)
361 dates[n] = util.parsedate(self.commitcache[n].date)
362 return dates[n]
362 return dates[n]
363
363
364 def picknext(nodes):
364 def picknext(nodes):
365 return min([(getdate(n), n) for n in nodes])[1]
365 return min([(getdate(n), n) for n in nodes])[1]
366
366
367 return picknext
367 return picknext
368
368
369 if sortmode == 'branchsort':
369 if sortmode == 'branchsort':
370 picknext = makebranchsorter()
370 picknext = makebranchsorter()
371 elif sortmode == 'datesort':
371 elif sortmode == 'datesort':
372 picknext = makedatesorter()
372 picknext = makedatesorter()
373 elif sortmode == 'sourcesort':
373 elif sortmode == 'sourcesort':
374 picknext = makesourcesorter()
374 picknext = makesourcesorter()
375 elif sortmode == 'closesort':
375 elif sortmode == 'closesort':
376 picknext = makeclosesorter()
376 picknext = makeclosesorter()
377 else:
377 else:
378 raise error.Abort(_('unknown sort mode: %s') % sortmode)
378 raise error.Abort(_('unknown sort mode: %s') % sortmode)
379
379
380 children, actives = mapchildren(parents)
380 children, actives = mapchildren(parents)
381
381
382 s = []
382 s = []
383 pendings = {}
383 pendings = {}
384 while actives:
384 while actives:
385 n = picknext(actives)
385 n = picknext(actives)
386 actives.remove(n)
386 actives.remove(n)
387 s.append(n)
387 s.append(n)
388
388
389 # Update dependents list
389 # Update dependents list
390 for c in children.get(n, []):
390 for c in children.get(n, []):
391 if c not in pendings:
391 if c not in pendings:
392 pendings[c] = [p for p in parents[c] if p not in self.map]
392 pendings[c] = [p for p in parents[c] if p not in self.map]
393 try:
393 try:
394 pendings[c].remove(n)
394 pendings[c].remove(n)
395 except ValueError:
395 except ValueError:
396 raise error.Abort(_('cycle detected between %s and %s')
396 raise error.Abort(_('cycle detected between %s and %s')
397 % (recode(c), recode(n)))
397 % (recode(c), recode(n)))
398 if not pendings[c]:
398 if not pendings[c]:
399 # Parents are converted, node is eligible
399 # Parents are converted, node is eligible
400 actives.insert(0, c)
400 actives.insert(0, c)
401 pendings[c] = None
401 pendings[c] = None
402
402
403 if len(s) != len(parents):
403 if len(s) != len(parents):
404 raise error.Abort(_("not all revisions were sorted"))
404 raise error.Abort(_("not all revisions were sorted"))
405
405
406 return s
406 return s
407
407
408 def writeauthormap(self):
408 def writeauthormap(self):
409 authorfile = self.authorfile
409 authorfile = self.authorfile
410 if authorfile:
410 if authorfile:
411 self.ui.status(_('writing author map file %s\n') % authorfile)
411 self.ui.status(_('writing author map file %s\n') % authorfile)
412 ofile = open(authorfile, 'wb+')
412 ofile = open(authorfile, 'wb+')
413 for author in self.authors:
413 for author in self.authors:
414 ofile.write(util.tonativeeol("%s=%s\n"
414 ofile.write(util.tonativeeol("%s=%s\n"
415 % (author, self.authors[author])))
415 % (author, self.authors[author])))
416 ofile.close()
416 ofile.close()
417
417
418 def readauthormap(self, authorfile):
418 def readauthormap(self, authorfile):
419 afile = open(authorfile, 'rb')
419 afile = open(authorfile, 'rb')
420 for line in afile:
420 for line in afile:
421
421
422 line = line.strip()
422 line = line.strip()
423 if not line or line.startswith('#'):
423 if not line or line.startswith('#'):
424 continue
424 continue
425
425
426 try:
426 try:
427 srcauthor, dstauthor = line.split('=', 1)
427 srcauthor, dstauthor = line.split('=', 1)
428 except ValueError:
428 except ValueError:
429 msg = _('ignoring bad line in author map file %s: %s\n')
429 msg = _('ignoring bad line in author map file %s: %s\n')
430 self.ui.warn(msg % (authorfile, line.rstrip()))
430 self.ui.warn(msg % (authorfile, line.rstrip()))
431 continue
431 continue
432
432
433 srcauthor = srcauthor.strip()
433 srcauthor = srcauthor.strip()
434 dstauthor = dstauthor.strip()
434 dstauthor = dstauthor.strip()
435 if self.authors.get(srcauthor) in (None, dstauthor):
435 if self.authors.get(srcauthor) in (None, dstauthor):
436 msg = _('mapping author %s to %s\n')
436 msg = _('mapping author %s to %s\n')
437 self.ui.debug(msg % (srcauthor, dstauthor))
437 self.ui.debug(msg % (srcauthor, dstauthor))
438 self.authors[srcauthor] = dstauthor
438 self.authors[srcauthor] = dstauthor
439 continue
439 continue
440
440
441 m = _('overriding mapping for author %s, was %s, will be %s\n')
441 m = _('overriding mapping for author %s, was %s, will be %s\n')
442 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
442 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
443
443
444 afile.close()
444 afile.close()
445
445
446 def cachecommit(self, rev):
446 def cachecommit(self, rev):
447 commit = self.source.getcommit(rev)
447 commit = self.source.getcommit(rev)
448 commit.author = self.authors.get(commit.author, commit.author)
448 commit.author = self.authors.get(commit.author, commit.author)
449 commit.branch = mapbranch(commit.branch, self.branchmap)
449 commit.branch = mapbranch(commit.branch, self.branchmap)
450 self.commitcache[rev] = commit
450 self.commitcache[rev] = commit
451 return commit
451 return commit
452
452
453 def copy(self, rev):
453 def copy(self, rev):
454 commit = self.commitcache[rev]
454 commit = self.commitcache[rev]
455 full = self.opts.get('full')
455 full = self.opts.get('full')
456 changes = self.source.getchanges(rev, full)
456 changes = self.source.getchanges(rev, full)
457 if isinstance(changes, bytes):
457 if isinstance(changes, bytes):
458 if changes == SKIPREV:
458 if changes == SKIPREV:
459 dest = SKIPREV
459 dest = SKIPREV
460 else:
460 else:
461 dest = self.map[changes]
461 dest = self.map[changes]
462 self.map[rev] = dest
462 self.map[rev] = dest
463 return
463 return
464 files, copies, cleanp2 = changes
464 files, copies, cleanp2 = changes
465 pbranches = []
465 pbranches = []
466 if commit.parents:
466 if commit.parents:
467 for prev in commit.parents:
467 for prev in commit.parents:
468 if prev not in self.commitcache:
468 if prev not in self.commitcache:
469 self.cachecommit(prev)
469 self.cachecommit(prev)
470 pbranches.append((self.map[prev],
470 pbranches.append((self.map[prev],
471 self.commitcache[prev].branch))
471 self.commitcache[prev].branch))
472 self.dest.setbranch(commit.branch, pbranches)
472 self.dest.setbranch(commit.branch, pbranches)
473 try:
473 try:
474 parents = self.splicemap[rev]
474 parents = self.splicemap[rev]
475 self.ui.status(_('spliced in %s as parents of %s\n') %
475 self.ui.status(_('spliced in %s as parents of %s\n') %
476 (_(' and ').join(parents), rev))
476 (_(' and ').join(parents), rev))
477 parents = [self.map.get(p, p) for p in parents]
477 parents = [self.map.get(p, p) for p in parents]
478 except KeyError:
478 except KeyError:
479 parents = [b[0] for b in pbranches]
479 parents = [b[0] for b in pbranches]
480 parents.extend(self.map[x]
480 parents.extend(self.map[x]
481 for x in commit.optparents
481 for x in commit.optparents
482 if x in self.map)
482 if x in self.map)
483 if len(pbranches) != 2:
483 if len(pbranches) != 2:
484 cleanp2 = set()
484 cleanp2 = set()
485 if len(parents) < 3:
485 if len(parents) < 3:
486 source = progresssource(self.ui, self.source, len(files))
486 source = progresssource(self.ui, self.source, len(files))
487 else:
487 else:
488 # For an octopus merge, we end up traversing the list of
488 # For an octopus merge, we end up traversing the list of
489 # changed files N-1 times. This tweak to the number of
489 # changed files N-1 times. This tweak to the number of
490 # files makes it so the progress bar doesn't overflow
490 # files makes it so the progress bar doesn't overflow
491 # itself.
491 # itself.
492 source = progresssource(self.ui, self.source,
492 source = progresssource(self.ui, self.source,
493 len(files) * (len(parents) - 1))
493 len(files) * (len(parents) - 1))
494 newnode = self.dest.putcommit(files, copies, parents, commit,
494 newnode = self.dest.putcommit(files, copies, parents, commit,
495 source, self.map, full, cleanp2)
495 source, self.map, full, cleanp2)
496 source.close()
496 source.close()
497 self.source.converted(rev, newnode)
497 self.source.converted(rev, newnode)
498 self.map[rev] = newnode
498 self.map[rev] = newnode
499
499
500 def convert(self, sortmode):
500 def convert(self, sortmode):
501 try:
501 try:
502 self.source.before()
502 self.source.before()
503 self.dest.before()
503 self.dest.before()
504 self.source.setrevmap(self.map)
504 self.source.setrevmap(self.map)
505 self.ui.status(_("scanning source...\n"))
505 self.ui.status(_("scanning source...\n"))
506 heads = self.source.getheads()
506 heads = self.source.getheads()
507 parents = self.walktree(heads)
507 parents = self.walktree(heads)
508 self.mergesplicemap(parents, self.splicemap)
508 self.mergesplicemap(parents, self.splicemap)
509 self.ui.status(_("sorting...\n"))
509 self.ui.status(_("sorting...\n"))
510 t = self.toposort(parents, sortmode)
510 t = self.toposort(parents, sortmode)
511 num = len(t)
511 num = len(t)
512 c = None
512 c = None
513
513
514 self.ui.status(_("converting...\n"))
514 self.ui.status(_("converting...\n"))
515 for i, c in enumerate(t):
515 for i, c in enumerate(t):
516 num -= 1
516 num -= 1
517 desc = self.commitcache[c].desc
517 desc = self.commitcache[c].desc
518 if "\n" in desc:
518 if "\n" in desc:
519 desc = desc.splitlines()[0]
519 desc = desc.splitlines()[0]
520 # convert log message to local encoding without using
520 # convert log message to local encoding without using
521 # tolocal() because the encoding.encoding convert()
521 # tolocal() because the encoding.encoding convert()
522 # uses is 'utf-8'
522 # uses is 'utf-8'
523 self.ui.status("%d %s\n" % (num, recode(desc)))
523 self.ui.status("%d %s\n" % (num, recode(desc)))
524 self.ui.note(_("source: %s\n") % recode(c))
524 self.ui.note(_("source: %s\n") % recode(c))
525 self.ui.progress(_('converting'), i, unit=_('revisions'),
525 self.ui.progress(_('converting'), i, unit=_('revisions'),
526 total=len(t))
526 total=len(t))
527 self.copy(c)
527 self.copy(c)
528 self.ui.progress(_('converting'), None)
528 self.ui.progress(_('converting'), None)
529
529
530 if not self.ui.configbool('convert', 'skiptags'):
530 if not self.ui.configbool('convert', 'skiptags'):
531 tags = self.source.gettags()
531 tags = self.source.gettags()
532 ctags = {}
532 ctags = {}
533 for k in tags:
533 for k in tags:
534 v = tags[k]
534 v = tags[k]
535 if self.map.get(v, SKIPREV) != SKIPREV:
535 if self.map.get(v, SKIPREV) != SKIPREV:
536 ctags[k] = self.map[v]
536 ctags[k] = self.map[v]
537
537
538 if c and ctags:
538 if c and ctags:
539 nrev, tagsparent = self.dest.puttags(ctags)
539 nrev, tagsparent = self.dest.puttags(ctags)
540 if nrev and tagsparent:
540 if nrev and tagsparent:
541 # write another hash correspondence to override the
541 # write another hash correspondence to override the
542 # previous one so we don't end up with extra tag heads
542 # previous one so we don't end up with extra tag heads
543 tagsparents = [e for e in self.map.iteritems()
543 tagsparents = [e for e in self.map.iteritems()
544 if e[1] == tagsparent]
544 if e[1] == tagsparent]
545 if tagsparents:
545 if tagsparents:
546 self.map[tagsparents[0][0]] = nrev
546 self.map[tagsparents[0][0]] = nrev
547
547
548 bookmarks = self.source.getbookmarks()
548 bookmarks = self.source.getbookmarks()
549 cbookmarks = {}
549 cbookmarks = {}
550 for k in bookmarks:
550 for k in bookmarks:
551 v = bookmarks[k]
551 v = bookmarks[k]
552 if self.map.get(v, SKIPREV) != SKIPREV:
552 if self.map.get(v, SKIPREV) != SKIPREV:
553 cbookmarks[k] = self.map[v]
553 cbookmarks[k] = self.map[v]
554
554
555 if c and cbookmarks:
555 if c and cbookmarks:
556 self.dest.putbookmarks(cbookmarks)
556 self.dest.putbookmarks(cbookmarks)
557
557
558 self.writeauthormap()
558 self.writeauthormap()
559 finally:
559 finally:
560 self.cleanup()
560 self.cleanup()
561
561
562 def cleanup(self):
562 def cleanup(self):
563 try:
563 try:
564 self.dest.after()
564 self.dest.after()
565 finally:
565 finally:
566 self.source.after()
566 self.source.after()
567 self.map.close()
567 self.map.close()
568
568
569 def convert(ui, src, dest=None, revmapfile=None, **opts):
569 def convert(ui, src, dest=None, revmapfile=None, **opts):
570 opts = pycompat.byteskwargs(opts)
570 global orig_encoding
571 global orig_encoding
571 orig_encoding = encoding.encoding
572 orig_encoding = encoding.encoding
572 encoding.encoding = 'UTF-8'
573 encoding.encoding = 'UTF-8'
573
574
574 # support --authors as an alias for --authormap
575 # support --authors as an alias for --authormap
575 if not opts.get('authormap'):
576 if not opts.get('authormap'):
576 opts['authormap'] = opts.get('authors')
577 opts['authormap'] = opts.get('authors')
577
578
578 if not dest:
579 if not dest:
579 dest = hg.defaultdest(src) + "-hg"
580 dest = hg.defaultdest(src) + "-hg"
580 ui.status(_("assuming destination %s\n") % dest)
581 ui.status(_("assuming destination %s\n") % dest)
581
582
582 destc = convertsink(ui, dest, opts.get('dest_type'))
583 destc = convertsink(ui, dest, opts.get('dest_type'))
583 destc = scmutil.wrapconvertsink(destc)
584 destc = scmutil.wrapconvertsink(destc)
584
585
585 try:
586 try:
586 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
587 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
587 opts.get('rev'))
588 opts.get('rev'))
588 except Exception:
589 except Exception:
589 for path in destc.created:
590 for path in destc.created:
590 shutil.rmtree(path, True)
591 shutil.rmtree(path, True)
591 raise
592 raise
592
593
593 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
594 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
594 sortmode = [m for m in sortmodes if opts.get(m)]
595 sortmode = [m for m in sortmodes if opts.get(m)]
595 if len(sortmode) > 1:
596 if len(sortmode) > 1:
596 raise error.Abort(_('more than one sort mode specified'))
597 raise error.Abort(_('more than one sort mode specified'))
597 if sortmode:
598 if sortmode:
598 sortmode = sortmode[0]
599 sortmode = sortmode[0]
599 else:
600 else:
600 sortmode = defaultsort
601 sortmode = defaultsort
601
602
602 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
603 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
603 raise error.Abort(_('--sourcesort is not supported by this data source')
604 raise error.Abort(_('--sourcesort is not supported by this data source')
604 )
605 )
605 if sortmode == 'closesort' and not srcc.hasnativeclose():
606 if sortmode == 'closesort' and not srcc.hasnativeclose():
606 raise error.Abort(_('--closesort is not supported by this data source'))
607 raise error.Abort(_('--closesort is not supported by this data source'))
607
608
608 fmap = opts.get('filemap')
609 fmap = opts.get('filemap')
609 if fmap:
610 if fmap:
610 srcc = filemap.filemap_source(ui, srcc, fmap)
611 srcc = filemap.filemap_source(ui, srcc, fmap)
611 destc.setfilemapmode(True)
612 destc.setfilemapmode(True)
612
613
613 if not revmapfile:
614 if not revmapfile:
614 revmapfile = destc.revmapfile()
615 revmapfile = destc.revmapfile()
615
616
616 c = converter(ui, srcc, destc, revmapfile, opts)
617 c = converter(ui, srcc, destc, revmapfile, opts)
617 c.convert(sortmode)
618 c.convert(sortmode)
@@ -1,951 +1,952 b''
1 # Mercurial built-in replacement for cvsps.
1 # Mercurial built-in replacement for cvsps.
2 #
2 #
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10 import re
10 import re
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 encoding,
14 encoding,
15 error,
15 error,
16 hook,
16 hook,
17 pycompat,
17 pycompat,
18 util,
18 util,
19 )
19 )
20
20
21 pickle = util.pickle
21 pickle = util.pickle
22
22
23 class logentry(object):
23 class logentry(object):
24 '''Class logentry has the following attributes:
24 '''Class logentry has the following attributes:
25 .author - author name as CVS knows it
25 .author - author name as CVS knows it
26 .branch - name of branch this revision is on
26 .branch - name of branch this revision is on
27 .branches - revision tuple of branches starting at this revision
27 .branches - revision tuple of branches starting at this revision
28 .comment - commit message
28 .comment - commit message
29 .commitid - CVS commitid or None
29 .commitid - CVS commitid or None
30 .date - the commit date as a (time, tz) tuple
30 .date - the commit date as a (time, tz) tuple
31 .dead - true if file revision is dead
31 .dead - true if file revision is dead
32 .file - Name of file
32 .file - Name of file
33 .lines - a tuple (+lines, -lines) or None
33 .lines - a tuple (+lines, -lines) or None
34 .parent - Previous revision of this entry
34 .parent - Previous revision of this entry
35 .rcs - name of file as returned from CVS
35 .rcs - name of file as returned from CVS
36 .revision - revision number as tuple
36 .revision - revision number as tuple
37 .tags - list of tags on the file
37 .tags - list of tags on the file
38 .synthetic - is this a synthetic "file ... added on ..." revision?
38 .synthetic - is this a synthetic "file ... added on ..." revision?
39 .mergepoint - the branch that has been merged from (if present in
39 .mergepoint - the branch that has been merged from (if present in
40 rlog output) or None
40 rlog output) or None
41 .branchpoints - the branches that start at the current entry or empty
41 .branchpoints - the branches that start at the current entry or empty
42 '''
42 '''
43 def __init__(self, **entries):
43 def __init__(self, **entries):
44 self.synthetic = False
44 self.synthetic = False
45 self.__dict__.update(entries)
45 self.__dict__.update(entries)
46
46
47 def __repr__(self):
47 def __repr__(self):
48 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
48 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
49 return "%s(%s)"%(type(self).__name__, ", ".join(items))
49 return "%s(%s)"%(type(self).__name__, ", ".join(items))
50
50
51 class logerror(Exception):
51 class logerror(Exception):
52 pass
52 pass
53
53
54 def getrepopath(cvspath):
54 def getrepopath(cvspath):
55 """Return the repository path from a CVS path.
55 """Return the repository path from a CVS path.
56
56
57 >>> getrepopath(b'/foo/bar')
57 >>> getrepopath(b'/foo/bar')
58 '/foo/bar'
58 '/foo/bar'
59 >>> getrepopath(b'c:/foo/bar')
59 >>> getrepopath(b'c:/foo/bar')
60 '/foo/bar'
60 '/foo/bar'
61 >>> getrepopath(b':pserver:10/foo/bar')
61 >>> getrepopath(b':pserver:10/foo/bar')
62 '/foo/bar'
62 '/foo/bar'
63 >>> getrepopath(b':pserver:10c:/foo/bar')
63 >>> getrepopath(b':pserver:10c:/foo/bar')
64 '/foo/bar'
64 '/foo/bar'
65 >>> getrepopath(b':pserver:/foo/bar')
65 >>> getrepopath(b':pserver:/foo/bar')
66 '/foo/bar'
66 '/foo/bar'
67 >>> getrepopath(b':pserver:c:/foo/bar')
67 >>> getrepopath(b':pserver:c:/foo/bar')
68 '/foo/bar'
68 '/foo/bar'
69 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
69 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
70 '/foo/bar'
70 '/foo/bar'
71 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
71 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
72 '/foo/bar'
72 '/foo/bar'
73 >>> getrepopath(b'user@server/path/to/repository')
73 >>> getrepopath(b'user@server/path/to/repository')
74 '/path/to/repository'
74 '/path/to/repository'
75 """
75 """
76 # According to CVS manual, CVS paths are expressed like:
76 # According to CVS manual, CVS paths are expressed like:
77 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
77 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
78 #
78 #
79 # CVSpath is splitted into parts and then position of the first occurrence
79 # CVSpath is splitted into parts and then position of the first occurrence
80 # of the '/' char after the '@' is located. The solution is the rest of the
80 # of the '/' char after the '@' is located. The solution is the rest of the
81 # string after that '/' sign including it
81 # string after that '/' sign including it
82
82
83 parts = cvspath.split(':')
83 parts = cvspath.split(':')
84 atposition = parts[-1].find('@')
84 atposition = parts[-1].find('@')
85 start = 0
85 start = 0
86
86
87 if atposition != -1:
87 if atposition != -1:
88 start = atposition
88 start = atposition
89
89
90 repopath = parts[-1][parts[-1].find('/', start):]
90 repopath = parts[-1][parts[-1].find('/', start):]
91 return repopath
91 return repopath
92
92
93 def createlog(ui, directory=None, root="", rlog=True, cache=None):
93 def createlog(ui, directory=None, root="", rlog=True, cache=None):
94 '''Collect the CVS rlog'''
94 '''Collect the CVS rlog'''
95
95
96 # Because we store many duplicate commit log messages, reusing strings
96 # Because we store many duplicate commit log messages, reusing strings
97 # saves a lot of memory and pickle storage space.
97 # saves a lot of memory and pickle storage space.
98 _scache = {}
98 _scache = {}
99 def scache(s):
99 def scache(s):
100 "return a shared version of a string"
100 "return a shared version of a string"
101 return _scache.setdefault(s, s)
101 return _scache.setdefault(s, s)
102
102
103 ui.status(_('collecting CVS rlog\n'))
103 ui.status(_('collecting CVS rlog\n'))
104
104
105 log = [] # list of logentry objects containing the CVS state
105 log = [] # list of logentry objects containing the CVS state
106
106
107 # patterns to match in CVS (r)log output, by state of use
107 # patterns to match in CVS (r)log output, by state of use
108 re_00 = re.compile('RCS file: (.+)$')
108 re_00 = re.compile('RCS file: (.+)$')
109 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
109 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
110 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
110 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
111 re_03 = re.compile("(Cannot access.+CVSROOT)|"
111 re_03 = re.compile("(Cannot access.+CVSROOT)|"
112 "(can't create temporary directory.+)$")
112 "(can't create temporary directory.+)$")
113 re_10 = re.compile('Working file: (.+)$')
113 re_10 = re.compile('Working file: (.+)$')
114 re_20 = re.compile('symbolic names:')
114 re_20 = re.compile('symbolic names:')
115 re_30 = re.compile('\t(.+): ([\\d.]+)$')
115 re_30 = re.compile('\t(.+): ([\\d.]+)$')
116 re_31 = re.compile('----------------------------$')
116 re_31 = re.compile('----------------------------$')
117 re_32 = re.compile('======================================='
117 re_32 = re.compile('======================================='
118 '======================================$')
118 '======================================$')
119 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
119 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
120 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
120 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
121 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
121 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
122 r'(\s+commitid:\s+([^;]+);)?'
122 r'(\s+commitid:\s+([^;]+);)?'
123 r'(.*mergepoint:\s+([^;]+);)?')
123 r'(.*mergepoint:\s+([^;]+);)?')
124 re_70 = re.compile('branches: (.+);$')
124 re_70 = re.compile('branches: (.+);$')
125
125
126 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
126 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
127
127
128 prefix = '' # leading path to strip of what we get from CVS
128 prefix = '' # leading path to strip of what we get from CVS
129
129
130 if directory is None:
130 if directory is None:
131 # Current working directory
131 # Current working directory
132
132
133 # Get the real directory in the repository
133 # Get the real directory in the repository
134 try:
134 try:
135 prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip()
135 prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip()
136 directory = prefix
136 directory = prefix
137 if prefix == ".":
137 if prefix == ".":
138 prefix = ""
138 prefix = ""
139 except IOError:
139 except IOError:
140 raise logerror(_('not a CVS sandbox'))
140 raise logerror(_('not a CVS sandbox'))
141
141
142 if prefix and not prefix.endswith(pycompat.ossep):
142 if prefix and not prefix.endswith(pycompat.ossep):
143 prefix += pycompat.ossep
143 prefix += pycompat.ossep
144
144
145 # Use the Root file in the sandbox, if it exists
145 # Use the Root file in the sandbox, if it exists
146 try:
146 try:
147 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
147 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
148 except IOError:
148 except IOError:
149 pass
149 pass
150
150
151 if not root:
151 if not root:
152 root = encoding.environ.get('CVSROOT', '')
152 root = encoding.environ.get('CVSROOT', '')
153
153
154 # read log cache if one exists
154 # read log cache if one exists
155 oldlog = []
155 oldlog = []
156 date = None
156 date = None
157
157
158 if cache:
158 if cache:
159 cachedir = os.path.expanduser('~/.hg.cvsps')
159 cachedir = os.path.expanduser('~/.hg.cvsps')
160 if not os.path.exists(cachedir):
160 if not os.path.exists(cachedir):
161 os.mkdir(cachedir)
161 os.mkdir(cachedir)
162
162
163 # The cvsps cache pickle needs a uniquified name, based on the
163 # The cvsps cache pickle needs a uniquified name, based on the
164 # repository location. The address may have all sort of nasties
164 # repository location. The address may have all sort of nasties
165 # in it, slashes, colons and such. So here we take just the
165 # in it, slashes, colons and such. So here we take just the
166 # alphanumeric characters, concatenated in a way that does not
166 # alphanumeric characters, concatenated in a way that does not
167 # mix up the various components, so that
167 # mix up the various components, so that
168 # :pserver:user@server:/path
168 # :pserver:user@server:/path
169 # and
169 # and
170 # /pserver/user/server/path
170 # /pserver/user/server/path
171 # are mapped to different cache file names.
171 # are mapped to different cache file names.
172 cachefile = root.split(":") + [directory, "cache"]
172 cachefile = root.split(":") + [directory, "cache"]
173 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
173 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
174 cachefile = os.path.join(cachedir,
174 cachefile = os.path.join(cachedir,
175 '.'.join([s for s in cachefile if s]))
175 '.'.join([s for s in cachefile if s]))
176
176
177 if cache == 'update':
177 if cache == 'update':
178 try:
178 try:
179 ui.note(_('reading cvs log cache %s\n') % cachefile)
179 ui.note(_('reading cvs log cache %s\n') % cachefile)
180 oldlog = pickle.load(open(cachefile, 'rb'))
180 oldlog = pickle.load(open(cachefile, 'rb'))
181 for e in oldlog:
181 for e in oldlog:
182 if not (util.safehasattr(e, 'branchpoints') and
182 if not (util.safehasattr(e, 'branchpoints') and
183 util.safehasattr(e, 'commitid') and
183 util.safehasattr(e, 'commitid') and
184 util.safehasattr(e, 'mergepoint')):
184 util.safehasattr(e, 'mergepoint')):
185 ui.status(_('ignoring old cache\n'))
185 ui.status(_('ignoring old cache\n'))
186 oldlog = []
186 oldlog = []
187 break
187 break
188
188
189 ui.note(_('cache has %d log entries\n') % len(oldlog))
189 ui.note(_('cache has %d log entries\n') % len(oldlog))
190 except Exception as e:
190 except Exception as e:
191 ui.note(_('error reading cache: %r\n') % e)
191 ui.note(_('error reading cache: %r\n') % e)
192
192
193 if oldlog:
193 if oldlog:
194 date = oldlog[-1].date # last commit date as a (time,tz) tuple
194 date = oldlog[-1].date # last commit date as a (time,tz) tuple
195 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
195 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
196
196
197 # build the CVS commandline
197 # build the CVS commandline
198 cmd = ['cvs', '-q']
198 cmd = ['cvs', '-q']
199 if root:
199 if root:
200 cmd.append('-d%s' % root)
200 cmd.append('-d%s' % root)
201 p = util.normpath(getrepopath(root))
201 p = util.normpath(getrepopath(root))
202 if not p.endswith('/'):
202 if not p.endswith('/'):
203 p += '/'
203 p += '/'
204 if prefix:
204 if prefix:
205 # looks like normpath replaces "" by "."
205 # looks like normpath replaces "" by "."
206 prefix = p + util.normpath(prefix)
206 prefix = p + util.normpath(prefix)
207 else:
207 else:
208 prefix = p
208 prefix = p
209 cmd.append(['log', 'rlog'][rlog])
209 cmd.append(['log', 'rlog'][rlog])
210 if date:
210 if date:
211 # no space between option and date string
211 # no space between option and date string
212 cmd.append('-d>%s' % date)
212 cmd.append('-d>%s' % date)
213 cmd.append(directory)
213 cmd.append(directory)
214
214
215 # state machine begins here
215 # state machine begins here
216 tags = {} # dictionary of revisions on current file with their tags
216 tags = {} # dictionary of revisions on current file with their tags
217 branchmap = {} # mapping between branch names and revision numbers
217 branchmap = {} # mapping between branch names and revision numbers
218 rcsmap = {}
218 rcsmap = {}
219 state = 0
219 state = 0
220 store = False # set when a new record can be appended
220 store = False # set when a new record can be appended
221
221
222 cmd = [util.shellquote(arg) for arg in cmd]
222 cmd = [util.shellquote(arg) for arg in cmd]
223 ui.note(_("running %s\n") % (' '.join(cmd)))
223 ui.note(_("running %s\n") % (' '.join(cmd)))
224 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
224 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
225
225
226 pfp = util.popen(' '.join(cmd))
226 pfp = util.popen(' '.join(cmd))
227 peek = pfp.readline()
227 peek = pfp.readline()
228 while True:
228 while True:
229 line = peek
229 line = peek
230 if line == '':
230 if line == '':
231 break
231 break
232 peek = pfp.readline()
232 peek = pfp.readline()
233 if line.endswith('\n'):
233 if line.endswith('\n'):
234 line = line[:-1]
234 line = line[:-1]
235 #ui.debug('state=%d line=%r\n' % (state, line))
235 #ui.debug('state=%d line=%r\n' % (state, line))
236
236
237 if state == 0:
237 if state == 0:
238 # initial state, consume input until we see 'RCS file'
238 # initial state, consume input until we see 'RCS file'
239 match = re_00.match(line)
239 match = re_00.match(line)
240 if match:
240 if match:
241 rcs = match.group(1)
241 rcs = match.group(1)
242 tags = {}
242 tags = {}
243 if rlog:
243 if rlog:
244 filename = util.normpath(rcs[:-2])
244 filename = util.normpath(rcs[:-2])
245 if filename.startswith(prefix):
245 if filename.startswith(prefix):
246 filename = filename[len(prefix):]
246 filename = filename[len(prefix):]
247 if filename.startswith('/'):
247 if filename.startswith('/'):
248 filename = filename[1:]
248 filename = filename[1:]
249 if filename.startswith('Attic/'):
249 if filename.startswith('Attic/'):
250 filename = filename[6:]
250 filename = filename[6:]
251 else:
251 else:
252 filename = filename.replace('/Attic/', '/')
252 filename = filename.replace('/Attic/', '/')
253 state = 2
253 state = 2
254 continue
254 continue
255 state = 1
255 state = 1
256 continue
256 continue
257 match = re_01.match(line)
257 match = re_01.match(line)
258 if match:
258 if match:
259 raise logerror(match.group(1))
259 raise logerror(match.group(1))
260 match = re_02.match(line)
260 match = re_02.match(line)
261 if match:
261 if match:
262 raise logerror(match.group(2))
262 raise logerror(match.group(2))
263 if re_03.match(line):
263 if re_03.match(line):
264 raise logerror(line)
264 raise logerror(line)
265
265
266 elif state == 1:
266 elif state == 1:
267 # expect 'Working file' (only when using log instead of rlog)
267 # expect 'Working file' (only when using log instead of rlog)
268 match = re_10.match(line)
268 match = re_10.match(line)
269 assert match, _('RCS file must be followed by working file')
269 assert match, _('RCS file must be followed by working file')
270 filename = util.normpath(match.group(1))
270 filename = util.normpath(match.group(1))
271 state = 2
271 state = 2
272
272
273 elif state == 2:
273 elif state == 2:
274 # expect 'symbolic names'
274 # expect 'symbolic names'
275 if re_20.match(line):
275 if re_20.match(line):
276 branchmap = {}
276 branchmap = {}
277 state = 3
277 state = 3
278
278
279 elif state == 3:
279 elif state == 3:
280 # read the symbolic names and store as tags
280 # read the symbolic names and store as tags
281 match = re_30.match(line)
281 match = re_30.match(line)
282 if match:
282 if match:
283 rev = [int(x) for x in match.group(2).split('.')]
283 rev = [int(x) for x in match.group(2).split('.')]
284
284
285 # Convert magic branch number to an odd-numbered one
285 # Convert magic branch number to an odd-numbered one
286 revn = len(rev)
286 revn = len(rev)
287 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
287 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
288 rev = rev[:-2] + rev[-1:]
288 rev = rev[:-2] + rev[-1:]
289 rev = tuple(rev)
289 rev = tuple(rev)
290
290
291 if rev not in tags:
291 if rev not in tags:
292 tags[rev] = []
292 tags[rev] = []
293 tags[rev].append(match.group(1))
293 tags[rev].append(match.group(1))
294 branchmap[match.group(1)] = match.group(2)
294 branchmap[match.group(1)] = match.group(2)
295
295
296 elif re_31.match(line):
296 elif re_31.match(line):
297 state = 5
297 state = 5
298 elif re_32.match(line):
298 elif re_32.match(line):
299 state = 0
299 state = 0
300
300
301 elif state == 4:
301 elif state == 4:
302 # expecting '------' separator before first revision
302 # expecting '------' separator before first revision
303 if re_31.match(line):
303 if re_31.match(line):
304 state = 5
304 state = 5
305 else:
305 else:
306 assert not re_32.match(line), _('must have at least '
306 assert not re_32.match(line), _('must have at least '
307 'some revisions')
307 'some revisions')
308
308
309 elif state == 5:
309 elif state == 5:
310 # expecting revision number and possibly (ignored) lock indication
310 # expecting revision number and possibly (ignored) lock indication
311 # we create the logentry here from values stored in states 0 to 4,
311 # we create the logentry here from values stored in states 0 to 4,
312 # as this state is re-entered for subsequent revisions of a file.
312 # as this state is re-entered for subsequent revisions of a file.
313 match = re_50.match(line)
313 match = re_50.match(line)
314 assert match, _('expected revision number')
314 assert match, _('expected revision number')
315 e = logentry(rcs=scache(rcs),
315 e = logentry(rcs=scache(rcs),
316 file=scache(filename),
316 file=scache(filename),
317 revision=tuple([int(x) for x in
317 revision=tuple([int(x) for x in
318 match.group(1).split('.')]),
318 match.group(1).split('.')]),
319 branches=[],
319 branches=[],
320 parent=None,
320 parent=None,
321 commitid=None,
321 commitid=None,
322 mergepoint=None,
322 mergepoint=None,
323 branchpoints=set())
323 branchpoints=set())
324
324
325 state = 6
325 state = 6
326
326
327 elif state == 6:
327 elif state == 6:
328 # expecting date, author, state, lines changed
328 # expecting date, author, state, lines changed
329 match = re_60.match(line)
329 match = re_60.match(line)
330 assert match, _('revision must be followed by date line')
330 assert match, _('revision must be followed by date line')
331 d = match.group(1)
331 d = match.group(1)
332 if d[2] == '/':
332 if d[2] == '/':
333 # Y2K
333 # Y2K
334 d = '19' + d
334 d = '19' + d
335
335
336 if len(d.split()) != 3:
336 if len(d.split()) != 3:
337 # cvs log dates always in GMT
337 # cvs log dates always in GMT
338 d = d + ' UTC'
338 d = d + ' UTC'
339 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
339 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
340 '%Y/%m/%d %H:%M:%S',
340 '%Y/%m/%d %H:%M:%S',
341 '%Y-%m-%d %H:%M:%S'])
341 '%Y-%m-%d %H:%M:%S'])
342 e.author = scache(match.group(2))
342 e.author = scache(match.group(2))
343 e.dead = match.group(3).lower() == 'dead'
343 e.dead = match.group(3).lower() == 'dead'
344
344
345 if match.group(5):
345 if match.group(5):
346 if match.group(6):
346 if match.group(6):
347 e.lines = (int(match.group(5)), int(match.group(6)))
347 e.lines = (int(match.group(5)), int(match.group(6)))
348 else:
348 else:
349 e.lines = (int(match.group(5)), 0)
349 e.lines = (int(match.group(5)), 0)
350 elif match.group(6):
350 elif match.group(6):
351 e.lines = (0, int(match.group(6)))
351 e.lines = (0, int(match.group(6)))
352 else:
352 else:
353 e.lines = None
353 e.lines = None
354
354
355 if match.group(7): # cvs 1.12 commitid
355 if match.group(7): # cvs 1.12 commitid
356 e.commitid = match.group(8)
356 e.commitid = match.group(8)
357
357
358 if match.group(9): # cvsnt mergepoint
358 if match.group(9): # cvsnt mergepoint
359 myrev = match.group(10).split('.')
359 myrev = match.group(10).split('.')
360 if len(myrev) == 2: # head
360 if len(myrev) == 2: # head
361 e.mergepoint = 'HEAD'
361 e.mergepoint = 'HEAD'
362 else:
362 else:
363 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
363 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
364 branches = [b for b in branchmap if branchmap[b] == myrev]
364 branches = [b for b in branchmap if branchmap[b] == myrev]
365 assert len(branches) == 1, ('unknown branch: %s'
365 assert len(branches) == 1, ('unknown branch: %s'
366 % e.mergepoint)
366 % e.mergepoint)
367 e.mergepoint = branches[0]
367 e.mergepoint = branches[0]
368
368
369 e.comment = []
369 e.comment = []
370 state = 7
370 state = 7
371
371
372 elif state == 7:
372 elif state == 7:
373 # read the revision numbers of branches that start at this revision
373 # read the revision numbers of branches that start at this revision
374 # or store the commit log message otherwise
374 # or store the commit log message otherwise
375 m = re_70.match(line)
375 m = re_70.match(line)
376 if m:
376 if m:
377 e.branches = [tuple([int(y) for y in x.strip().split('.')])
377 e.branches = [tuple([int(y) for y in x.strip().split('.')])
378 for x in m.group(1).split(';')]
378 for x in m.group(1).split(';')]
379 state = 8
379 state = 8
380 elif re_31.match(line) and re_50.match(peek):
380 elif re_31.match(line) and re_50.match(peek):
381 state = 5
381 state = 5
382 store = True
382 store = True
383 elif re_32.match(line):
383 elif re_32.match(line):
384 state = 0
384 state = 0
385 store = True
385 store = True
386 else:
386 else:
387 e.comment.append(line)
387 e.comment.append(line)
388
388
389 elif state == 8:
389 elif state == 8:
390 # store commit log message
390 # store commit log message
391 if re_31.match(line):
391 if re_31.match(line):
392 cpeek = peek
392 cpeek = peek
393 if cpeek.endswith('\n'):
393 if cpeek.endswith('\n'):
394 cpeek = cpeek[:-1]
394 cpeek = cpeek[:-1]
395 if re_50.match(cpeek):
395 if re_50.match(cpeek):
396 state = 5
396 state = 5
397 store = True
397 store = True
398 else:
398 else:
399 e.comment.append(line)
399 e.comment.append(line)
400 elif re_32.match(line):
400 elif re_32.match(line):
401 state = 0
401 state = 0
402 store = True
402 store = True
403 else:
403 else:
404 e.comment.append(line)
404 e.comment.append(line)
405
405
406 # When a file is added on a branch B1, CVS creates a synthetic
406 # When a file is added on a branch B1, CVS creates a synthetic
407 # dead trunk revision 1.1 so that the branch has a root.
407 # dead trunk revision 1.1 so that the branch has a root.
408 # Likewise, if you merge such a file to a later branch B2 (one
408 # Likewise, if you merge such a file to a later branch B2 (one
409 # that already existed when the file was added on B1), CVS
409 # that already existed when the file was added on B1), CVS
410 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
410 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
411 # these revisions now, but mark them synthetic so
411 # these revisions now, but mark them synthetic so
412 # createchangeset() can take care of them.
412 # createchangeset() can take care of them.
413 if (store and
413 if (store and
414 e.dead and
414 e.dead and
415 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
415 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
416 len(e.comment) == 1 and
416 len(e.comment) == 1 and
417 file_added_re.match(e.comment[0])):
417 file_added_re.match(e.comment[0])):
418 ui.debug('found synthetic revision in %s: %r\n'
418 ui.debug('found synthetic revision in %s: %r\n'
419 % (e.rcs, e.comment[0]))
419 % (e.rcs, e.comment[0]))
420 e.synthetic = True
420 e.synthetic = True
421
421
422 if store:
422 if store:
423 # clean up the results and save in the log.
423 # clean up the results and save in the log.
424 store = False
424 store = False
425 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
425 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
426 e.comment = scache('\n'.join(e.comment))
426 e.comment = scache('\n'.join(e.comment))
427
427
428 revn = len(e.revision)
428 revn = len(e.revision)
429 if revn > 3 and (revn % 2) == 0:
429 if revn > 3 and (revn % 2) == 0:
430 e.branch = tags.get(e.revision[:-1], [None])[0]
430 e.branch = tags.get(e.revision[:-1], [None])[0]
431 else:
431 else:
432 e.branch = None
432 e.branch = None
433
433
434 # find the branches starting from this revision
434 # find the branches starting from this revision
435 branchpoints = set()
435 branchpoints = set()
436 for branch, revision in branchmap.iteritems():
436 for branch, revision in branchmap.iteritems():
437 revparts = tuple([int(i) for i in revision.split('.')])
437 revparts = tuple([int(i) for i in revision.split('.')])
438 if len(revparts) < 2: # bad tags
438 if len(revparts) < 2: # bad tags
439 continue
439 continue
440 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
440 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
441 # normal branch
441 # normal branch
442 if revparts[:-2] == e.revision:
442 if revparts[:-2] == e.revision:
443 branchpoints.add(branch)
443 branchpoints.add(branch)
444 elif revparts == (1, 1, 1): # vendor branch
444 elif revparts == (1, 1, 1): # vendor branch
445 if revparts in e.branches:
445 if revparts in e.branches:
446 branchpoints.add(branch)
446 branchpoints.add(branch)
447 e.branchpoints = branchpoints
447 e.branchpoints = branchpoints
448
448
449 log.append(e)
449 log.append(e)
450
450
451 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
451 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
452
452
453 if len(log) % 100 == 0:
453 if len(log) % 100 == 0:
454 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
454 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
455
455
456 log.sort(key=lambda x: (x.rcs, x.revision))
456 log.sort(key=lambda x: (x.rcs, x.revision))
457
457
458 # find parent revisions of individual files
458 # find parent revisions of individual files
459 versions = {}
459 versions = {}
460 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
460 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
461 rcs = e.rcs.replace('/Attic/', '/')
461 rcs = e.rcs.replace('/Attic/', '/')
462 if rcs in rcsmap:
462 if rcs in rcsmap:
463 e.rcs = rcsmap[rcs]
463 e.rcs = rcsmap[rcs]
464 branch = e.revision[:-1]
464 branch = e.revision[:-1]
465 versions[(e.rcs, branch)] = e.revision
465 versions[(e.rcs, branch)] = e.revision
466
466
467 for e in log:
467 for e in log:
468 branch = e.revision[:-1]
468 branch = e.revision[:-1]
469 p = versions.get((e.rcs, branch), None)
469 p = versions.get((e.rcs, branch), None)
470 if p is None:
470 if p is None:
471 p = e.revision[:-2]
471 p = e.revision[:-2]
472 e.parent = p
472 e.parent = p
473 versions[(e.rcs, branch)] = e.revision
473 versions[(e.rcs, branch)] = e.revision
474
474
475 # update the log cache
475 # update the log cache
476 if cache:
476 if cache:
477 if log:
477 if log:
478 # join up the old and new logs
478 # join up the old and new logs
479 log.sort(key=lambda x: x.date)
479 log.sort(key=lambda x: x.date)
480
480
481 if oldlog and oldlog[-1].date >= log[0].date:
481 if oldlog and oldlog[-1].date >= log[0].date:
482 raise logerror(_('log cache overlaps with new log entries,'
482 raise logerror(_('log cache overlaps with new log entries,'
483 ' re-run without cache.'))
483 ' re-run without cache.'))
484
484
485 log = oldlog + log
485 log = oldlog + log
486
486
487 # write the new cachefile
487 # write the new cachefile
488 ui.note(_('writing cvs log cache %s\n') % cachefile)
488 ui.note(_('writing cvs log cache %s\n') % cachefile)
489 pickle.dump(log, open(cachefile, 'wb'))
489 pickle.dump(log, open(cachefile, 'wb'))
490 else:
490 else:
491 log = oldlog
491 log = oldlog
492
492
493 ui.status(_('%d log entries\n') % len(log))
493 ui.status(_('%d log entries\n') % len(log))
494
494
495 encodings = ui.configlist('convert', 'cvsps.logencoding')
495 encodings = ui.configlist('convert', 'cvsps.logencoding')
496 if encodings:
496 if encodings:
497 def revstr(r):
497 def revstr(r):
498 # this is needed, because logentry.revision is a tuple of "int"
498 # this is needed, because logentry.revision is a tuple of "int"
499 # (e.g. (1, 2) for "1.2")
499 # (e.g. (1, 2) for "1.2")
500 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
500 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
501
501
502 for entry in log:
502 for entry in log:
503 comment = entry.comment
503 comment = entry.comment
504 for e in encodings:
504 for e in encodings:
505 try:
505 try:
506 entry.comment = comment.decode(e).encode('utf-8')
506 entry.comment = comment.decode(e).encode('utf-8')
507 if ui.debugflag:
507 if ui.debugflag:
508 ui.debug("transcoding by %s: %s of %s\n" %
508 ui.debug("transcoding by %s: %s of %s\n" %
509 (e, revstr(entry.revision), entry.file))
509 (e, revstr(entry.revision), entry.file))
510 break
510 break
511 except UnicodeDecodeError:
511 except UnicodeDecodeError:
512 pass # try next encoding
512 pass # try next encoding
513 except LookupError as inst: # unknown encoding, maybe
513 except LookupError as inst: # unknown encoding, maybe
514 raise error.Abort(inst,
514 raise error.Abort(inst,
515 hint=_('check convert.cvsps.logencoding'
515 hint=_('check convert.cvsps.logencoding'
516 ' configuration'))
516 ' configuration'))
517 else:
517 else:
518 raise error.Abort(_("no encoding can transcode"
518 raise error.Abort(_("no encoding can transcode"
519 " CVS log message for %s of %s")
519 " CVS log message for %s of %s")
520 % (revstr(entry.revision), entry.file),
520 % (revstr(entry.revision), entry.file),
521 hint=_('check convert.cvsps.logencoding'
521 hint=_('check convert.cvsps.logencoding'
522 ' configuration'))
522 ' configuration'))
523
523
524 hook.hook(ui, None, "cvslog", True, log=log)
524 hook.hook(ui, None, "cvslog", True, log=log)
525
525
526 return log
526 return log
527
527
528
528
529 class changeset(object):
529 class changeset(object):
530 '''Class changeset has the following attributes:
530 '''Class changeset has the following attributes:
531 .id - integer identifying this changeset (list index)
531 .id - integer identifying this changeset (list index)
532 .author - author name as CVS knows it
532 .author - author name as CVS knows it
533 .branch - name of branch this changeset is on, or None
533 .branch - name of branch this changeset is on, or None
534 .comment - commit message
534 .comment - commit message
535 .commitid - CVS commitid or None
535 .commitid - CVS commitid or None
536 .date - the commit date as a (time,tz) tuple
536 .date - the commit date as a (time,tz) tuple
537 .entries - list of logentry objects in this changeset
537 .entries - list of logentry objects in this changeset
538 .parents - list of one or two parent changesets
538 .parents - list of one or two parent changesets
539 .tags - list of tags on this changeset
539 .tags - list of tags on this changeset
540 .synthetic - from synthetic revision "file ... added on branch ..."
540 .synthetic - from synthetic revision "file ... added on branch ..."
541 .mergepoint- the branch that has been merged from or None
541 .mergepoint- the branch that has been merged from or None
542 .branchpoints- the branches that start at the current entry or empty
542 .branchpoints- the branches that start at the current entry or empty
543 '''
543 '''
544 def __init__(self, **entries):
544 def __init__(self, **entries):
545 self.id = None
545 self.id = None
546 self.synthetic = False
546 self.synthetic = False
547 self.__dict__.update(entries)
547 self.__dict__.update(entries)
548
548
549 def __repr__(self):
549 def __repr__(self):
550 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
550 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
551 return "%s(%s)"%(type(self).__name__, ", ".join(items))
551 return "%s(%s)"%(type(self).__name__, ", ".join(items))
552
552
553 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
553 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
554 '''Convert log into changesets.'''
554 '''Convert log into changesets.'''
555
555
556 ui.status(_('creating changesets\n'))
556 ui.status(_('creating changesets\n'))
557
557
558 # try to order commitids by date
558 # try to order commitids by date
559 mindate = {}
559 mindate = {}
560 for e in log:
560 for e in log:
561 if e.commitid:
561 if e.commitid:
562 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
562 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
563
563
564 # Merge changesets
564 # Merge changesets
565 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
565 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
566 x.author, x.branch, x.date, x.branchpoints))
566 x.author, x.branch, x.date, x.branchpoints))
567
567
568 changesets = []
568 changesets = []
569 files = set()
569 files = set()
570 c = None
570 c = None
571 for i, e in enumerate(log):
571 for i, e in enumerate(log):
572
572
573 # Check if log entry belongs to the current changeset or not.
573 # Check if log entry belongs to the current changeset or not.
574
574
575 # Since CVS is file-centric, two different file revisions with
575 # Since CVS is file-centric, two different file revisions with
576 # different branchpoints should be treated as belonging to two
576 # different branchpoints should be treated as belonging to two
577 # different changesets (and the ordering is important and not
577 # different changesets (and the ordering is important and not
578 # honoured by cvsps at this point).
578 # honoured by cvsps at this point).
579 #
579 #
580 # Consider the following case:
580 # Consider the following case:
581 # foo 1.1 branchpoints: [MYBRANCH]
581 # foo 1.1 branchpoints: [MYBRANCH]
582 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
582 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
583 #
583 #
584 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
584 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
585 # later version of foo may be in MYBRANCH2, so foo should be the
585 # later version of foo may be in MYBRANCH2, so foo should be the
586 # first changeset and bar the next and MYBRANCH and MYBRANCH2
586 # first changeset and bar the next and MYBRANCH and MYBRANCH2
587 # should both start off of the bar changeset. No provisions are
587 # should both start off of the bar changeset. No provisions are
588 # made to ensure that this is, in fact, what happens.
588 # made to ensure that this is, in fact, what happens.
589 if not (c and e.branchpoints == c.branchpoints and
589 if not (c and e.branchpoints == c.branchpoints and
590 (# cvs commitids
590 (# cvs commitids
591 (e.commitid is not None and e.commitid == c.commitid) or
591 (e.commitid is not None and e.commitid == c.commitid) or
592 (# no commitids, use fuzzy commit detection
592 (# no commitids, use fuzzy commit detection
593 (e.commitid is None or c.commitid is None) and
593 (e.commitid is None or c.commitid is None) and
594 e.comment == c.comment and
594 e.comment == c.comment and
595 e.author == c.author and
595 e.author == c.author and
596 e.branch == c.branch and
596 e.branch == c.branch and
597 ((c.date[0] + c.date[1]) <=
597 ((c.date[0] + c.date[1]) <=
598 (e.date[0] + e.date[1]) <=
598 (e.date[0] + e.date[1]) <=
599 (c.date[0] + c.date[1]) + fuzz) and
599 (c.date[0] + c.date[1]) + fuzz) and
600 e.file not in files))):
600 e.file not in files))):
601 c = changeset(comment=e.comment, author=e.author,
601 c = changeset(comment=e.comment, author=e.author,
602 branch=e.branch, date=e.date,
602 branch=e.branch, date=e.date,
603 entries=[], mergepoint=e.mergepoint,
603 entries=[], mergepoint=e.mergepoint,
604 branchpoints=e.branchpoints, commitid=e.commitid)
604 branchpoints=e.branchpoints, commitid=e.commitid)
605 changesets.append(c)
605 changesets.append(c)
606
606
607 files = set()
607 files = set()
608 if len(changesets) % 100 == 0:
608 if len(changesets) % 100 == 0:
609 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
609 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
610 ui.status(util.ellipsis(t, 80) + '\n')
610 ui.status(util.ellipsis(t, 80) + '\n')
611
611
612 c.entries.append(e)
612 c.entries.append(e)
613 files.add(e.file)
613 files.add(e.file)
614 c.date = e.date # changeset date is date of latest commit in it
614 c.date = e.date # changeset date is date of latest commit in it
615
615
616 # Mark synthetic changesets
616 # Mark synthetic changesets
617
617
618 for c in changesets:
618 for c in changesets:
619 # Synthetic revisions always get their own changeset, because
619 # Synthetic revisions always get their own changeset, because
620 # the log message includes the filename. E.g. if you add file3
620 # the log message includes the filename. E.g. if you add file3
621 # and file4 on a branch, you get four log entries and three
621 # and file4 on a branch, you get four log entries and three
622 # changesets:
622 # changesets:
623 # "File file3 was added on branch ..." (synthetic, 1 entry)
623 # "File file3 was added on branch ..." (synthetic, 1 entry)
624 # "File file4 was added on branch ..." (synthetic, 1 entry)
624 # "File file4 was added on branch ..." (synthetic, 1 entry)
625 # "Add file3 and file4 to fix ..." (real, 2 entries)
625 # "Add file3 and file4 to fix ..." (real, 2 entries)
626 # Hence the check for 1 entry here.
626 # Hence the check for 1 entry here.
627 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
627 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
628
628
629 # Sort files in each changeset
629 # Sort files in each changeset
630
630
631 def entitycompare(l, r):
631 def entitycompare(l, r):
632 'Mimic cvsps sorting order'
632 'Mimic cvsps sorting order'
633 l = l.file.split('/')
633 l = l.file.split('/')
634 r = r.file.split('/')
634 r = r.file.split('/')
635 nl = len(l)
635 nl = len(l)
636 nr = len(r)
636 nr = len(r)
637 n = min(nl, nr)
637 n = min(nl, nr)
638 for i in range(n):
638 for i in range(n):
639 if i + 1 == nl and nl < nr:
639 if i + 1 == nl and nl < nr:
640 return -1
640 return -1
641 elif i + 1 == nr and nl > nr:
641 elif i + 1 == nr and nl > nr:
642 return +1
642 return +1
643 elif l[i] < r[i]:
643 elif l[i] < r[i]:
644 return -1
644 return -1
645 elif l[i] > r[i]:
645 elif l[i] > r[i]:
646 return +1
646 return +1
647 return 0
647 return 0
648
648
649 for c in changesets:
649 for c in changesets:
650 c.entries.sort(entitycompare)
650 c.entries.sort(entitycompare)
651
651
652 # Sort changesets by date
652 # Sort changesets by date
653
653
654 odd = set()
654 odd = set()
655 def cscmp(l, r):
655 def cscmp(l, r):
656 d = sum(l.date) - sum(r.date)
656 d = sum(l.date) - sum(r.date)
657 if d:
657 if d:
658 return d
658 return d
659
659
660 # detect vendor branches and initial commits on a branch
660 # detect vendor branches and initial commits on a branch
661 le = {}
661 le = {}
662 for e in l.entries:
662 for e in l.entries:
663 le[e.rcs] = e.revision
663 le[e.rcs] = e.revision
664 re = {}
664 re = {}
665 for e in r.entries:
665 for e in r.entries:
666 re[e.rcs] = e.revision
666 re[e.rcs] = e.revision
667
667
668 d = 0
668 d = 0
669 for e in l.entries:
669 for e in l.entries:
670 if re.get(e.rcs, None) == e.parent:
670 if re.get(e.rcs, None) == e.parent:
671 assert not d
671 assert not d
672 d = 1
672 d = 1
673 break
673 break
674
674
675 for e in r.entries:
675 for e in r.entries:
676 if le.get(e.rcs, None) == e.parent:
676 if le.get(e.rcs, None) == e.parent:
677 if d:
677 if d:
678 odd.add((l, r))
678 odd.add((l, r))
679 d = -1
679 d = -1
680 break
680 break
681 # By this point, the changesets are sufficiently compared that
681 # By this point, the changesets are sufficiently compared that
682 # we don't really care about ordering. However, this leaves
682 # we don't really care about ordering. However, this leaves
683 # some race conditions in the tests, so we compare on the
683 # some race conditions in the tests, so we compare on the
684 # number of files modified, the files contained in each
684 # number of files modified, the files contained in each
685 # changeset, and the branchpoints in the change to ensure test
685 # changeset, and the branchpoints in the change to ensure test
686 # output remains stable.
686 # output remains stable.
687
687
688 # recommended replacement for cmp from
688 # recommended replacement for cmp from
689 # https://docs.python.org/3.0/whatsnew/3.0.html
689 # https://docs.python.org/3.0/whatsnew/3.0.html
690 c = lambda x, y: (x > y) - (x < y)
690 c = lambda x, y: (x > y) - (x < y)
691 # Sort bigger changes first.
691 # Sort bigger changes first.
692 if not d:
692 if not d:
693 d = c(len(l.entries), len(r.entries))
693 d = c(len(l.entries), len(r.entries))
694 # Try sorting by filename in the change.
694 # Try sorting by filename in the change.
695 if not d:
695 if not d:
696 d = c([e.file for e in l.entries], [e.file for e in r.entries])
696 d = c([e.file for e in l.entries], [e.file for e in r.entries])
697 # Try and put changes without a branch point before ones with
697 # Try and put changes without a branch point before ones with
698 # a branch point.
698 # a branch point.
699 if not d:
699 if not d:
700 d = c(len(l.branchpoints), len(r.branchpoints))
700 d = c(len(l.branchpoints), len(r.branchpoints))
701 return d
701 return d
702
702
703 changesets.sort(cscmp)
703 changesets.sort(cscmp)
704
704
705 # Collect tags
705 # Collect tags
706
706
707 globaltags = {}
707 globaltags = {}
708 for c in changesets:
708 for c in changesets:
709 for e in c.entries:
709 for e in c.entries:
710 for tag in e.tags:
710 for tag in e.tags:
711 # remember which is the latest changeset to have this tag
711 # remember which is the latest changeset to have this tag
712 globaltags[tag] = c
712 globaltags[tag] = c
713
713
714 for c in changesets:
714 for c in changesets:
715 tags = set()
715 tags = set()
716 for e in c.entries:
716 for e in c.entries:
717 tags.update(e.tags)
717 tags.update(e.tags)
718 # remember tags only if this is the latest changeset to have it
718 # remember tags only if this is the latest changeset to have it
719 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
719 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
720
720
721 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
721 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
722 # by inserting dummy changesets with two parents, and handle
722 # by inserting dummy changesets with two parents, and handle
723 # {{mergefrombranch BRANCHNAME}} by setting two parents.
723 # {{mergefrombranch BRANCHNAME}} by setting two parents.
724
724
725 if mergeto is None:
725 if mergeto is None:
726 mergeto = r'{{mergetobranch ([-\w]+)}}'
726 mergeto = r'{{mergetobranch ([-\w]+)}}'
727 if mergeto:
727 if mergeto:
728 mergeto = re.compile(mergeto)
728 mergeto = re.compile(mergeto)
729
729
730 if mergefrom is None:
730 if mergefrom is None:
731 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
731 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
732 if mergefrom:
732 if mergefrom:
733 mergefrom = re.compile(mergefrom)
733 mergefrom = re.compile(mergefrom)
734
734
735 versions = {} # changeset index where we saw any particular file version
735 versions = {} # changeset index where we saw any particular file version
736 branches = {} # changeset index where we saw a branch
736 branches = {} # changeset index where we saw a branch
737 n = len(changesets)
737 n = len(changesets)
738 i = 0
738 i = 0
739 while i < n:
739 while i < n:
740 c = changesets[i]
740 c = changesets[i]
741
741
742 for f in c.entries:
742 for f in c.entries:
743 versions[(f.rcs, f.revision)] = i
743 versions[(f.rcs, f.revision)] = i
744
744
745 p = None
745 p = None
746 if c.branch in branches:
746 if c.branch in branches:
747 p = branches[c.branch]
747 p = branches[c.branch]
748 else:
748 else:
749 # first changeset on a new branch
749 # first changeset on a new branch
750 # the parent is a changeset with the branch in its
750 # the parent is a changeset with the branch in its
751 # branchpoints such that it is the latest possible
751 # branchpoints such that it is the latest possible
752 # commit without any intervening, unrelated commits.
752 # commit without any intervening, unrelated commits.
753
753
754 for candidate in xrange(i):
754 for candidate in xrange(i):
755 if c.branch not in changesets[candidate].branchpoints:
755 if c.branch not in changesets[candidate].branchpoints:
756 if p is not None:
756 if p is not None:
757 break
757 break
758 continue
758 continue
759 p = candidate
759 p = candidate
760
760
761 c.parents = []
761 c.parents = []
762 if p is not None:
762 if p is not None:
763 p = changesets[p]
763 p = changesets[p]
764
764
765 # Ensure no changeset has a synthetic changeset as a parent.
765 # Ensure no changeset has a synthetic changeset as a parent.
766 while p.synthetic:
766 while p.synthetic:
767 assert len(p.parents) <= 1, \
767 assert len(p.parents) <= 1, \
768 _('synthetic changeset cannot have multiple parents')
768 _('synthetic changeset cannot have multiple parents')
769 if p.parents:
769 if p.parents:
770 p = p.parents[0]
770 p = p.parents[0]
771 else:
771 else:
772 p = None
772 p = None
773 break
773 break
774
774
775 if p is not None:
775 if p is not None:
776 c.parents.append(p)
776 c.parents.append(p)
777
777
778 if c.mergepoint:
778 if c.mergepoint:
779 if c.mergepoint == 'HEAD':
779 if c.mergepoint == 'HEAD':
780 c.mergepoint = None
780 c.mergepoint = None
781 c.parents.append(changesets[branches[c.mergepoint]])
781 c.parents.append(changesets[branches[c.mergepoint]])
782
782
783 if mergefrom:
783 if mergefrom:
784 m = mergefrom.search(c.comment)
784 m = mergefrom.search(c.comment)
785 if m:
785 if m:
786 m = m.group(1)
786 m = m.group(1)
787 if m == 'HEAD':
787 if m == 'HEAD':
788 m = None
788 m = None
789 try:
789 try:
790 candidate = changesets[branches[m]]
790 candidate = changesets[branches[m]]
791 except KeyError:
791 except KeyError:
792 ui.warn(_("warning: CVS commit message references "
792 ui.warn(_("warning: CVS commit message references "
793 "non-existent branch %r:\n%s\n")
793 "non-existent branch %r:\n%s\n")
794 % (m, c.comment))
794 % (m, c.comment))
795 if m in branches and c.branch != m and not candidate.synthetic:
795 if m in branches and c.branch != m and not candidate.synthetic:
796 c.parents.append(candidate)
796 c.parents.append(candidate)
797
797
798 if mergeto:
798 if mergeto:
799 m = mergeto.search(c.comment)
799 m = mergeto.search(c.comment)
800 if m:
800 if m:
801 if m.groups():
801 if m.groups():
802 m = m.group(1)
802 m = m.group(1)
803 if m == 'HEAD':
803 if m == 'HEAD':
804 m = None
804 m = None
805 else:
805 else:
806 m = None # if no group found then merge to HEAD
806 m = None # if no group found then merge to HEAD
807 if m in branches and c.branch != m:
807 if m in branches and c.branch != m:
808 # insert empty changeset for merge
808 # insert empty changeset for merge
809 cc = changeset(
809 cc = changeset(
810 author=c.author, branch=m, date=c.date,
810 author=c.author, branch=m, date=c.date,
811 comment='convert-repo: CVS merge from branch %s'
811 comment='convert-repo: CVS merge from branch %s'
812 % c.branch,
812 % c.branch,
813 entries=[], tags=[],
813 entries=[], tags=[],
814 parents=[changesets[branches[m]], c])
814 parents=[changesets[branches[m]], c])
815 changesets.insert(i + 1, cc)
815 changesets.insert(i + 1, cc)
816 branches[m] = i + 1
816 branches[m] = i + 1
817
817
818 # adjust our loop counters now we have inserted a new entry
818 # adjust our loop counters now we have inserted a new entry
819 n += 1
819 n += 1
820 i += 2
820 i += 2
821 continue
821 continue
822
822
823 branches[c.branch] = i
823 branches[c.branch] = i
824 i += 1
824 i += 1
825
825
826 # Drop synthetic changesets (safe now that we have ensured no other
826 # Drop synthetic changesets (safe now that we have ensured no other
827 # changesets can have them as parents).
827 # changesets can have them as parents).
828 i = 0
828 i = 0
829 while i < len(changesets):
829 while i < len(changesets):
830 if changesets[i].synthetic:
830 if changesets[i].synthetic:
831 del changesets[i]
831 del changesets[i]
832 else:
832 else:
833 i += 1
833 i += 1
834
834
835 # Number changesets
835 # Number changesets
836
836
837 for i, c in enumerate(changesets):
837 for i, c in enumerate(changesets):
838 c.id = i + 1
838 c.id = i + 1
839
839
840 if odd:
840 if odd:
841 for l, r in odd:
841 for l, r in odd:
842 if l.id is not None and r.id is not None:
842 if l.id is not None and r.id is not None:
843 ui.warn(_('changeset %d is both before and after %d\n')
843 ui.warn(_('changeset %d is both before and after %d\n')
844 % (l.id, r.id))
844 % (l.id, r.id))
845
845
846 ui.status(_('%d changeset entries\n') % len(changesets))
846 ui.status(_('%d changeset entries\n') % len(changesets))
847
847
848 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
848 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
849
849
850 return changesets
850 return changesets
851
851
852
852
853 def debugcvsps(ui, *args, **opts):
853 def debugcvsps(ui, *args, **opts):
854 '''Read CVS rlog for current directory or named path in
854 '''Read CVS rlog for current directory or named path in
855 repository, and convert the log to changesets based on matching
855 repository, and convert the log to changesets based on matching
856 commit log entries and dates.
856 commit log entries and dates.
857 '''
857 '''
858 opts = pycompat.byteskwargs(opts)
858 if opts["new_cache"]:
859 if opts["new_cache"]:
859 cache = "write"
860 cache = "write"
860 elif opts["update_cache"]:
861 elif opts["update_cache"]:
861 cache = "update"
862 cache = "update"
862 else:
863 else:
863 cache = None
864 cache = None
864
865
865 revisions = opts["revisions"]
866 revisions = opts["revisions"]
866
867
867 try:
868 try:
868 if args:
869 if args:
869 log = []
870 log = []
870 for d in args:
871 for d in args:
871 log += createlog(ui, d, root=opts["root"], cache=cache)
872 log += createlog(ui, d, root=opts["root"], cache=cache)
872 else:
873 else:
873 log = createlog(ui, root=opts["root"], cache=cache)
874 log = createlog(ui, root=opts["root"], cache=cache)
874 except logerror as e:
875 except logerror as e:
875 ui.write("%r\n"%e)
876 ui.write("%r\n"%e)
876 return
877 return
877
878
878 changesets = createchangeset(ui, log, opts["fuzz"])
879 changesets = createchangeset(ui, log, opts["fuzz"])
879 del log
880 del log
880
881
881 # Print changesets (optionally filtered)
882 # Print changesets (optionally filtered)
882
883
883 off = len(revisions)
884 off = len(revisions)
884 branches = {} # latest version number in each branch
885 branches = {} # latest version number in each branch
885 ancestors = {} # parent branch
886 ancestors = {} # parent branch
886 for cs in changesets:
887 for cs in changesets:
887
888
888 if opts["ancestors"]:
889 if opts["ancestors"]:
889 if cs.branch not in branches and cs.parents and cs.parents[0].id:
890 if cs.branch not in branches and cs.parents and cs.parents[0].id:
890 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
891 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
891 cs.parents[0].id)
892 cs.parents[0].id)
892 branches[cs.branch] = cs.id
893 branches[cs.branch] = cs.id
893
894
894 # limit by branches
895 # limit by branches
895 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
896 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
896 continue
897 continue
897
898
898 if not off:
899 if not off:
899 # Note: trailing spaces on several lines here are needed to have
900 # Note: trailing spaces on several lines here are needed to have
900 # bug-for-bug compatibility with cvsps.
901 # bug-for-bug compatibility with cvsps.
901 ui.write('---------------------\n')
902 ui.write('---------------------\n')
902 ui.write(('PatchSet %d \n' % cs.id))
903 ui.write(('PatchSet %d \n' % cs.id))
903 ui.write(('Date: %s\n' % util.datestr(cs.date,
904 ui.write(('Date: %s\n' % util.datestr(cs.date,
904 '%Y/%m/%d %H:%M:%S %1%2')))
905 '%Y/%m/%d %H:%M:%S %1%2')))
905 ui.write(('Author: %s\n' % cs.author))
906 ui.write(('Author: %s\n' % cs.author))
906 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
907 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
907 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
908 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
908 ','.join(cs.tags) or '(none)')))
909 ','.join(cs.tags) or '(none)')))
909 if cs.branchpoints:
910 if cs.branchpoints:
910 ui.write(('Branchpoints: %s \n') %
911 ui.write(('Branchpoints: %s \n') %
911 ', '.join(sorted(cs.branchpoints)))
912 ', '.join(sorted(cs.branchpoints)))
912 if opts["parents"] and cs.parents:
913 if opts["parents"] and cs.parents:
913 if len(cs.parents) > 1:
914 if len(cs.parents) > 1:
914 ui.write(('Parents: %s\n' %
915 ui.write(('Parents: %s\n' %
915 (','.join([str(p.id) for p in cs.parents]))))
916 (','.join([str(p.id) for p in cs.parents]))))
916 else:
917 else:
917 ui.write(('Parent: %d\n' % cs.parents[0].id))
918 ui.write(('Parent: %d\n' % cs.parents[0].id))
918
919
919 if opts["ancestors"]:
920 if opts["ancestors"]:
920 b = cs.branch
921 b = cs.branch
921 r = []
922 r = []
922 while b:
923 while b:
923 b, c = ancestors[b]
924 b, c = ancestors[b]
924 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
925 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
925 if r:
926 if r:
926 ui.write(('Ancestors: %s\n' % (','.join(r))))
927 ui.write(('Ancestors: %s\n' % (','.join(r))))
927
928
928 ui.write(('Log:\n'))
929 ui.write(('Log:\n'))
929 ui.write('%s\n\n' % cs.comment)
930 ui.write('%s\n\n' % cs.comment)
930 ui.write(('Members: \n'))
931 ui.write(('Members: \n'))
931 for f in cs.entries:
932 for f in cs.entries:
932 fn = f.file
933 fn = f.file
933 if fn.startswith(opts["prefix"]):
934 if fn.startswith(opts["prefix"]):
934 fn = fn[len(opts["prefix"]):]
935 fn = fn[len(opts["prefix"]):]
935 ui.write('\t%s:%s->%s%s \n' % (
936 ui.write('\t%s:%s->%s%s \n' % (
936 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
937 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
937 '.'.join([str(x) for x in f.revision]),
938 '.'.join([str(x) for x in f.revision]),
938 ['', '(DEAD)'][f.dead]))
939 ['', '(DEAD)'][f.dead]))
939 ui.write('\n')
940 ui.write('\n')
940
941
941 # have we seen the start tag?
942 # have we seen the start tag?
942 if revisions and off:
943 if revisions and off:
943 if revisions[0] == str(cs.id) or \
944 if revisions[0] == str(cs.id) or \
944 revisions[0] in cs.tags:
945 revisions[0] in cs.tags:
945 off = False
946 off = False
946
947
947 # see if we reached the end tag
948 # see if we reached the end tag
948 if len(revisions) > 1 and not off:
949 if len(revisions) > 1 and not off:
949 if revisions[1] == str(cs.id) or \
950 if revisions[1] == str(cs.id) or \
950 revisions[1] in cs.tags:
951 revisions[1] in cs.tags:
951 break
952 break
@@ -1,371 +1,373 b''
1 # monotone.py - monotone support for the convert extension
1 # monotone.py - monotone support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 # others
4 # others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial import (
14 from mercurial import (
15 error,
15 error,
16 pycompat,
16 util,
17 util,
17 )
18 )
18
19
19 from . import common
20 from . import common
20
21
21 class monotone_source(common.converter_source, common.commandline):
22 class monotone_source(common.converter_source, common.commandline):
22 def __init__(self, ui, repotype, path=None, revs=None):
23 def __init__(self, ui, repotype, path=None, revs=None):
23 common.converter_source.__init__(self, ui, repotype, path, revs)
24 common.converter_source.__init__(self, ui, repotype, path, revs)
24 if revs and len(revs) > 1:
25 if revs and len(revs) > 1:
25 raise error.Abort(_('monotone source does not support specifying '
26 raise error.Abort(_('monotone source does not support specifying '
26 'multiple revs'))
27 'multiple revs'))
27 common.commandline.__init__(self, ui, 'mtn')
28 common.commandline.__init__(self, ui, 'mtn')
28
29
29 self.ui = ui
30 self.ui = ui
30 self.path = path
31 self.path = path
31 self.automatestdio = False
32 self.automatestdio = False
32 self.revs = revs
33 self.revs = revs
33
34
34 norepo = common.NoRepo(_("%s does not look like a monotone repository")
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
35 % path)
36 % path)
36 if not os.path.exists(os.path.join(path, '_MTN')):
37 if not os.path.exists(os.path.join(path, '_MTN')):
37 # Could be a monotone repository (SQLite db file)
38 # Could be a monotone repository (SQLite db file)
38 try:
39 try:
39 f = file(path, 'rb')
40 f = file(path, 'rb')
40 header = f.read(16)
41 header = f.read(16)
41 f.close()
42 f.close()
42 except IOError:
43 except IOError:
43 header = ''
44 header = ''
44 if header != 'SQLite format 3\x00':
45 if header != 'SQLite format 3\x00':
45 raise norepo
46 raise norepo
46
47
47 # regular expressions for parsing monotone output
48 # regular expressions for parsing monotone output
48 space = r'\s*'
49 space = r'\s*'
49 name = r'\s+"((?:\\"|[^"])*)"\s*'
50 name = r'\s+"((?:\\"|[^"])*)"\s*'
50 value = name
51 value = name
51 revision = r'\s+\[(\w+)\]\s*'
52 revision = r'\s+\[(\w+)\]\s*'
52 lines = r'(?:.|\n)+'
53 lines = r'(?:.|\n)+'
53
54
54 self.dir_re = re.compile(space + "dir" + name)
55 self.dir_re = re.compile(space + "dir" + name)
55 self.file_re = re.compile(space + "file" + name +
56 self.file_re = re.compile(space + "file" + name +
56 "content" + revision)
57 "content" + revision)
57 self.add_file_re = re.compile(space + "add_file" + name +
58 self.add_file_re = re.compile(space + "add_file" + name +
58 "content" + revision)
59 "content" + revision)
59 self.patch_re = re.compile(space + "patch" + name +
60 self.patch_re = re.compile(space + "patch" + name +
60 "from" + revision + "to" + revision)
61 "from" + revision + "to" + revision)
61 self.rename_re = re.compile(space + "rename" + name + "to" + name)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
62 self.delete_re = re.compile(space + "delete" + name)
63 self.delete_re = re.compile(space + "delete" + name)
63 self.tag_re = re.compile(space + "tag" + name + "revision" +
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
64 revision)
65 revision)
65 self.cert_re = re.compile(lines + space + "name" + name +
66 self.cert_re = re.compile(lines + space + "name" + name +
66 "value" + value)
67 "value" + value)
67
68
68 attr = space + "file" + lines + space + "attr" + space
69 attr = space + "file" + lines + space + "attr" + space
69 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
70 space + '"true"')
71 space + '"true"')
71
72
72 # cached data
73 # cached data
73 self.manifest_rev = None
74 self.manifest_rev = None
74 self.manifest = None
75 self.manifest = None
75 self.files = None
76 self.files = None
76 self.dirs = None
77 self.dirs = None
77
78
78 common.checktool('mtn', abort=False)
79 common.checktool('mtn', abort=False)
79
80
80 def mtnrun(self, *args, **kwargs):
81 def mtnrun(self, *args, **kwargs):
81 if self.automatestdio:
82 if self.automatestdio:
82 return self.mtnrunstdio(*args, **kwargs)
83 return self.mtnrunstdio(*args, **kwargs)
83 else:
84 else:
84 return self.mtnrunsingle(*args, **kwargs)
85 return self.mtnrunsingle(*args, **kwargs)
85
86
86 def mtnrunsingle(self, *args, **kwargs):
87 def mtnrunsingle(self, *args, **kwargs):
87 kwargs['d'] = self.path
88 kwargs['d'] = self.path
88 return self.run0('automate', *args, **kwargs)
89 return self.run0('automate', *args, **kwargs)
89
90
90 def mtnrunstdio(self, *args, **kwargs):
91 def mtnrunstdio(self, *args, **kwargs):
91 # Prepare the command in automate stdio format
92 # Prepare the command in automate stdio format
93 kwargs = pycompat.byteskwargs(kwargs)
92 command = []
94 command = []
93 for k, v in kwargs.iteritems():
95 for k, v in kwargs.iteritems():
94 command.append("%s:%s" % (len(k), k))
96 command.append("%s:%s" % (len(k), k))
95 if v:
97 if v:
96 command.append("%s:%s" % (len(v), v))
98 command.append("%s:%s" % (len(v), v))
97 if command:
99 if command:
98 command.insert(0, 'o')
100 command.insert(0, 'o')
99 command.append('e')
101 command.append('e')
100
102
101 command.append('l')
103 command.append('l')
102 for arg in args:
104 for arg in args:
103 command += "%s:%s" % (len(arg), arg)
105 command += "%s:%s" % (len(arg), arg)
104 command.append('e')
106 command.append('e')
105 command = ''.join(command)
107 command = ''.join(command)
106
108
107 self.ui.debug("mtn: sending '%s'\n" % command)
109 self.ui.debug("mtn: sending '%s'\n" % command)
108 self.mtnwritefp.write(command)
110 self.mtnwritefp.write(command)
109 self.mtnwritefp.flush()
111 self.mtnwritefp.flush()
110
112
111 return self.mtnstdioreadcommandoutput(command)
113 return self.mtnstdioreadcommandoutput(command)
112
114
113 def mtnstdioreadpacket(self):
115 def mtnstdioreadpacket(self):
114 read = None
116 read = None
115 commandnbr = ''
117 commandnbr = ''
116 while read != ':':
118 while read != ':':
117 read = self.mtnreadfp.read(1)
119 read = self.mtnreadfp.read(1)
118 if not read:
120 if not read:
119 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
120 commandnbr += read
122 commandnbr += read
121 commandnbr = commandnbr[:-1]
123 commandnbr = commandnbr[:-1]
122
124
123 stream = self.mtnreadfp.read(1)
125 stream = self.mtnreadfp.read(1)
124 if stream not in 'mewptl':
126 if stream not in 'mewptl':
125 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
126
128
127 read = self.mtnreadfp.read(1)
129 read = self.mtnreadfp.read(1)
128 if read != ':':
130 if read != ':':
129 raise error.Abort(_('bad mtn packet - no divider before size'))
131 raise error.Abort(_('bad mtn packet - no divider before size'))
130
132
131 read = None
133 read = None
132 lengthstr = ''
134 lengthstr = ''
133 while read != ':':
135 while read != ':':
134 read = self.mtnreadfp.read(1)
136 read = self.mtnreadfp.read(1)
135 if not read:
137 if not read:
136 raise error.Abort(_('bad mtn packet - no end of packet size'))
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
137 lengthstr += read
139 lengthstr += read
138 try:
140 try:
139 length = long(lengthstr[:-1])
141 length = long(lengthstr[:-1])
140 except TypeError:
142 except TypeError:
141 raise error.Abort(_('bad mtn packet - bad packet size %s')
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
142 % lengthstr)
144 % lengthstr)
143
145
144 read = self.mtnreadfp.read(length)
146 read = self.mtnreadfp.read(length)
145 if len(read) != length:
147 if len(read) != length:
146 raise error.Abort(_("bad mtn packet - unable to read full packet "
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
147 "read %s of %s") % (len(read), length))
149 "read %s of %s") % (len(read), length))
148
150
149 return (commandnbr, stream, length, read)
151 return (commandnbr, stream, length, read)
150
152
151 def mtnstdioreadcommandoutput(self, command):
153 def mtnstdioreadcommandoutput(self, command):
152 retval = []
154 retval = []
153 while True:
155 while True:
154 commandnbr, stream, length, output = self.mtnstdioreadpacket()
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
155 self.ui.debug('mtn: read packet %s:%s:%s\n' %
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
156 (commandnbr, stream, length))
158 (commandnbr, stream, length))
157
159
158 if stream == 'l':
160 if stream == 'l':
159 # End of command
161 # End of command
160 if output != '0':
162 if output != '0':
161 raise error.Abort(_("mtn command '%s' returned %s") %
163 raise error.Abort(_("mtn command '%s' returned %s") %
162 (command, output))
164 (command, output))
163 break
165 break
164 elif stream in 'ew':
166 elif stream in 'ew':
165 # Error, warning output
167 # Error, warning output
166 self.ui.warn(_('%s error:\n') % self.command)
168 self.ui.warn(_('%s error:\n') % self.command)
167 self.ui.warn(output)
169 self.ui.warn(output)
168 elif stream == 'p':
170 elif stream == 'p':
169 # Progress messages
171 # Progress messages
170 self.ui.debug('mtn: ' + output)
172 self.ui.debug('mtn: ' + output)
171 elif stream == 'm':
173 elif stream == 'm':
172 # Main stream - command output
174 # Main stream - command output
173 retval.append(output)
175 retval.append(output)
174
176
175 return ''.join(retval)
177 return ''.join(retval)
176
178
177 def mtnloadmanifest(self, rev):
179 def mtnloadmanifest(self, rev):
178 if self.manifest_rev == rev:
180 if self.manifest_rev == rev:
179 return
181 return
180 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
181 self.manifest_rev = rev
183 self.manifest_rev = rev
182 self.files = {}
184 self.files = {}
183 self.dirs = {}
185 self.dirs = {}
184
186
185 for e in self.manifest:
187 for e in self.manifest:
186 m = self.file_re.match(e)
188 m = self.file_re.match(e)
187 if m:
189 if m:
188 attr = ""
190 attr = ""
189 name = m.group(1)
191 name = m.group(1)
190 node = m.group(2)
192 node = m.group(2)
191 if self.attr_execute_re.match(e):
193 if self.attr_execute_re.match(e):
192 attr += "x"
194 attr += "x"
193 self.files[name] = (node, attr)
195 self.files[name] = (node, attr)
194 m = self.dir_re.match(e)
196 m = self.dir_re.match(e)
195 if m:
197 if m:
196 self.dirs[m.group(1)] = True
198 self.dirs[m.group(1)] = True
197
199
198 def mtnisfile(self, name, rev):
200 def mtnisfile(self, name, rev):
199 # a non-file could be a directory or a deleted or renamed file
201 # a non-file could be a directory or a deleted or renamed file
200 self.mtnloadmanifest(rev)
202 self.mtnloadmanifest(rev)
201 return name in self.files
203 return name in self.files
202
204
203 def mtnisdir(self, name, rev):
205 def mtnisdir(self, name, rev):
204 self.mtnloadmanifest(rev)
206 self.mtnloadmanifest(rev)
205 return name in self.dirs
207 return name in self.dirs
206
208
207 def mtngetcerts(self, rev):
209 def mtngetcerts(self, rev):
208 certs = {"author":"<missing>", "date":"<missing>",
210 certs = {"author":"<missing>", "date":"<missing>",
209 "changelog":"<missing>", "branch":"<missing>"}
211 "changelog":"<missing>", "branch":"<missing>"}
210 certlist = self.mtnrun("certs", rev)
212 certlist = self.mtnrun("certs", rev)
211 # mtn < 0.45:
213 # mtn < 0.45:
212 # key "test@selenic.com"
214 # key "test@selenic.com"
213 # mtn >= 0.45:
215 # mtn >= 0.45:
214 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
215 certlist = re.split('\n\n key ["\[]', certlist)
217 certlist = re.split('\n\n key ["\[]', certlist)
216 for e in certlist:
218 for e in certlist:
217 m = self.cert_re.match(e)
219 m = self.cert_re.match(e)
218 if m:
220 if m:
219 name, value = m.groups()
221 name, value = m.groups()
220 value = value.replace(r'\"', '"')
222 value = value.replace(r'\"', '"')
221 value = value.replace(r'\\', '\\')
223 value = value.replace(r'\\', '\\')
222 certs[name] = value
224 certs[name] = value
223 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
224 # and all times are stored in UTC
226 # and all times are stored in UTC
225 certs["date"] = certs["date"].split('.')[0] + " UTC"
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
226 return certs
228 return certs
227
229
228 # implement the converter_source interface:
230 # implement the converter_source interface:
229
231
230 def getheads(self):
232 def getheads(self):
231 if not self.revs:
233 if not self.revs:
232 return self.mtnrun("leaves").splitlines()
234 return self.mtnrun("leaves").splitlines()
233 else:
235 else:
234 return self.revs
236 return self.revs
235
237
236 def getchanges(self, rev, full):
238 def getchanges(self, rev, full):
237 if full:
239 if full:
238 raise error.Abort(_("convert from monotone does not support "
240 raise error.Abort(_("convert from monotone does not support "
239 "--full"))
241 "--full"))
240 revision = self.mtnrun("get_revision", rev).split("\n\n")
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
241 files = {}
243 files = {}
242 ignoremove = {}
244 ignoremove = {}
243 renameddirs = []
245 renameddirs = []
244 copies = {}
246 copies = {}
245 for e in revision:
247 for e in revision:
246 m = self.add_file_re.match(e)
248 m = self.add_file_re.match(e)
247 if m:
249 if m:
248 files[m.group(1)] = rev
250 files[m.group(1)] = rev
249 ignoremove[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
250 m = self.patch_re.match(e)
252 m = self.patch_re.match(e)
251 if m:
253 if m:
252 files[m.group(1)] = rev
254 files[m.group(1)] = rev
253 # Delete/rename is handled later when the convert engine
255 # Delete/rename is handled later when the convert engine
254 # discovers an IOError exception from getfile,
256 # discovers an IOError exception from getfile,
255 # but only if we add the "from" file to the list of changes.
257 # but only if we add the "from" file to the list of changes.
256 m = self.delete_re.match(e)
258 m = self.delete_re.match(e)
257 if m:
259 if m:
258 files[m.group(1)] = rev
260 files[m.group(1)] = rev
259 m = self.rename_re.match(e)
261 m = self.rename_re.match(e)
260 if m:
262 if m:
261 toname = m.group(2)
263 toname = m.group(2)
262 fromname = m.group(1)
264 fromname = m.group(1)
263 if self.mtnisfile(toname, rev):
265 if self.mtnisfile(toname, rev):
264 ignoremove[toname] = 1
266 ignoremove[toname] = 1
265 copies[toname] = fromname
267 copies[toname] = fromname
266 files[toname] = rev
268 files[toname] = rev
267 files[fromname] = rev
269 files[fromname] = rev
268 elif self.mtnisdir(toname, rev):
270 elif self.mtnisdir(toname, rev):
269 renameddirs.append((fromname, toname))
271 renameddirs.append((fromname, toname))
270
272
271 # Directory renames can be handled only once we have recorded
273 # Directory renames can be handled only once we have recorded
272 # all new files
274 # all new files
273 for fromdir, todir in renameddirs:
275 for fromdir, todir in renameddirs:
274 renamed = {}
276 renamed = {}
275 for tofile in self.files:
277 for tofile in self.files:
276 if tofile in ignoremove:
278 if tofile in ignoremove:
277 continue
279 continue
278 if tofile.startswith(todir + '/'):
280 if tofile.startswith(todir + '/'):
279 renamed[tofile] = fromdir + tofile[len(todir):]
281 renamed[tofile] = fromdir + tofile[len(todir):]
280 # Avoid chained moves like:
282 # Avoid chained moves like:
281 # d1(/a) => d3/d1(/a)
283 # d1(/a) => d3/d1(/a)
282 # d2 => d3
284 # d2 => d3
283 ignoremove[tofile] = 1
285 ignoremove[tofile] = 1
284 for tofile, fromfile in renamed.items():
286 for tofile, fromfile in renamed.items():
285 self.ui.debug (_("copying file in renamed directory "
287 self.ui.debug (_("copying file in renamed directory "
286 "from '%s' to '%s'")
288 "from '%s' to '%s'")
287 % (fromfile, tofile), '\n')
289 % (fromfile, tofile), '\n')
288 files[tofile] = rev
290 files[tofile] = rev
289 copies[tofile] = fromfile
291 copies[tofile] = fromfile
290 for fromfile in renamed.values():
292 for fromfile in renamed.values():
291 files[fromfile] = rev
293 files[fromfile] = rev
292
294
293 return (files.items(), copies, set())
295 return (files.items(), copies, set())
294
296
295 def getfile(self, name, rev):
297 def getfile(self, name, rev):
296 if not self.mtnisfile(name, rev):
298 if not self.mtnisfile(name, rev):
297 return None, None
299 return None, None
298 try:
300 try:
299 data = self.mtnrun("get_file_of", name, r=rev)
301 data = self.mtnrun("get_file_of", name, r=rev)
300 except Exception:
302 except Exception:
301 return None, None
303 return None, None
302 self.mtnloadmanifest(rev)
304 self.mtnloadmanifest(rev)
303 node, attr = self.files.get(name, (None, ""))
305 node, attr = self.files.get(name, (None, ""))
304 return data, attr
306 return data, attr
305
307
306 def getcommit(self, rev):
308 def getcommit(self, rev):
307 extra = {}
309 extra = {}
308 certs = self.mtngetcerts(rev)
310 certs = self.mtngetcerts(rev)
309 if certs.get('suspend') == certs["branch"]:
311 if certs.get('suspend') == certs["branch"]:
310 extra['close'] = 1
312 extra['close'] = 1
311 return common.commit(
313 return common.commit(
312 author=certs["author"],
314 author=certs["author"],
313 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
315 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
314 desc=certs["changelog"],
316 desc=certs["changelog"],
315 rev=rev,
317 rev=rev,
316 parents=self.mtnrun("parents", rev).splitlines(),
318 parents=self.mtnrun("parents", rev).splitlines(),
317 branch=certs["branch"],
319 branch=certs["branch"],
318 extra=extra)
320 extra=extra)
319
321
320 def gettags(self):
322 def gettags(self):
321 tags = {}
323 tags = {}
322 for e in self.mtnrun("tags").split("\n\n"):
324 for e in self.mtnrun("tags").split("\n\n"):
323 m = self.tag_re.match(e)
325 m = self.tag_re.match(e)
324 if m:
326 if m:
325 tags[m.group(1)] = m.group(2)
327 tags[m.group(1)] = m.group(2)
326 return tags
328 return tags
327
329
328 def getchangedfiles(self, rev, i):
330 def getchangedfiles(self, rev, i):
329 # This function is only needed to support --filemap
331 # This function is only needed to support --filemap
330 # ... and we don't support that
332 # ... and we don't support that
331 raise NotImplementedError
333 raise NotImplementedError
332
334
333 def before(self):
335 def before(self):
334 # Check if we have a new enough version to use automate stdio
336 # Check if we have a new enough version to use automate stdio
335 version = 0.0
337 version = 0.0
336 try:
338 try:
337 versionstr = self.mtnrunsingle("interface_version")
339 versionstr = self.mtnrunsingle("interface_version")
338 version = float(versionstr)
340 version = float(versionstr)
339 except Exception:
341 except Exception:
340 raise error.Abort(_("unable to determine mtn automate interface "
342 raise error.Abort(_("unable to determine mtn automate interface "
341 "version"))
343 "version"))
342
344
343 if version >= 12.0:
345 if version >= 12.0:
344 self.automatestdio = True
346 self.automatestdio = True
345 self.ui.debug("mtn automate version %s - using automate stdio\n" %
347 self.ui.debug("mtn automate version %s - using automate stdio\n" %
346 version)
348 version)
347
349
348 # launch the long-running automate stdio process
350 # launch the long-running automate stdio process
349 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
350 '-d', self.path)
352 '-d', self.path)
351 # read the headers
353 # read the headers
352 read = self.mtnreadfp.readline()
354 read = self.mtnreadfp.readline()
353 if read != 'format-version: 2\n':
355 if read != 'format-version: 2\n':
354 raise error.Abort(_('mtn automate stdio header unexpected: %s')
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
355 % read)
357 % read)
356 while read != '\n':
358 while read != '\n':
357 read = self.mtnreadfp.readline()
359 read = self.mtnreadfp.readline()
358 if not read:
360 if not read:
359 raise error.Abort(_("failed to reach end of mtn automate "
361 raise error.Abort(_("failed to reach end of mtn automate "
360 "stdio headers"))
362 "stdio headers"))
361 else:
363 else:
362 self.ui.debug("mtn automate version %s - not using automate stdio "
364 self.ui.debug("mtn automate version %s - not using automate stdio "
363 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
364
366
365 def after(self):
367 def after(self):
366 if self.automatestdio:
368 if self.automatestdio:
367 self.mtnwritefp.close()
369 self.mtnwritefp.close()
368 self.mtnwritefp = None
370 self.mtnwritefp = None
369 self.mtnreadfp.close()
371 self.mtnreadfp.close()
370 self.mtnreadfp = None
372 self.mtnreadfp = None
371
373
General Comments 0
You need to be logged in to leave comments. Login now