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