##// END OF EJS Templates
cvsps: wrap cmp methods (deprecated) in functools.cmp_to_key...
Augie Fackler -
r37903:fe148d75 default
parent child Browse files
Show More
@@ -1,958 +1,959 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 functools
9 import os
10 import os
10 import re
11 import re
11
12
12 from mercurial.i18n import _
13 from mercurial.i18n import _
13 from mercurial import (
14 from mercurial import (
14 encoding,
15 encoding,
15 error,
16 error,
16 hook,
17 hook,
17 pycompat,
18 pycompat,
18 util,
19 util,
19 )
20 )
20 from mercurial.utils import (
21 from mercurial.utils import (
21 dateutil,
22 dateutil,
22 procutil,
23 procutil,
23 stringutil,
24 stringutil,
24 )
25 )
25
26
26 pickle = util.pickle
27 pickle = util.pickle
27
28
28 class logentry(object):
29 class logentry(object):
29 '''Class logentry has the following attributes:
30 '''Class logentry has the following attributes:
30 .author - author name as CVS knows it
31 .author - author name as CVS knows it
31 .branch - name of branch this revision is on
32 .branch - name of branch this revision is on
32 .branches - revision tuple of branches starting at this revision
33 .branches - revision tuple of branches starting at this revision
33 .comment - commit message
34 .comment - commit message
34 .commitid - CVS commitid or None
35 .commitid - CVS commitid or None
35 .date - the commit date as a (time, tz) tuple
36 .date - the commit date as a (time, tz) tuple
36 .dead - true if file revision is dead
37 .dead - true if file revision is dead
37 .file - Name of file
38 .file - Name of file
38 .lines - a tuple (+lines, -lines) or None
39 .lines - a tuple (+lines, -lines) or None
39 .parent - Previous revision of this entry
40 .parent - Previous revision of this entry
40 .rcs - name of file as returned from CVS
41 .rcs - name of file as returned from CVS
41 .revision - revision number as tuple
42 .revision - revision number as tuple
42 .tags - list of tags on the file
43 .tags - list of tags on the file
43 .synthetic - is this a synthetic "file ... added on ..." revision?
44 .synthetic - is this a synthetic "file ... added on ..." revision?
44 .mergepoint - the branch that has been merged from (if present in
45 .mergepoint - the branch that has been merged from (if present in
45 rlog output) or None
46 rlog output) or None
46 .branchpoints - the branches that start at the current entry or empty
47 .branchpoints - the branches that start at the current entry or empty
47 '''
48 '''
48 def __init__(self, **entries):
49 def __init__(self, **entries):
49 self.synthetic = False
50 self.synthetic = False
50 self.__dict__.update(entries)
51 self.__dict__.update(entries)
51
52
52 def __repr__(self):
53 def __repr__(self):
53 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
54 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
54 return "%s(%s)"%(type(self).__name__, ", ".join(items))
55 return "%s(%s)"%(type(self).__name__, ", ".join(items))
55
56
56 class logerror(Exception):
57 class logerror(Exception):
57 pass
58 pass
58
59
59 def getrepopath(cvspath):
60 def getrepopath(cvspath):
60 """Return the repository path from a CVS path.
61 """Return the repository path from a CVS path.
61
62
62 >>> getrepopath(b'/foo/bar')
63 >>> getrepopath(b'/foo/bar')
63 '/foo/bar'
64 '/foo/bar'
64 >>> getrepopath(b'c:/foo/bar')
65 >>> getrepopath(b'c:/foo/bar')
65 '/foo/bar'
66 '/foo/bar'
66 >>> getrepopath(b':pserver:10/foo/bar')
67 >>> getrepopath(b':pserver:10/foo/bar')
67 '/foo/bar'
68 '/foo/bar'
68 >>> getrepopath(b':pserver:10c:/foo/bar')
69 >>> getrepopath(b':pserver:10c:/foo/bar')
69 '/foo/bar'
70 '/foo/bar'
70 >>> getrepopath(b':pserver:/foo/bar')
71 >>> getrepopath(b':pserver:/foo/bar')
71 '/foo/bar'
72 '/foo/bar'
72 >>> getrepopath(b':pserver:c:/foo/bar')
73 >>> getrepopath(b':pserver:c:/foo/bar')
73 '/foo/bar'
74 '/foo/bar'
74 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
75 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
75 '/foo/bar'
76 '/foo/bar'
76 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
77 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
77 '/foo/bar'
78 '/foo/bar'
78 >>> getrepopath(b'user@server/path/to/repository')
79 >>> getrepopath(b'user@server/path/to/repository')
79 '/path/to/repository'
80 '/path/to/repository'
80 """
81 """
81 # According to CVS manual, CVS paths are expressed like:
82 # According to CVS manual, CVS paths are expressed like:
82 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
83 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
83 #
84 #
84 # CVSpath is splitted into parts and then position of the first occurrence
85 # CVSpath is splitted into parts and then position of the first occurrence
85 # of the '/' char after the '@' is located. The solution is the rest of the
86 # of the '/' char after the '@' is located. The solution is the rest of the
86 # string after that '/' sign including it
87 # string after that '/' sign including it
87
88
88 parts = cvspath.split(':')
89 parts = cvspath.split(':')
89 atposition = parts[-1].find('@')
90 atposition = parts[-1].find('@')
90 start = 0
91 start = 0
91
92
92 if atposition != -1:
93 if atposition != -1:
93 start = atposition
94 start = atposition
94
95
95 repopath = parts[-1][parts[-1].find('/', start):]
96 repopath = parts[-1][parts[-1].find('/', start):]
96 return repopath
97 return repopath
97
98
98 def createlog(ui, directory=None, root="", rlog=True, cache=None):
99 def createlog(ui, directory=None, root="", rlog=True, cache=None):
99 '''Collect the CVS rlog'''
100 '''Collect the CVS rlog'''
100
101
101 # Because we store many duplicate commit log messages, reusing strings
102 # Because we store many duplicate commit log messages, reusing strings
102 # saves a lot of memory and pickle storage space.
103 # saves a lot of memory and pickle storage space.
103 _scache = {}
104 _scache = {}
104 def scache(s):
105 def scache(s):
105 "return a shared version of a string"
106 "return a shared version of a string"
106 return _scache.setdefault(s, s)
107 return _scache.setdefault(s, s)
107
108
108 ui.status(_('collecting CVS rlog\n'))
109 ui.status(_('collecting CVS rlog\n'))
109
110
110 log = [] # list of logentry objects containing the CVS state
111 log = [] # list of logentry objects containing the CVS state
111
112
112 # patterns to match in CVS (r)log output, by state of use
113 # patterns to match in CVS (r)log output, by state of use
113 re_00 = re.compile(b'RCS file: (.+)$')
114 re_00 = re.compile(b'RCS file: (.+)$')
114 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
115 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
115 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
116 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
116 re_03 = re.compile(b"(Cannot access.+CVSROOT)|"
117 re_03 = re.compile(b"(Cannot access.+CVSROOT)|"
117 b"(can't create temporary directory.+)$")
118 b"(can't create temporary directory.+)$")
118 re_10 = re.compile(b'Working file: (.+)$')
119 re_10 = re.compile(b'Working file: (.+)$')
119 re_20 = re.compile(b'symbolic names:')
120 re_20 = re.compile(b'symbolic names:')
120 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
121 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
121 re_31 = re.compile(b'----------------------------$')
122 re_31 = re.compile(b'----------------------------$')
122 re_32 = re.compile(b'======================================='
123 re_32 = re.compile(b'======================================='
123 b'======================================$')
124 b'======================================$')
124 re_50 = re.compile(b'revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
125 re_50 = re.compile(b'revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
125 re_60 = re.compile(br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
126 re_60 = re.compile(br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
126 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
127 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
127 br'(\s+commitid:\s+([^;]+);)?'
128 br'(\s+commitid:\s+([^;]+);)?'
128 br'(.*mergepoint:\s+([^;]+);)?')
129 br'(.*mergepoint:\s+([^;]+);)?')
129 re_70 = re.compile(b'branches: (.+);$')
130 re_70 = re.compile(b'branches: (.+);$')
130
131
131 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
132 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
132
133
133 prefix = '' # leading path to strip of what we get from CVS
134 prefix = '' # leading path to strip of what we get from CVS
134
135
135 if directory is None:
136 if directory is None:
136 # Current working directory
137 # Current working directory
137
138
138 # Get the real directory in the repository
139 # Get the real directory in the repository
139 try:
140 try:
140 prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip()
141 prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip()
141 directory = prefix
142 directory = prefix
142 if prefix == ".":
143 if prefix == ".":
143 prefix = ""
144 prefix = ""
144 except IOError:
145 except IOError:
145 raise logerror(_('not a CVS sandbox'))
146 raise logerror(_('not a CVS sandbox'))
146
147
147 if prefix and not prefix.endswith(pycompat.ossep):
148 if prefix and not prefix.endswith(pycompat.ossep):
148 prefix += pycompat.ossep
149 prefix += pycompat.ossep
149
150
150 # Use the Root file in the sandbox, if it exists
151 # Use the Root file in the sandbox, if it exists
151 try:
152 try:
152 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
153 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
153 except IOError:
154 except IOError:
154 pass
155 pass
155
156
156 if not root:
157 if not root:
157 root = encoding.environ.get('CVSROOT', '')
158 root = encoding.environ.get('CVSROOT', '')
158
159
159 # read log cache if one exists
160 # read log cache if one exists
160 oldlog = []
161 oldlog = []
161 date = None
162 date = None
162
163
163 if cache:
164 if cache:
164 cachedir = os.path.expanduser('~/.hg.cvsps')
165 cachedir = os.path.expanduser('~/.hg.cvsps')
165 if not os.path.exists(cachedir):
166 if not os.path.exists(cachedir):
166 os.mkdir(cachedir)
167 os.mkdir(cachedir)
167
168
168 # The cvsps cache pickle needs a uniquified name, based on the
169 # The cvsps cache pickle needs a uniquified name, based on the
169 # repository location. The address may have all sort of nasties
170 # repository location. The address may have all sort of nasties
170 # in it, slashes, colons and such. So here we take just the
171 # in it, slashes, colons and such. So here we take just the
171 # alphanumeric characters, concatenated in a way that does not
172 # alphanumeric characters, concatenated in a way that does not
172 # mix up the various components, so that
173 # mix up the various components, so that
173 # :pserver:user@server:/path
174 # :pserver:user@server:/path
174 # and
175 # and
175 # /pserver/user/server/path
176 # /pserver/user/server/path
176 # are mapped to different cache file names.
177 # are mapped to different cache file names.
177 cachefile = root.split(":") + [directory, "cache"]
178 cachefile = root.split(":") + [directory, "cache"]
178 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
179 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
179 cachefile = os.path.join(cachedir,
180 cachefile = os.path.join(cachedir,
180 '.'.join([s for s in cachefile if s]))
181 '.'.join([s for s in cachefile if s]))
181
182
182 if cache == 'update':
183 if cache == 'update':
183 try:
184 try:
184 ui.note(_('reading cvs log cache %s\n') % cachefile)
185 ui.note(_('reading cvs log cache %s\n') % cachefile)
185 oldlog = pickle.load(open(cachefile, 'rb'))
186 oldlog = pickle.load(open(cachefile, 'rb'))
186 for e in oldlog:
187 for e in oldlog:
187 if not (util.safehasattr(e, 'branchpoints') and
188 if not (util.safehasattr(e, 'branchpoints') and
188 util.safehasattr(e, 'commitid') and
189 util.safehasattr(e, 'commitid') and
189 util.safehasattr(e, 'mergepoint')):
190 util.safehasattr(e, 'mergepoint')):
190 ui.status(_('ignoring old cache\n'))
191 ui.status(_('ignoring old cache\n'))
191 oldlog = []
192 oldlog = []
192 break
193 break
193
194
194 ui.note(_('cache has %d log entries\n') % len(oldlog))
195 ui.note(_('cache has %d log entries\n') % len(oldlog))
195 except Exception as e:
196 except Exception as e:
196 ui.note(_('error reading cache: %r\n') % e)
197 ui.note(_('error reading cache: %r\n') % e)
197
198
198 if oldlog:
199 if oldlog:
199 date = oldlog[-1].date # last commit date as a (time,tz) tuple
200 date = oldlog[-1].date # last commit date as a (time,tz) tuple
200 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
201 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
201
202
202 # build the CVS commandline
203 # build the CVS commandline
203 cmd = ['cvs', '-q']
204 cmd = ['cvs', '-q']
204 if root:
205 if root:
205 cmd.append('-d%s' % root)
206 cmd.append('-d%s' % root)
206 p = util.normpath(getrepopath(root))
207 p = util.normpath(getrepopath(root))
207 if not p.endswith('/'):
208 if not p.endswith('/'):
208 p += '/'
209 p += '/'
209 if prefix:
210 if prefix:
210 # looks like normpath replaces "" by "."
211 # looks like normpath replaces "" by "."
211 prefix = p + util.normpath(prefix)
212 prefix = p + util.normpath(prefix)
212 else:
213 else:
213 prefix = p
214 prefix = p
214 cmd.append(['log', 'rlog'][rlog])
215 cmd.append(['log', 'rlog'][rlog])
215 if date:
216 if date:
216 # no space between option and date string
217 # no space between option and date string
217 cmd.append('-d>%s' % date)
218 cmd.append('-d>%s' % date)
218 cmd.append(directory)
219 cmd.append(directory)
219
220
220 # state machine begins here
221 # state machine begins here
221 tags = {} # dictionary of revisions on current file with their tags
222 tags = {} # dictionary of revisions on current file with their tags
222 branchmap = {} # mapping between branch names and revision numbers
223 branchmap = {} # mapping between branch names and revision numbers
223 rcsmap = {}
224 rcsmap = {}
224 state = 0
225 state = 0
225 store = False # set when a new record can be appended
226 store = False # set when a new record can be appended
226
227
227 cmd = [procutil.shellquote(arg) for arg in cmd]
228 cmd = [procutil.shellquote(arg) for arg in cmd]
228 ui.note(_("running %s\n") % (' '.join(cmd)))
229 ui.note(_("running %s\n") % (' '.join(cmd)))
229 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
230 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
230
231
231 pfp = procutil.popen(' '.join(cmd), 'rb')
232 pfp = procutil.popen(' '.join(cmd), 'rb')
232 peek = util.fromnativeeol(pfp.readline())
233 peek = util.fromnativeeol(pfp.readline())
233 while True:
234 while True:
234 line = peek
235 line = peek
235 if line == '':
236 if line == '':
236 break
237 break
237 peek = util.fromnativeeol(pfp.readline())
238 peek = util.fromnativeeol(pfp.readline())
238 if line.endswith('\n'):
239 if line.endswith('\n'):
239 line = line[:-1]
240 line = line[:-1]
240 #ui.debug('state=%d line=%r\n' % (state, line))
241 #ui.debug('state=%d line=%r\n' % (state, line))
241
242
242 if state == 0:
243 if state == 0:
243 # initial state, consume input until we see 'RCS file'
244 # initial state, consume input until we see 'RCS file'
244 match = re_00.match(line)
245 match = re_00.match(line)
245 if match:
246 if match:
246 rcs = match.group(1)
247 rcs = match.group(1)
247 tags = {}
248 tags = {}
248 if rlog:
249 if rlog:
249 filename = util.normpath(rcs[:-2])
250 filename = util.normpath(rcs[:-2])
250 if filename.startswith(prefix):
251 if filename.startswith(prefix):
251 filename = filename[len(prefix):]
252 filename = filename[len(prefix):]
252 if filename.startswith('/'):
253 if filename.startswith('/'):
253 filename = filename[1:]
254 filename = filename[1:]
254 if filename.startswith('Attic/'):
255 if filename.startswith('Attic/'):
255 filename = filename[6:]
256 filename = filename[6:]
256 else:
257 else:
257 filename = filename.replace('/Attic/', '/')
258 filename = filename.replace('/Attic/', '/')
258 state = 2
259 state = 2
259 continue
260 continue
260 state = 1
261 state = 1
261 continue
262 continue
262 match = re_01.match(line)
263 match = re_01.match(line)
263 if match:
264 if match:
264 raise logerror(match.group(1))
265 raise logerror(match.group(1))
265 match = re_02.match(line)
266 match = re_02.match(line)
266 if match:
267 if match:
267 raise logerror(match.group(2))
268 raise logerror(match.group(2))
268 if re_03.match(line):
269 if re_03.match(line):
269 raise logerror(line)
270 raise logerror(line)
270
271
271 elif state == 1:
272 elif state == 1:
272 # expect 'Working file' (only when using log instead of rlog)
273 # expect 'Working file' (only when using log instead of rlog)
273 match = re_10.match(line)
274 match = re_10.match(line)
274 assert match, _('RCS file must be followed by working file')
275 assert match, _('RCS file must be followed by working file')
275 filename = util.normpath(match.group(1))
276 filename = util.normpath(match.group(1))
276 state = 2
277 state = 2
277
278
278 elif state == 2:
279 elif state == 2:
279 # expect 'symbolic names'
280 # expect 'symbolic names'
280 if re_20.match(line):
281 if re_20.match(line):
281 branchmap = {}
282 branchmap = {}
282 state = 3
283 state = 3
283
284
284 elif state == 3:
285 elif state == 3:
285 # read the symbolic names and store as tags
286 # read the symbolic names and store as tags
286 match = re_30.match(line)
287 match = re_30.match(line)
287 if match:
288 if match:
288 rev = [int(x) for x in match.group(2).split('.')]
289 rev = [int(x) for x in match.group(2).split('.')]
289
290
290 # Convert magic branch number to an odd-numbered one
291 # Convert magic branch number to an odd-numbered one
291 revn = len(rev)
292 revn = len(rev)
292 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
293 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
293 rev = rev[:-2] + rev[-1:]
294 rev = rev[:-2] + rev[-1:]
294 rev = tuple(rev)
295 rev = tuple(rev)
295
296
296 if rev not in tags:
297 if rev not in tags:
297 tags[rev] = []
298 tags[rev] = []
298 tags[rev].append(match.group(1))
299 tags[rev].append(match.group(1))
299 branchmap[match.group(1)] = match.group(2)
300 branchmap[match.group(1)] = match.group(2)
300
301
301 elif re_31.match(line):
302 elif re_31.match(line):
302 state = 5
303 state = 5
303 elif re_32.match(line):
304 elif re_32.match(line):
304 state = 0
305 state = 0
305
306
306 elif state == 4:
307 elif state == 4:
307 # expecting '------' separator before first revision
308 # expecting '------' separator before first revision
308 if re_31.match(line):
309 if re_31.match(line):
309 state = 5
310 state = 5
310 else:
311 else:
311 assert not re_32.match(line), _('must have at least '
312 assert not re_32.match(line), _('must have at least '
312 'some revisions')
313 'some revisions')
313
314
314 elif state == 5:
315 elif state == 5:
315 # expecting revision number and possibly (ignored) lock indication
316 # expecting revision number and possibly (ignored) lock indication
316 # we create the logentry here from values stored in states 0 to 4,
317 # we create the logentry here from values stored in states 0 to 4,
317 # as this state is re-entered for subsequent revisions of a file.
318 # as this state is re-entered for subsequent revisions of a file.
318 match = re_50.match(line)
319 match = re_50.match(line)
319 assert match, _('expected revision number')
320 assert match, _('expected revision number')
320 e = logentry(rcs=scache(rcs),
321 e = logentry(rcs=scache(rcs),
321 file=scache(filename),
322 file=scache(filename),
322 revision=tuple([int(x) for x in
323 revision=tuple([int(x) for x in
323 match.group(1).split('.')]),
324 match.group(1).split('.')]),
324 branches=[],
325 branches=[],
325 parent=None,
326 parent=None,
326 commitid=None,
327 commitid=None,
327 mergepoint=None,
328 mergepoint=None,
328 branchpoints=set())
329 branchpoints=set())
329
330
330 state = 6
331 state = 6
331
332
332 elif state == 6:
333 elif state == 6:
333 # expecting date, author, state, lines changed
334 # expecting date, author, state, lines changed
334 match = re_60.match(line)
335 match = re_60.match(line)
335 assert match, _('revision must be followed by date line')
336 assert match, _('revision must be followed by date line')
336 d = match.group(1)
337 d = match.group(1)
337 if d[2] == '/':
338 if d[2] == '/':
338 # Y2K
339 # Y2K
339 d = '19' + d
340 d = '19' + d
340
341
341 if len(d.split()) != 3:
342 if len(d.split()) != 3:
342 # cvs log dates always in GMT
343 # cvs log dates always in GMT
343 d = d + ' UTC'
344 d = d + ' UTC'
344 e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S',
345 e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S',
345 '%Y/%m/%d %H:%M:%S',
346 '%Y/%m/%d %H:%M:%S',
346 '%Y-%m-%d %H:%M:%S'])
347 '%Y-%m-%d %H:%M:%S'])
347 e.author = scache(match.group(2))
348 e.author = scache(match.group(2))
348 e.dead = match.group(3).lower() == 'dead'
349 e.dead = match.group(3).lower() == 'dead'
349
350
350 if match.group(5):
351 if match.group(5):
351 if match.group(6):
352 if match.group(6):
352 e.lines = (int(match.group(5)), int(match.group(6)))
353 e.lines = (int(match.group(5)), int(match.group(6)))
353 else:
354 else:
354 e.lines = (int(match.group(5)), 0)
355 e.lines = (int(match.group(5)), 0)
355 elif match.group(6):
356 elif match.group(6):
356 e.lines = (0, int(match.group(6)))
357 e.lines = (0, int(match.group(6)))
357 else:
358 else:
358 e.lines = None
359 e.lines = None
359
360
360 if match.group(7): # cvs 1.12 commitid
361 if match.group(7): # cvs 1.12 commitid
361 e.commitid = match.group(8)
362 e.commitid = match.group(8)
362
363
363 if match.group(9): # cvsnt mergepoint
364 if match.group(9): # cvsnt mergepoint
364 myrev = match.group(10).split('.')
365 myrev = match.group(10).split('.')
365 if len(myrev) == 2: # head
366 if len(myrev) == 2: # head
366 e.mergepoint = 'HEAD'
367 e.mergepoint = 'HEAD'
367 else:
368 else:
368 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
369 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
369 branches = [b for b in branchmap if branchmap[b] == myrev]
370 branches = [b for b in branchmap if branchmap[b] == myrev]
370 assert len(branches) == 1, ('unknown branch: %s'
371 assert len(branches) == 1, ('unknown branch: %s'
371 % e.mergepoint)
372 % e.mergepoint)
372 e.mergepoint = branches[0]
373 e.mergepoint = branches[0]
373
374
374 e.comment = []
375 e.comment = []
375 state = 7
376 state = 7
376
377
377 elif state == 7:
378 elif state == 7:
378 # read the revision numbers of branches that start at this revision
379 # read the revision numbers of branches that start at this revision
379 # or store the commit log message otherwise
380 # or store the commit log message otherwise
380 m = re_70.match(line)
381 m = re_70.match(line)
381 if m:
382 if m:
382 e.branches = [tuple([int(y) for y in x.strip().split('.')])
383 e.branches = [tuple([int(y) for y in x.strip().split('.')])
383 for x in m.group(1).split(';')]
384 for x in m.group(1).split(';')]
384 state = 8
385 state = 8
385 elif re_31.match(line) and re_50.match(peek):
386 elif re_31.match(line) and re_50.match(peek):
386 state = 5
387 state = 5
387 store = True
388 store = True
388 elif re_32.match(line):
389 elif re_32.match(line):
389 state = 0
390 state = 0
390 store = True
391 store = True
391 else:
392 else:
392 e.comment.append(line)
393 e.comment.append(line)
393
394
394 elif state == 8:
395 elif state == 8:
395 # store commit log message
396 # store commit log message
396 if re_31.match(line):
397 if re_31.match(line):
397 cpeek = peek
398 cpeek = peek
398 if cpeek.endswith('\n'):
399 if cpeek.endswith('\n'):
399 cpeek = cpeek[:-1]
400 cpeek = cpeek[:-1]
400 if re_50.match(cpeek):
401 if re_50.match(cpeek):
401 state = 5
402 state = 5
402 store = True
403 store = True
403 else:
404 else:
404 e.comment.append(line)
405 e.comment.append(line)
405 elif re_32.match(line):
406 elif re_32.match(line):
406 state = 0
407 state = 0
407 store = True
408 store = True
408 else:
409 else:
409 e.comment.append(line)
410 e.comment.append(line)
410
411
411 # When a file is added on a branch B1, CVS creates a synthetic
412 # When a file is added on a branch B1, CVS creates a synthetic
412 # dead trunk revision 1.1 so that the branch has a root.
413 # dead trunk revision 1.1 so that the branch has a root.
413 # Likewise, if you merge such a file to a later branch B2 (one
414 # Likewise, if you merge such a file to a later branch B2 (one
414 # that already existed when the file was added on B1), CVS
415 # that already existed when the file was added on B1), CVS
415 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
416 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
416 # these revisions now, but mark them synthetic so
417 # these revisions now, but mark them synthetic so
417 # createchangeset() can take care of them.
418 # createchangeset() can take care of them.
418 if (store and
419 if (store and
419 e.dead and
420 e.dead and
420 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
421 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
421 len(e.comment) == 1 and
422 len(e.comment) == 1 and
422 file_added_re.match(e.comment[0])):
423 file_added_re.match(e.comment[0])):
423 ui.debug('found synthetic revision in %s: %r\n'
424 ui.debug('found synthetic revision in %s: %r\n'
424 % (e.rcs, e.comment[0]))
425 % (e.rcs, e.comment[0]))
425 e.synthetic = True
426 e.synthetic = True
426
427
427 if store:
428 if store:
428 # clean up the results and save in the log.
429 # clean up the results and save in the log.
429 store = False
430 store = False
430 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
431 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
431 e.comment = scache('\n'.join(e.comment))
432 e.comment = scache('\n'.join(e.comment))
432
433
433 revn = len(e.revision)
434 revn = len(e.revision)
434 if revn > 3 and (revn % 2) == 0:
435 if revn > 3 and (revn % 2) == 0:
435 e.branch = tags.get(e.revision[:-1], [None])[0]
436 e.branch = tags.get(e.revision[:-1], [None])[0]
436 else:
437 else:
437 e.branch = None
438 e.branch = None
438
439
439 # find the branches starting from this revision
440 # find the branches starting from this revision
440 branchpoints = set()
441 branchpoints = set()
441 for branch, revision in branchmap.iteritems():
442 for branch, revision in branchmap.iteritems():
442 revparts = tuple([int(i) for i in revision.split('.')])
443 revparts = tuple([int(i) for i in revision.split('.')])
443 if len(revparts) < 2: # bad tags
444 if len(revparts) < 2: # bad tags
444 continue
445 continue
445 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
446 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
446 # normal branch
447 # normal branch
447 if revparts[:-2] == e.revision:
448 if revparts[:-2] == e.revision:
448 branchpoints.add(branch)
449 branchpoints.add(branch)
449 elif revparts == (1, 1, 1): # vendor branch
450 elif revparts == (1, 1, 1): # vendor branch
450 if revparts in e.branches:
451 if revparts in e.branches:
451 branchpoints.add(branch)
452 branchpoints.add(branch)
452 e.branchpoints = branchpoints
453 e.branchpoints = branchpoints
453
454
454 log.append(e)
455 log.append(e)
455
456
456 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
457 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
457
458
458 if len(log) % 100 == 0:
459 if len(log) % 100 == 0:
459 ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80)
460 ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80)
460 + '\n')
461 + '\n')
461
462
462 log.sort(key=lambda x: (x.rcs, x.revision))
463 log.sort(key=lambda x: (x.rcs, x.revision))
463
464
464 # find parent revisions of individual files
465 # find parent revisions of individual files
465 versions = {}
466 versions = {}
466 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
467 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
467 rcs = e.rcs.replace('/Attic/', '/')
468 rcs = e.rcs.replace('/Attic/', '/')
468 if rcs in rcsmap:
469 if rcs in rcsmap:
469 e.rcs = rcsmap[rcs]
470 e.rcs = rcsmap[rcs]
470 branch = e.revision[:-1]
471 branch = e.revision[:-1]
471 versions[(e.rcs, branch)] = e.revision
472 versions[(e.rcs, branch)] = e.revision
472
473
473 for e in log:
474 for e in log:
474 branch = e.revision[:-1]
475 branch = e.revision[:-1]
475 p = versions.get((e.rcs, branch), None)
476 p = versions.get((e.rcs, branch), None)
476 if p is None:
477 if p is None:
477 p = e.revision[:-2]
478 p = e.revision[:-2]
478 e.parent = p
479 e.parent = p
479 versions[(e.rcs, branch)] = e.revision
480 versions[(e.rcs, branch)] = e.revision
480
481
481 # update the log cache
482 # update the log cache
482 if cache:
483 if cache:
483 if log:
484 if log:
484 # join up the old and new logs
485 # join up the old and new logs
485 log.sort(key=lambda x: x.date)
486 log.sort(key=lambda x: x.date)
486
487
487 if oldlog and oldlog[-1].date >= log[0].date:
488 if oldlog and oldlog[-1].date >= log[0].date:
488 raise logerror(_('log cache overlaps with new log entries,'
489 raise logerror(_('log cache overlaps with new log entries,'
489 ' re-run without cache.'))
490 ' re-run without cache.'))
490
491
491 log = oldlog + log
492 log = oldlog + log
492
493
493 # write the new cachefile
494 # write the new cachefile
494 ui.note(_('writing cvs log cache %s\n') % cachefile)
495 ui.note(_('writing cvs log cache %s\n') % cachefile)
495 pickle.dump(log, open(cachefile, 'wb'))
496 pickle.dump(log, open(cachefile, 'wb'))
496 else:
497 else:
497 log = oldlog
498 log = oldlog
498
499
499 ui.status(_('%d log entries\n') % len(log))
500 ui.status(_('%d log entries\n') % len(log))
500
501
501 encodings = ui.configlist('convert', 'cvsps.logencoding')
502 encodings = ui.configlist('convert', 'cvsps.logencoding')
502 if encodings:
503 if encodings:
503 def revstr(r):
504 def revstr(r):
504 # this is needed, because logentry.revision is a tuple of "int"
505 # this is needed, because logentry.revision is a tuple of "int"
505 # (e.g. (1, 2) for "1.2")
506 # (e.g. (1, 2) for "1.2")
506 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
507 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
507
508
508 for entry in log:
509 for entry in log:
509 comment = entry.comment
510 comment = entry.comment
510 for e in encodings:
511 for e in encodings:
511 try:
512 try:
512 entry.comment = comment.decode(e).encode('utf-8')
513 entry.comment = comment.decode(e).encode('utf-8')
513 if ui.debugflag:
514 if ui.debugflag:
514 ui.debug("transcoding by %s: %s of %s\n" %
515 ui.debug("transcoding by %s: %s of %s\n" %
515 (e, revstr(entry.revision), entry.file))
516 (e, revstr(entry.revision), entry.file))
516 break
517 break
517 except UnicodeDecodeError:
518 except UnicodeDecodeError:
518 pass # try next encoding
519 pass # try next encoding
519 except LookupError as inst: # unknown encoding, maybe
520 except LookupError as inst: # unknown encoding, maybe
520 raise error.Abort(inst,
521 raise error.Abort(inst,
521 hint=_('check convert.cvsps.logencoding'
522 hint=_('check convert.cvsps.logencoding'
522 ' configuration'))
523 ' configuration'))
523 else:
524 else:
524 raise error.Abort(_("no encoding can transcode"
525 raise error.Abort(_("no encoding can transcode"
525 " CVS log message for %s of %s")
526 " CVS log message for %s of %s")
526 % (revstr(entry.revision), entry.file),
527 % (revstr(entry.revision), entry.file),
527 hint=_('check convert.cvsps.logencoding'
528 hint=_('check convert.cvsps.logencoding'
528 ' configuration'))
529 ' configuration'))
529
530
530 hook.hook(ui, None, "cvslog", True, log=log)
531 hook.hook(ui, None, "cvslog", True, log=log)
531
532
532 return log
533 return log
533
534
534
535
535 class changeset(object):
536 class changeset(object):
536 '''Class changeset has the following attributes:
537 '''Class changeset has the following attributes:
537 .id - integer identifying this changeset (list index)
538 .id - integer identifying this changeset (list index)
538 .author - author name as CVS knows it
539 .author - author name as CVS knows it
539 .branch - name of branch this changeset is on, or None
540 .branch - name of branch this changeset is on, or None
540 .comment - commit message
541 .comment - commit message
541 .commitid - CVS commitid or None
542 .commitid - CVS commitid or None
542 .date - the commit date as a (time,tz) tuple
543 .date - the commit date as a (time,tz) tuple
543 .entries - list of logentry objects in this changeset
544 .entries - list of logentry objects in this changeset
544 .parents - list of one or two parent changesets
545 .parents - list of one or two parent changesets
545 .tags - list of tags on this changeset
546 .tags - list of tags on this changeset
546 .synthetic - from synthetic revision "file ... added on branch ..."
547 .synthetic - from synthetic revision "file ... added on branch ..."
547 .mergepoint- the branch that has been merged from or None
548 .mergepoint- the branch that has been merged from or None
548 .branchpoints- the branches that start at the current entry or empty
549 .branchpoints- the branches that start at the current entry or empty
549 '''
550 '''
550 def __init__(self, **entries):
551 def __init__(self, **entries):
551 self.id = None
552 self.id = None
552 self.synthetic = False
553 self.synthetic = False
553 self.__dict__.update(entries)
554 self.__dict__.update(entries)
554
555
555 def __repr__(self):
556 def __repr__(self):
556 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
557 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
557 return "%s(%s)"%(type(self).__name__, ", ".join(items))
558 return "%s(%s)"%(type(self).__name__, ", ".join(items))
558
559
559 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
560 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
560 '''Convert log into changesets.'''
561 '''Convert log into changesets.'''
561
562
562 ui.status(_('creating changesets\n'))
563 ui.status(_('creating changesets\n'))
563
564
564 # try to order commitids by date
565 # try to order commitids by date
565 mindate = {}
566 mindate = {}
566 for e in log:
567 for e in log:
567 if e.commitid:
568 if e.commitid:
568 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
569 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
569
570
570 # Merge changesets
571 # Merge changesets
571 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
572 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
572 x.author, x.branch, x.date, x.branchpoints))
573 x.author, x.branch, x.date, x.branchpoints))
573
574
574 changesets = []
575 changesets = []
575 files = set()
576 files = set()
576 c = None
577 c = None
577 for i, e in enumerate(log):
578 for i, e in enumerate(log):
578
579
579 # Check if log entry belongs to the current changeset or not.
580 # Check if log entry belongs to the current changeset or not.
580
581
581 # Since CVS is file-centric, two different file revisions with
582 # Since CVS is file-centric, two different file revisions with
582 # different branchpoints should be treated as belonging to two
583 # different branchpoints should be treated as belonging to two
583 # different changesets (and the ordering is important and not
584 # different changesets (and the ordering is important and not
584 # honoured by cvsps at this point).
585 # honoured by cvsps at this point).
585 #
586 #
586 # Consider the following case:
587 # Consider the following case:
587 # foo 1.1 branchpoints: [MYBRANCH]
588 # foo 1.1 branchpoints: [MYBRANCH]
588 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
589 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
589 #
590 #
590 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
591 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
591 # later version of foo may be in MYBRANCH2, so foo should be the
592 # later version of foo may be in MYBRANCH2, so foo should be the
592 # first changeset and bar the next and MYBRANCH and MYBRANCH2
593 # first changeset and bar the next and MYBRANCH and MYBRANCH2
593 # should both start off of the bar changeset. No provisions are
594 # should both start off of the bar changeset. No provisions are
594 # made to ensure that this is, in fact, what happens.
595 # made to ensure that this is, in fact, what happens.
595 if not (c and e.branchpoints == c.branchpoints and
596 if not (c and e.branchpoints == c.branchpoints and
596 (# cvs commitids
597 (# cvs commitids
597 (e.commitid is not None and e.commitid == c.commitid) or
598 (e.commitid is not None and e.commitid == c.commitid) or
598 (# no commitids, use fuzzy commit detection
599 (# no commitids, use fuzzy commit detection
599 (e.commitid is None or c.commitid is None) and
600 (e.commitid is None or c.commitid is None) and
600 e.comment == c.comment and
601 e.comment == c.comment and
601 e.author == c.author and
602 e.author == c.author and
602 e.branch == c.branch and
603 e.branch == c.branch and
603 ((c.date[0] + c.date[1]) <=
604 ((c.date[0] + c.date[1]) <=
604 (e.date[0] + e.date[1]) <=
605 (e.date[0] + e.date[1]) <=
605 (c.date[0] + c.date[1]) + fuzz) and
606 (c.date[0] + c.date[1]) + fuzz) and
606 e.file not in files))):
607 e.file not in files))):
607 c = changeset(comment=e.comment, author=e.author,
608 c = changeset(comment=e.comment, author=e.author,
608 branch=e.branch, date=e.date,
609 branch=e.branch, date=e.date,
609 entries=[], mergepoint=e.mergepoint,
610 entries=[], mergepoint=e.mergepoint,
610 branchpoints=e.branchpoints, commitid=e.commitid)
611 branchpoints=e.branchpoints, commitid=e.commitid)
611 changesets.append(c)
612 changesets.append(c)
612
613
613 files = set()
614 files = set()
614 if len(changesets) % 100 == 0:
615 if len(changesets) % 100 == 0:
615 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
616 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
616 ui.status(stringutil.ellipsis(t, 80) + '\n')
617 ui.status(stringutil.ellipsis(t, 80) + '\n')
617
618
618 c.entries.append(e)
619 c.entries.append(e)
619 files.add(e.file)
620 files.add(e.file)
620 c.date = e.date # changeset date is date of latest commit in it
621 c.date = e.date # changeset date is date of latest commit in it
621
622
622 # Mark synthetic changesets
623 # Mark synthetic changesets
623
624
624 for c in changesets:
625 for c in changesets:
625 # Synthetic revisions always get their own changeset, because
626 # Synthetic revisions always get their own changeset, because
626 # the log message includes the filename. E.g. if you add file3
627 # the log message includes the filename. E.g. if you add file3
627 # and file4 on a branch, you get four log entries and three
628 # and file4 on a branch, you get four log entries and three
628 # changesets:
629 # changesets:
629 # "File file3 was added on branch ..." (synthetic, 1 entry)
630 # "File file3 was added on branch ..." (synthetic, 1 entry)
630 # "File file4 was added on branch ..." (synthetic, 1 entry)
631 # "File file4 was added on branch ..." (synthetic, 1 entry)
631 # "Add file3 and file4 to fix ..." (real, 2 entries)
632 # "Add file3 and file4 to fix ..." (real, 2 entries)
632 # Hence the check for 1 entry here.
633 # Hence the check for 1 entry here.
633 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
634 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
634
635
635 # Sort files in each changeset
636 # Sort files in each changeset
636
637
637 def entitycompare(l, r):
638 def entitycompare(l, r):
638 'Mimic cvsps sorting order'
639 'Mimic cvsps sorting order'
639 l = l.file.split('/')
640 l = l.file.split('/')
640 r = r.file.split('/')
641 r = r.file.split('/')
641 nl = len(l)
642 nl = len(l)
642 nr = len(r)
643 nr = len(r)
643 n = min(nl, nr)
644 n = min(nl, nr)
644 for i in range(n):
645 for i in range(n):
645 if i + 1 == nl and nl < nr:
646 if i + 1 == nl and nl < nr:
646 return -1
647 return -1
647 elif i + 1 == nr and nl > nr:
648 elif i + 1 == nr and nl > nr:
648 return +1
649 return +1
649 elif l[i] < r[i]:
650 elif l[i] < r[i]:
650 return -1
651 return -1
651 elif l[i] > r[i]:
652 elif l[i] > r[i]:
652 return +1
653 return +1
653 return 0
654 return 0
654
655
655 for c in changesets:
656 for c in changesets:
656 c.entries.sort(entitycompare)
657 c.entries.sort(key=functools.cmp_to_key(entitycompare))
657
658
658 # Sort changesets by date
659 # Sort changesets by date
659
660
660 odd = set()
661 odd = set()
661 def cscmp(l, r):
662 def cscmp(l, r):
662 d = sum(l.date) - sum(r.date)
663 d = sum(l.date) - sum(r.date)
663 if d:
664 if d:
664 return d
665 return d
665
666
666 # detect vendor branches and initial commits on a branch
667 # detect vendor branches and initial commits on a branch
667 le = {}
668 le = {}
668 for e in l.entries:
669 for e in l.entries:
669 le[e.rcs] = e.revision
670 le[e.rcs] = e.revision
670 re = {}
671 re = {}
671 for e in r.entries:
672 for e in r.entries:
672 re[e.rcs] = e.revision
673 re[e.rcs] = e.revision
673
674
674 d = 0
675 d = 0
675 for e in l.entries:
676 for e in l.entries:
676 if re.get(e.rcs, None) == e.parent:
677 if re.get(e.rcs, None) == e.parent:
677 assert not d
678 assert not d
678 d = 1
679 d = 1
679 break
680 break
680
681
681 for e in r.entries:
682 for e in r.entries:
682 if le.get(e.rcs, None) == e.parent:
683 if le.get(e.rcs, None) == e.parent:
683 if d:
684 if d:
684 odd.add((l, r))
685 odd.add((l, r))
685 d = -1
686 d = -1
686 break
687 break
687 # By this point, the changesets are sufficiently compared that
688 # By this point, the changesets are sufficiently compared that
688 # we don't really care about ordering. However, this leaves
689 # we don't really care about ordering. However, this leaves
689 # some race conditions in the tests, so we compare on the
690 # some race conditions in the tests, so we compare on the
690 # number of files modified, the files contained in each
691 # number of files modified, the files contained in each
691 # changeset, and the branchpoints in the change to ensure test
692 # changeset, and the branchpoints in the change to ensure test
692 # output remains stable.
693 # output remains stable.
693
694
694 # recommended replacement for cmp from
695 # recommended replacement for cmp from
695 # https://docs.python.org/3.0/whatsnew/3.0.html
696 # https://docs.python.org/3.0/whatsnew/3.0.html
696 c = lambda x, y: (x > y) - (x < y)
697 c = lambda x, y: (x > y) - (x < y)
697 # Sort bigger changes first.
698 # Sort bigger changes first.
698 if not d:
699 if not d:
699 d = c(len(l.entries), len(r.entries))
700 d = c(len(l.entries), len(r.entries))
700 # Try sorting by filename in the change.
701 # Try sorting by filename in the change.
701 if not d:
702 if not d:
702 d = c([e.file for e in l.entries], [e.file for e in r.entries])
703 d = c([e.file for e in l.entries], [e.file for e in r.entries])
703 # Try and put changes without a branch point before ones with
704 # Try and put changes without a branch point before ones with
704 # a branch point.
705 # a branch point.
705 if not d:
706 if not d:
706 d = c(len(l.branchpoints), len(r.branchpoints))
707 d = c(len(l.branchpoints), len(r.branchpoints))
707 return d
708 return d
708
709
709 changesets.sort(cscmp)
710 changesets.sort(key=functools.cmp_to_key(cscmp))
710
711
711 # Collect tags
712 # Collect tags
712
713
713 globaltags = {}
714 globaltags = {}
714 for c in changesets:
715 for c in changesets:
715 for e in c.entries:
716 for e in c.entries:
716 for tag in e.tags:
717 for tag in e.tags:
717 # remember which is the latest changeset to have this tag
718 # remember which is the latest changeset to have this tag
718 globaltags[tag] = c
719 globaltags[tag] = c
719
720
720 for c in changesets:
721 for c in changesets:
721 tags = set()
722 tags = set()
722 for e in c.entries:
723 for e in c.entries:
723 tags.update(e.tags)
724 tags.update(e.tags)
724 # remember tags only if this is the latest changeset to have it
725 # remember tags only if this is the latest changeset to have it
725 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
726 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
726
727
727 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
728 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
728 # by inserting dummy changesets with two parents, and handle
729 # by inserting dummy changesets with two parents, and handle
729 # {{mergefrombranch BRANCHNAME}} by setting two parents.
730 # {{mergefrombranch BRANCHNAME}} by setting two parents.
730
731
731 if mergeto is None:
732 if mergeto is None:
732 mergeto = br'{{mergetobranch ([-\w]+)}}'
733 mergeto = br'{{mergetobranch ([-\w]+)}}'
733 if mergeto:
734 if mergeto:
734 mergeto = re.compile(mergeto)
735 mergeto = re.compile(mergeto)
735
736
736 if mergefrom is None:
737 if mergefrom is None:
737 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
738 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
738 if mergefrom:
739 if mergefrom:
739 mergefrom = re.compile(mergefrom)
740 mergefrom = re.compile(mergefrom)
740
741
741 versions = {} # changeset index where we saw any particular file version
742 versions = {} # changeset index where we saw any particular file version
742 branches = {} # changeset index where we saw a branch
743 branches = {} # changeset index where we saw a branch
743 n = len(changesets)
744 n = len(changesets)
744 i = 0
745 i = 0
745 while i < n:
746 while i < n:
746 c = changesets[i]
747 c = changesets[i]
747
748
748 for f in c.entries:
749 for f in c.entries:
749 versions[(f.rcs, f.revision)] = i
750 versions[(f.rcs, f.revision)] = i
750
751
751 p = None
752 p = None
752 if c.branch in branches:
753 if c.branch in branches:
753 p = branches[c.branch]
754 p = branches[c.branch]
754 else:
755 else:
755 # first changeset on a new branch
756 # first changeset on a new branch
756 # the parent is a changeset with the branch in its
757 # the parent is a changeset with the branch in its
757 # branchpoints such that it is the latest possible
758 # branchpoints such that it is the latest possible
758 # commit without any intervening, unrelated commits.
759 # commit without any intervening, unrelated commits.
759
760
760 for candidate in xrange(i):
761 for candidate in xrange(i):
761 if c.branch not in changesets[candidate].branchpoints:
762 if c.branch not in changesets[candidate].branchpoints:
762 if p is not None:
763 if p is not None:
763 break
764 break
764 continue
765 continue
765 p = candidate
766 p = candidate
766
767
767 c.parents = []
768 c.parents = []
768 if p is not None:
769 if p is not None:
769 p = changesets[p]
770 p = changesets[p]
770
771
771 # Ensure no changeset has a synthetic changeset as a parent.
772 # Ensure no changeset has a synthetic changeset as a parent.
772 while p.synthetic:
773 while p.synthetic:
773 assert len(p.parents) <= 1, \
774 assert len(p.parents) <= 1, \
774 _('synthetic changeset cannot have multiple parents')
775 _('synthetic changeset cannot have multiple parents')
775 if p.parents:
776 if p.parents:
776 p = p.parents[0]
777 p = p.parents[0]
777 else:
778 else:
778 p = None
779 p = None
779 break
780 break
780
781
781 if p is not None:
782 if p is not None:
782 c.parents.append(p)
783 c.parents.append(p)
783
784
784 if c.mergepoint:
785 if c.mergepoint:
785 if c.mergepoint == 'HEAD':
786 if c.mergepoint == 'HEAD':
786 c.mergepoint = None
787 c.mergepoint = None
787 c.parents.append(changesets[branches[c.mergepoint]])
788 c.parents.append(changesets[branches[c.mergepoint]])
788
789
789 if mergefrom:
790 if mergefrom:
790 m = mergefrom.search(c.comment)
791 m = mergefrom.search(c.comment)
791 if m:
792 if m:
792 m = m.group(1)
793 m = m.group(1)
793 if m == 'HEAD':
794 if m == 'HEAD':
794 m = None
795 m = None
795 try:
796 try:
796 candidate = changesets[branches[m]]
797 candidate = changesets[branches[m]]
797 except KeyError:
798 except KeyError:
798 ui.warn(_("warning: CVS commit message references "
799 ui.warn(_("warning: CVS commit message references "
799 "non-existent branch %r:\n%s\n")
800 "non-existent branch %r:\n%s\n")
800 % (m, c.comment))
801 % (m, c.comment))
801 if m in branches and c.branch != m and not candidate.synthetic:
802 if m in branches and c.branch != m and not candidate.synthetic:
802 c.parents.append(candidate)
803 c.parents.append(candidate)
803
804
804 if mergeto:
805 if mergeto:
805 m = mergeto.search(c.comment)
806 m = mergeto.search(c.comment)
806 if m:
807 if m:
807 if m.groups():
808 if m.groups():
808 m = m.group(1)
809 m = m.group(1)
809 if m == 'HEAD':
810 if m == 'HEAD':
810 m = None
811 m = None
811 else:
812 else:
812 m = None # if no group found then merge to HEAD
813 m = None # if no group found then merge to HEAD
813 if m in branches and c.branch != m:
814 if m in branches and c.branch != m:
814 # insert empty changeset for merge
815 # insert empty changeset for merge
815 cc = changeset(
816 cc = changeset(
816 author=c.author, branch=m, date=c.date,
817 author=c.author, branch=m, date=c.date,
817 comment='convert-repo: CVS merge from branch %s'
818 comment='convert-repo: CVS merge from branch %s'
818 % c.branch,
819 % c.branch,
819 entries=[], tags=[],
820 entries=[], tags=[],
820 parents=[changesets[branches[m]], c])
821 parents=[changesets[branches[m]], c])
821 changesets.insert(i + 1, cc)
822 changesets.insert(i + 1, cc)
822 branches[m] = i + 1
823 branches[m] = i + 1
823
824
824 # adjust our loop counters now we have inserted a new entry
825 # adjust our loop counters now we have inserted a new entry
825 n += 1
826 n += 1
826 i += 2
827 i += 2
827 continue
828 continue
828
829
829 branches[c.branch] = i
830 branches[c.branch] = i
830 i += 1
831 i += 1
831
832
832 # Drop synthetic changesets (safe now that we have ensured no other
833 # Drop synthetic changesets (safe now that we have ensured no other
833 # changesets can have them as parents).
834 # changesets can have them as parents).
834 i = 0
835 i = 0
835 while i < len(changesets):
836 while i < len(changesets):
836 if changesets[i].synthetic:
837 if changesets[i].synthetic:
837 del changesets[i]
838 del changesets[i]
838 else:
839 else:
839 i += 1
840 i += 1
840
841
841 # Number changesets
842 # Number changesets
842
843
843 for i, c in enumerate(changesets):
844 for i, c in enumerate(changesets):
844 c.id = i + 1
845 c.id = i + 1
845
846
846 if odd:
847 if odd:
847 for l, r in odd:
848 for l, r in odd:
848 if l.id is not None and r.id is not None:
849 if l.id is not None and r.id is not None:
849 ui.warn(_('changeset %d is both before and after %d\n')
850 ui.warn(_('changeset %d is both before and after %d\n')
850 % (l.id, r.id))
851 % (l.id, r.id))
851
852
852 ui.status(_('%d changeset entries\n') % len(changesets))
853 ui.status(_('%d changeset entries\n') % len(changesets))
853
854
854 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
855 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
855
856
856 return changesets
857 return changesets
857
858
858
859
859 def debugcvsps(ui, *args, **opts):
860 def debugcvsps(ui, *args, **opts):
860 '''Read CVS rlog for current directory or named path in
861 '''Read CVS rlog for current directory or named path in
861 repository, and convert the log to changesets based on matching
862 repository, and convert the log to changesets based on matching
862 commit log entries and dates.
863 commit log entries and dates.
863 '''
864 '''
864 opts = pycompat.byteskwargs(opts)
865 opts = pycompat.byteskwargs(opts)
865 if opts["new_cache"]:
866 if opts["new_cache"]:
866 cache = "write"
867 cache = "write"
867 elif opts["update_cache"]:
868 elif opts["update_cache"]:
868 cache = "update"
869 cache = "update"
869 else:
870 else:
870 cache = None
871 cache = None
871
872
872 revisions = opts["revisions"]
873 revisions = opts["revisions"]
873
874
874 try:
875 try:
875 if args:
876 if args:
876 log = []
877 log = []
877 for d in args:
878 for d in args:
878 log += createlog(ui, d, root=opts["root"], cache=cache)
879 log += createlog(ui, d, root=opts["root"], cache=cache)
879 else:
880 else:
880 log = createlog(ui, root=opts["root"], cache=cache)
881 log = createlog(ui, root=opts["root"], cache=cache)
881 except logerror as e:
882 except logerror as e:
882 ui.write("%r\n"%e)
883 ui.write("%r\n"%e)
883 return
884 return
884
885
885 changesets = createchangeset(ui, log, opts["fuzz"])
886 changesets = createchangeset(ui, log, opts["fuzz"])
886 del log
887 del log
887
888
888 # Print changesets (optionally filtered)
889 # Print changesets (optionally filtered)
889
890
890 off = len(revisions)
891 off = len(revisions)
891 branches = {} # latest version number in each branch
892 branches = {} # latest version number in each branch
892 ancestors = {} # parent branch
893 ancestors = {} # parent branch
893 for cs in changesets:
894 for cs in changesets:
894
895
895 if opts["ancestors"]:
896 if opts["ancestors"]:
896 if cs.branch not in branches and cs.parents and cs.parents[0].id:
897 if cs.branch not in branches and cs.parents and cs.parents[0].id:
897 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
898 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
898 cs.parents[0].id)
899 cs.parents[0].id)
899 branches[cs.branch] = cs.id
900 branches[cs.branch] = cs.id
900
901
901 # limit by branches
902 # limit by branches
902 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
903 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
903 continue
904 continue
904
905
905 if not off:
906 if not off:
906 # Note: trailing spaces on several lines here are needed to have
907 # Note: trailing spaces on several lines here are needed to have
907 # bug-for-bug compatibility with cvsps.
908 # bug-for-bug compatibility with cvsps.
908 ui.write('---------------------\n')
909 ui.write('---------------------\n')
909 ui.write(('PatchSet %d \n' % cs.id))
910 ui.write(('PatchSet %d \n' % cs.id))
910 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
911 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
911 '%Y/%m/%d %H:%M:%S %1%2')))
912 '%Y/%m/%d %H:%M:%S %1%2')))
912 ui.write(('Author: %s\n' % cs.author))
913 ui.write(('Author: %s\n' % cs.author))
913 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
914 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
914 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
915 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
915 ','.join(cs.tags) or '(none)')))
916 ','.join(cs.tags) or '(none)')))
916 if cs.branchpoints:
917 if cs.branchpoints:
917 ui.write(('Branchpoints: %s \n') %
918 ui.write(('Branchpoints: %s \n') %
918 ', '.join(sorted(cs.branchpoints)))
919 ', '.join(sorted(cs.branchpoints)))
919 if opts["parents"] and cs.parents:
920 if opts["parents"] and cs.parents:
920 if len(cs.parents) > 1:
921 if len(cs.parents) > 1:
921 ui.write(('Parents: %s\n' %
922 ui.write(('Parents: %s\n' %
922 (','.join([(b"%d" % p.id) for p in cs.parents]))))
923 (','.join([(b"%d" % p.id) for p in cs.parents]))))
923 else:
924 else:
924 ui.write(('Parent: %d\n' % cs.parents[0].id))
925 ui.write(('Parent: %d\n' % cs.parents[0].id))
925
926
926 if opts["ancestors"]:
927 if opts["ancestors"]:
927 b = cs.branch
928 b = cs.branch
928 r = []
929 r = []
929 while b:
930 while b:
930 b, c = ancestors[b]
931 b, c = ancestors[b]
931 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
932 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
932 if r:
933 if r:
933 ui.write(('Ancestors: %s\n' % (','.join(r))))
934 ui.write(('Ancestors: %s\n' % (','.join(r))))
934
935
935 ui.write(('Log:\n'))
936 ui.write(('Log:\n'))
936 ui.write('%s\n\n' % cs.comment)
937 ui.write('%s\n\n' % cs.comment)
937 ui.write(('Members: \n'))
938 ui.write(('Members: \n'))
938 for f in cs.entries:
939 for f in cs.entries:
939 fn = f.file
940 fn = f.file
940 if fn.startswith(opts["prefix"]):
941 if fn.startswith(opts["prefix"]):
941 fn = fn[len(opts["prefix"]):]
942 fn = fn[len(opts["prefix"]):]
942 ui.write('\t%s:%s->%s%s \n' % (
943 ui.write('\t%s:%s->%s%s \n' % (
943 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
944 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
944 '.'.join([(b"%d" % x) for x in f.revision]),
945 '.'.join([(b"%d" % x) for x in f.revision]),
945 ['', '(DEAD)'][f.dead]))
946 ['', '(DEAD)'][f.dead]))
946 ui.write('\n')
947 ui.write('\n')
947
948
948 # have we seen the start tag?
949 # have we seen the start tag?
949 if revisions and off:
950 if revisions and off:
950 if revisions[0] == (b"%d" % cs.id) or \
951 if revisions[0] == (b"%d" % cs.id) or \
951 revisions[0] in cs.tags:
952 revisions[0] in cs.tags:
952 off = False
953 off = False
953
954
954 # see if we reached the end tag
955 # see if we reached the end tag
955 if len(revisions) > 1 and not off:
956 if len(revisions) > 1 and not off:
956 if revisions[1] == (b"%d" % cs.id) or \
957 if revisions[1] == (b"%d" % cs.id) or \
957 revisions[1] in cs.tags:
958 revisions[1] in cs.tags:
958 break
959 break
General Comments 0
You need to be logged in to leave comments. Login now