##// END OF EJS Templates
cvsps: wrap bytes in bytestr before %r-ing it...
Augie Fackler -
r37906:d4aad0dd default
parent child Browse files
Show More
@@ -1,960 +1,960
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(e).encode('utf-8')
513 entry.comment = comment.decode(e).encode('utf-8')
514 if ui.debugflag:
514 if ui.debugflag:
515 ui.debug("transcoding by %s: %s of %s\n" %
515 ui.debug("transcoding by %s: %s of %s\n" %
516 (e, revstr(entry.revision), entry.file))
516 (e, revstr(entry.revision), entry.file))
517 break
517 break
518 except UnicodeDecodeError:
518 except UnicodeDecodeError:
519 pass # try next encoding
519 pass # try next encoding
520 except LookupError as inst: # unknown encoding, maybe
520 except LookupError as inst: # unknown encoding, maybe
521 raise error.Abort(inst,
521 raise error.Abort(inst,
522 hint=_('check convert.cvsps.logencoding'
522 hint=_('check convert.cvsps.logencoding'
523 ' configuration'))
523 ' configuration'))
524 else:
524 else:
525 raise error.Abort(_("no encoding can transcode"
525 raise error.Abort(_("no encoding can transcode"
526 " CVS log message for %s of %s")
526 " CVS log message for %s of %s")
527 % (revstr(entry.revision), entry.file),
527 % (revstr(entry.revision), entry.file),
528 hint=_('check convert.cvsps.logencoding'
528 hint=_('check convert.cvsps.logencoding'
529 ' configuration'))
529 ' configuration'))
530
530
531 hook.hook(ui, None, "cvslog", True, log=log)
531 hook.hook(ui, None, "cvslog", True, log=log)
532
532
533 return log
533 return log
534
534
535
535
536 class changeset(object):
536 class changeset(object):
537 '''Class changeset has the following attributes:
537 '''Class changeset has the following attributes:
538 .id - integer identifying this changeset (list index)
538 .id - integer identifying this changeset (list index)
539 .author - author name as CVS knows it
539 .author - author name as CVS knows it
540 .branch - name of branch this changeset is on, or None
540 .branch - name of branch this changeset is on, or None
541 .comment - commit message
541 .comment - commit message
542 .commitid - CVS commitid or None
542 .commitid - CVS commitid or None
543 .date - the commit date as a (time,tz) tuple
543 .date - the commit date as a (time,tz) tuple
544 .entries - list of logentry objects in this changeset
544 .entries - list of logentry objects in this changeset
545 .parents - list of one or two parent changesets
545 .parents - list of one or two parent changesets
546 .tags - list of tags on this changeset
546 .tags - list of tags on this changeset
547 .synthetic - from synthetic revision "file ... added on branch ..."
547 .synthetic - from synthetic revision "file ... added on branch ..."
548 .mergepoint- the branch that has been merged from or None
548 .mergepoint- the branch that has been merged from or None
549 .branchpoints- the branches that start at the current entry or empty
549 .branchpoints- the branches that start at the current entry or empty
550 '''
550 '''
551 def __init__(self, **entries):
551 def __init__(self, **entries):
552 self.id = None
552 self.id = None
553 self.synthetic = False
553 self.synthetic = False
554 self.__dict__.update(entries)
554 self.__dict__.update(entries)
555
555
556 def __repr__(self):
556 def __repr__(self):
557 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__))
558 return "%s(%s)"%(type(self).__name__, ", ".join(items))
558 return "%s(%s)"%(type(self).__name__, ", ".join(items))
559
559
560 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
560 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
561 '''Convert log into changesets.'''
561 '''Convert log into changesets.'''
562
562
563 ui.status(_('creating changesets\n'))
563 ui.status(_('creating changesets\n'))
564
564
565 # try to order commitids by date
565 # try to order commitids by date
566 mindate = {}
566 mindate = {}
567 for e in log:
567 for e in log:
568 if e.commitid:
568 if e.commitid:
569 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
569 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
570
570
571 # Merge changesets
571 # Merge changesets
572 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,
573 x.author, x.branch, x.date, x.branchpoints))
573 x.author, x.branch, x.date, x.branchpoints))
574
574
575 changesets = []
575 changesets = []
576 files = set()
576 files = set()
577 c = None
577 c = None
578 for i, e in enumerate(log):
578 for i, e in enumerate(log):
579
579
580 # Check if log entry belongs to the current changeset or not.
580 # Check if log entry belongs to the current changeset or not.
581
581
582 # Since CVS is file-centric, two different file revisions with
582 # Since CVS is file-centric, two different file revisions with
583 # different branchpoints should be treated as belonging to two
583 # different branchpoints should be treated as belonging to two
584 # different changesets (and the ordering is important and not
584 # different changesets (and the ordering is important and not
585 # honoured by cvsps at this point).
585 # honoured by cvsps at this point).
586 #
586 #
587 # Consider the following case:
587 # Consider the following case:
588 # foo 1.1 branchpoints: [MYBRANCH]
588 # foo 1.1 branchpoints: [MYBRANCH]
589 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
589 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
590 #
590 #
591 # 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
592 # 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
593 # first changeset and bar the next and MYBRANCH and MYBRANCH2
593 # first changeset and bar the next and MYBRANCH and MYBRANCH2
594 # should both start off of the bar changeset. No provisions are
594 # should both start off of the bar changeset. No provisions are
595 # made to ensure that this is, in fact, what happens.
595 # made to ensure that this is, in fact, what happens.
596 if not (c and e.branchpoints == c.branchpoints and
596 if not (c and e.branchpoints == c.branchpoints and
597 (# cvs commitids
597 (# cvs commitids
598 (e.commitid is not None and e.commitid == c.commitid) or
598 (e.commitid is not None and e.commitid == c.commitid) or
599 (# no commitids, use fuzzy commit detection
599 (# no commitids, use fuzzy commit detection
600 (e.commitid is None or c.commitid is None) and
600 (e.commitid is None or c.commitid is None) and
601 e.comment == c.comment and
601 e.comment == c.comment and
602 e.author == c.author and
602 e.author == c.author and
603 e.branch == c.branch and
603 e.branch == c.branch and
604 ((c.date[0] + c.date[1]) <=
604 ((c.date[0] + c.date[1]) <=
605 (e.date[0] + e.date[1]) <=
605 (e.date[0] + e.date[1]) <=
606 (c.date[0] + c.date[1]) + fuzz) and
606 (c.date[0] + c.date[1]) + fuzz) and
607 e.file not in files))):
607 e.file not in files))):
608 c = changeset(comment=e.comment, author=e.author,
608 c = changeset(comment=e.comment, author=e.author,
609 branch=e.branch, date=e.date,
609 branch=e.branch, date=e.date,
610 entries=[], mergepoint=e.mergepoint,
610 entries=[], mergepoint=e.mergepoint,
611 branchpoints=e.branchpoints, commitid=e.commitid)
611 branchpoints=e.branchpoints, commitid=e.commitid)
612 changesets.append(c)
612 changesets.append(c)
613
613
614 files = set()
614 files = set()
615 if len(changesets) % 100 == 0:
615 if len(changesets) % 100 == 0:
616 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
616 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
617 ui.status(stringutil.ellipsis(t, 80) + '\n')
617 ui.status(stringutil.ellipsis(t, 80) + '\n')
618
618
619 c.entries.append(e)
619 c.entries.append(e)
620 files.add(e.file)
620 files.add(e.file)
621 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
622
622
623 # Mark synthetic changesets
623 # Mark synthetic changesets
624
624
625 for c in changesets:
625 for c in changesets:
626 # Synthetic revisions always get their own changeset, because
626 # Synthetic revisions always get their own changeset, because
627 # 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
628 # 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
629 # changesets:
629 # changesets:
630 # "File file3 was added on branch ..." (synthetic, 1 entry)
630 # "File file3 was added on branch ..." (synthetic, 1 entry)
631 # "File file4 was added on branch ..." (synthetic, 1 entry)
631 # "File file4 was added on branch ..." (synthetic, 1 entry)
632 # "Add file3 and file4 to fix ..." (real, 2 entries)
632 # "Add file3 and file4 to fix ..." (real, 2 entries)
633 # Hence the check for 1 entry here.
633 # Hence the check for 1 entry here.
634 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
634 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
635
635
636 # Sort files in each changeset
636 # Sort files in each changeset
637
637
638 def entitycompare(l, r):
638 def entitycompare(l, r):
639 'Mimic cvsps sorting order'
639 'Mimic cvsps sorting order'
640 l = l.file.split('/')
640 l = l.file.split('/')
641 r = r.file.split('/')
641 r = r.file.split('/')
642 nl = len(l)
642 nl = len(l)
643 nr = len(r)
643 nr = len(r)
644 n = min(nl, nr)
644 n = min(nl, nr)
645 for i in range(n):
645 for i in range(n):
646 if i + 1 == nl and nl < nr:
646 if i + 1 == nl and nl < nr:
647 return -1
647 return -1
648 elif i + 1 == nr and nl > nr:
648 elif i + 1 == nr and nl > nr:
649 return +1
649 return +1
650 elif l[i] < r[i]:
650 elif l[i] < r[i]:
651 return -1
651 return -1
652 elif l[i] > r[i]:
652 elif l[i] > r[i]:
653 return +1
653 return +1
654 return 0
654 return 0
655
655
656 for c in changesets:
656 for c in changesets:
657 c.entries.sort(key=functools.cmp_to_key(entitycompare))
657 c.entries.sort(key=functools.cmp_to_key(entitycompare))
658
658
659 # Sort changesets by date
659 # Sort changesets by date
660
660
661 odd = set()
661 odd = set()
662 def cscmp(l, r):
662 def cscmp(l, r):
663 d = sum(l.date) - sum(r.date)
663 d = sum(l.date) - sum(r.date)
664 if d:
664 if d:
665 return d
665 return d
666
666
667 # detect vendor branches and initial commits on a branch
667 # detect vendor branches and initial commits on a branch
668 le = {}
668 le = {}
669 for e in l.entries:
669 for e in l.entries:
670 le[e.rcs] = e.revision
670 le[e.rcs] = e.revision
671 re = {}
671 re = {}
672 for e in r.entries:
672 for e in r.entries:
673 re[e.rcs] = e.revision
673 re[e.rcs] = e.revision
674
674
675 d = 0
675 d = 0
676 for e in l.entries:
676 for e in l.entries:
677 if re.get(e.rcs, None) == e.parent:
677 if re.get(e.rcs, None) == e.parent:
678 assert not d
678 assert not d
679 d = 1
679 d = 1
680 break
680 break
681
681
682 for e in r.entries:
682 for e in r.entries:
683 if le.get(e.rcs, None) == e.parent:
683 if le.get(e.rcs, None) == e.parent:
684 if d:
684 if d:
685 odd.add((l, r))
685 odd.add((l, r))
686 d = -1
686 d = -1
687 break
687 break
688 # By this point, the changesets are sufficiently compared that
688 # By this point, the changesets are sufficiently compared that
689 # we don't really care about ordering. However, this leaves
689 # we don't really care about ordering. However, this leaves
690 # some race conditions in the tests, so we compare on the
690 # some race conditions in the tests, so we compare on the
691 # number of files modified, the files contained in each
691 # number of files modified, the files contained in each
692 # changeset, and the branchpoints in the change to ensure test
692 # changeset, and the branchpoints in the change to ensure test
693 # output remains stable.
693 # output remains stable.
694
694
695 # recommended replacement for cmp from
695 # recommended replacement for cmp from
696 # https://docs.python.org/3.0/whatsnew/3.0.html
696 # https://docs.python.org/3.0/whatsnew/3.0.html
697 c = lambda x, y: (x > y) - (x < y)
697 c = lambda x, y: (x > y) - (x < y)
698 # Sort bigger changes first.
698 # Sort bigger changes first.
699 if not d:
699 if not d:
700 d = c(len(l.entries), len(r.entries))
700 d = c(len(l.entries), len(r.entries))
701 # Try sorting by filename in the change.
701 # Try sorting by filename in the change.
702 if not d:
702 if not d:
703 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])
704 # Try and put changes without a branch point before ones with
704 # Try and put changes without a branch point before ones with
705 # a branch point.
705 # a branch point.
706 if not d:
706 if not d:
707 d = c(len(l.branchpoints), len(r.branchpoints))
707 d = c(len(l.branchpoints), len(r.branchpoints))
708 return d
708 return d
709
709
710 changesets.sort(key=functools.cmp_to_key(cscmp))
710 changesets.sort(key=functools.cmp_to_key(cscmp))
711
711
712 # Collect tags
712 # Collect tags
713
713
714 globaltags = {}
714 globaltags = {}
715 for c in changesets:
715 for c in changesets:
716 for e in c.entries:
716 for e in c.entries:
717 for tag in e.tags:
717 for tag in e.tags:
718 # remember which is the latest changeset to have this tag
718 # remember which is the latest changeset to have this tag
719 globaltags[tag] = c
719 globaltags[tag] = c
720
720
721 for c in changesets:
721 for c in changesets:
722 tags = set()
722 tags = set()
723 for e in c.entries:
723 for e in c.entries:
724 tags.update(e.tags)
724 tags.update(e.tags)
725 # 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
726 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)
727
727
728 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
728 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
729 # by inserting dummy changesets with two parents, and handle
729 # by inserting dummy changesets with two parents, and handle
730 # {{mergefrombranch BRANCHNAME}} by setting two parents.
730 # {{mergefrombranch BRANCHNAME}} by setting two parents.
731
731
732 if mergeto is None:
732 if mergeto is None:
733 mergeto = br'{{mergetobranch ([-\w]+)}}'
733 mergeto = br'{{mergetobranch ([-\w]+)}}'
734 if mergeto:
734 if mergeto:
735 mergeto = re.compile(mergeto)
735 mergeto = re.compile(mergeto)
736
736
737 if mergefrom is None:
737 if mergefrom is None:
738 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
738 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
739 if mergefrom:
739 if mergefrom:
740 mergefrom = re.compile(mergefrom)
740 mergefrom = re.compile(mergefrom)
741
741
742 versions = {} # changeset index where we saw any particular file version
742 versions = {} # changeset index where we saw any particular file version
743 branches = {} # changeset index where we saw a branch
743 branches = {} # changeset index where we saw a branch
744 n = len(changesets)
744 n = len(changesets)
745 i = 0
745 i = 0
746 while i < n:
746 while i < n:
747 c = changesets[i]
747 c = changesets[i]
748
748
749 for f in c.entries:
749 for f in c.entries:
750 versions[(f.rcs, f.revision)] = i
750 versions[(f.rcs, f.revision)] = i
751
751
752 p = None
752 p = None
753 if c.branch in branches:
753 if c.branch in branches:
754 p = branches[c.branch]
754 p = branches[c.branch]
755 else:
755 else:
756 # first changeset on a new branch
756 # first changeset on a new branch
757 # the parent is a changeset with the branch in its
757 # the parent is a changeset with the branch in its
758 # branchpoints such that it is the latest possible
758 # branchpoints such that it is the latest possible
759 # commit without any intervening, unrelated commits.
759 # commit without any intervening, unrelated commits.
760
760
761 for candidate in xrange(i):
761 for candidate in xrange(i):
762 if c.branch not in changesets[candidate].branchpoints:
762 if c.branch not in changesets[candidate].branchpoints:
763 if p is not None:
763 if p is not None:
764 break
764 break
765 continue
765 continue
766 p = candidate
766 p = candidate
767
767
768 c.parents = []
768 c.parents = []
769 if p is not None:
769 if p is not None:
770 p = changesets[p]
770 p = changesets[p]
771
771
772 # Ensure no changeset has a synthetic changeset as a parent.
772 # Ensure no changeset has a synthetic changeset as a parent.
773 while p.synthetic:
773 while p.synthetic:
774 assert len(p.parents) <= 1, \
774 assert len(p.parents) <= 1, \
775 _('synthetic changeset cannot have multiple parents')
775 _('synthetic changeset cannot have multiple parents')
776 if p.parents:
776 if p.parents:
777 p = p.parents[0]
777 p = p.parents[0]
778 else:
778 else:
779 p = None
779 p = None
780 break
780 break
781
781
782 if p is not None:
782 if p is not None:
783 c.parents.append(p)
783 c.parents.append(p)
784
784
785 if c.mergepoint:
785 if c.mergepoint:
786 if c.mergepoint == 'HEAD':
786 if c.mergepoint == 'HEAD':
787 c.mergepoint = None
787 c.mergepoint = None
788 c.parents.append(changesets[branches[c.mergepoint]])
788 c.parents.append(changesets[branches[c.mergepoint]])
789
789
790 if mergefrom:
790 if mergefrom:
791 m = mergefrom.search(c.comment)
791 m = mergefrom.search(c.comment)
792 if m:
792 if m:
793 m = m.group(1)
793 m = m.group(1)
794 if m == 'HEAD':
794 if m == 'HEAD':
795 m = None
795 m = None
796 try:
796 try:
797 candidate = changesets[branches[m]]
797 candidate = changesets[branches[m]]
798 except KeyError:
798 except KeyError:
799 ui.warn(_("warning: CVS commit message references "
799 ui.warn(_("warning: CVS commit message references "
800 "non-existent branch %r:\n%s\n")
800 "non-existent branch %r:\n%s\n")
801 % (m, c.comment))
801 % (pycompat.bytestr(m), c.comment))
802 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:
803 c.parents.append(candidate)
803 c.parents.append(candidate)
804
804
805 if mergeto:
805 if mergeto:
806 m = mergeto.search(c.comment)
806 m = mergeto.search(c.comment)
807 if m:
807 if m:
808 if m.groups():
808 if m.groups():
809 m = m.group(1)
809 m = m.group(1)
810 if m == 'HEAD':
810 if m == 'HEAD':
811 m = None
811 m = None
812 else:
812 else:
813 m = None # if no group found then merge to HEAD
813 m = None # if no group found then merge to HEAD
814 if m in branches and c.branch != m:
814 if m in branches and c.branch != m:
815 # insert empty changeset for merge
815 # insert empty changeset for merge
816 cc = changeset(
816 cc = changeset(
817 author=c.author, branch=m, date=c.date,
817 author=c.author, branch=m, date=c.date,
818 comment='convert-repo: CVS merge from branch %s'
818 comment='convert-repo: CVS merge from branch %s'
819 % c.branch,
819 % c.branch,
820 entries=[], tags=[],
820 entries=[], tags=[],
821 parents=[changesets[branches[m]], c])
821 parents=[changesets[branches[m]], c])
822 changesets.insert(i + 1, cc)
822 changesets.insert(i + 1, cc)
823 branches[m] = i + 1
823 branches[m] = i + 1
824
824
825 # adjust our loop counters now we have inserted a new entry
825 # adjust our loop counters now we have inserted a new entry
826 n += 1
826 n += 1
827 i += 2
827 i += 2
828 continue
828 continue
829
829
830 branches[c.branch] = i
830 branches[c.branch] = i
831 i += 1
831 i += 1
832
832
833 # Drop synthetic changesets (safe now that we have ensured no other
833 # Drop synthetic changesets (safe now that we have ensured no other
834 # changesets can have them as parents).
834 # changesets can have them as parents).
835 i = 0
835 i = 0
836 while i < len(changesets):
836 while i < len(changesets):
837 if changesets[i].synthetic:
837 if changesets[i].synthetic:
838 del changesets[i]
838 del changesets[i]
839 else:
839 else:
840 i += 1
840 i += 1
841
841
842 # Number changesets
842 # Number changesets
843
843
844 for i, c in enumerate(changesets):
844 for i, c in enumerate(changesets):
845 c.id = i + 1
845 c.id = i + 1
846
846
847 if odd:
847 if odd:
848 for l, r in odd:
848 for l, r in odd:
849 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:
850 ui.warn(_('changeset %d is both before and after %d\n')
850 ui.warn(_('changeset %d is both before and after %d\n')
851 % (l.id, r.id))
851 % (l.id, r.id))
852
852
853 ui.status(_('%d changeset entries\n') % len(changesets))
853 ui.status(_('%d changeset entries\n') % len(changesets))
854
854
855 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
855 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
856
856
857 return changesets
857 return changesets
858
858
859
859
860 def debugcvsps(ui, *args, **opts):
860 def debugcvsps(ui, *args, **opts):
861 '''Read CVS rlog for current directory or named path in
861 '''Read CVS rlog for current directory or named path in
862 repository, and convert the log to changesets based on matching
862 repository, and convert the log to changesets based on matching
863 commit log entries and dates.
863 commit log entries and dates.
864 '''
864 '''
865 opts = pycompat.byteskwargs(opts)
865 opts = pycompat.byteskwargs(opts)
866 if opts["new_cache"]:
866 if opts["new_cache"]:
867 cache = "write"
867 cache = "write"
868 elif opts["update_cache"]:
868 elif opts["update_cache"]:
869 cache = "update"
869 cache = "update"
870 else:
870 else:
871 cache = None
871 cache = None
872
872
873 revisions = opts["revisions"]
873 revisions = opts["revisions"]
874
874
875 try:
875 try:
876 if args:
876 if args:
877 log = []
877 log = []
878 for d in args:
878 for d in args:
879 log += createlog(ui, d, root=opts["root"], cache=cache)
879 log += createlog(ui, d, root=opts["root"], cache=cache)
880 else:
880 else:
881 log = createlog(ui, root=opts["root"], cache=cache)
881 log = createlog(ui, root=opts["root"], cache=cache)
882 except logerror as e:
882 except logerror as e:
883 ui.write("%r\n"%e)
883 ui.write("%r\n"%e)
884 return
884 return
885
885
886 changesets = createchangeset(ui, log, opts["fuzz"])
886 changesets = createchangeset(ui, log, opts["fuzz"])
887 del log
887 del log
888
888
889 # Print changesets (optionally filtered)
889 # Print changesets (optionally filtered)
890
890
891 off = len(revisions)
891 off = len(revisions)
892 branches = {} # latest version number in each branch
892 branches = {} # latest version number in each branch
893 ancestors = {} # parent branch
893 ancestors = {} # parent branch
894 for cs in changesets:
894 for cs in changesets:
895
895
896 if opts["ancestors"]:
896 if opts["ancestors"]:
897 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:
898 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
898 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
899 cs.parents[0].id)
899 cs.parents[0].id)
900 branches[cs.branch] = cs.id
900 branches[cs.branch] = cs.id
901
901
902 # limit by branches
902 # limit by branches
903 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"]:
904 continue
904 continue
905
905
906 if not off:
906 if not off:
907 # Note: trailing spaces on several lines here are needed to have
907 # Note: trailing spaces on several lines here are needed to have
908 # bug-for-bug compatibility with cvsps.
908 # bug-for-bug compatibility with cvsps.
909 ui.write('---------------------\n')
909 ui.write('---------------------\n')
910 ui.write(('PatchSet %d \n' % cs.id))
910 ui.write(('PatchSet %d \n' % cs.id))
911 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
911 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
912 '%Y/%m/%d %H:%M:%S %1%2')))
912 '%Y/%m/%d %H:%M:%S %1%2')))
913 ui.write(('Author: %s\n' % cs.author))
913 ui.write(('Author: %s\n' % cs.author))
914 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
914 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
915 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
915 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
916 ','.join(cs.tags) or '(none)')))
916 ','.join(cs.tags) or '(none)')))
917 if cs.branchpoints:
917 if cs.branchpoints:
918 ui.write(('Branchpoints: %s \n') %
918 ui.write(('Branchpoints: %s \n') %
919 ', '.join(sorted(cs.branchpoints)))
919 ', '.join(sorted(cs.branchpoints)))
920 if opts["parents"] and cs.parents:
920 if opts["parents"] and cs.parents:
921 if len(cs.parents) > 1:
921 if len(cs.parents) > 1:
922 ui.write(('Parents: %s\n' %
922 ui.write(('Parents: %s\n' %
923 (','.join([(b"%d" % p.id) for p in cs.parents]))))
923 (','.join([(b"%d" % p.id) for p in cs.parents]))))
924 else:
924 else:
925 ui.write(('Parent: %d\n' % cs.parents[0].id))
925 ui.write(('Parent: %d\n' % cs.parents[0].id))
926
926
927 if opts["ancestors"]:
927 if opts["ancestors"]:
928 b = cs.branch
928 b = cs.branch
929 r = []
929 r = []
930 while b:
930 while b:
931 b, c = ancestors[b]
931 b, c = ancestors[b]
932 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
932 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
933 if r:
933 if r:
934 ui.write(('Ancestors: %s\n' % (','.join(r))))
934 ui.write(('Ancestors: %s\n' % (','.join(r))))
935
935
936 ui.write(('Log:\n'))
936 ui.write(('Log:\n'))
937 ui.write('%s\n\n' % cs.comment)
937 ui.write('%s\n\n' % cs.comment)
938 ui.write(('Members: \n'))
938 ui.write(('Members: \n'))
939 for f in cs.entries:
939 for f in cs.entries:
940 fn = f.file
940 fn = f.file
941 if fn.startswith(opts["prefix"]):
941 if fn.startswith(opts["prefix"]):
942 fn = fn[len(opts["prefix"]):]
942 fn = fn[len(opts["prefix"]):]
943 ui.write('\t%s:%s->%s%s \n' % (
943 ui.write('\t%s:%s->%s%s \n' % (
944 fn,
944 fn,
945 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL',
945 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL',
946 '.'.join([(b"%d" % x) for x in f.revision]),
946 '.'.join([(b"%d" % x) for x in f.revision]),
947 ['', '(DEAD)'][f.dead]))
947 ['', '(DEAD)'][f.dead]))
948 ui.write('\n')
948 ui.write('\n')
949
949
950 # have we seen the start tag?
950 # have we seen the start tag?
951 if revisions and off:
951 if revisions and off:
952 if revisions[0] == (b"%d" % cs.id) or \
952 if revisions[0] == (b"%d" % cs.id) or \
953 revisions[0] in cs.tags:
953 revisions[0] in cs.tags:
954 off = False
954 off = False
955
955
956 # see if we reached the end tag
956 # see if we reached the end tag
957 if len(revisions) > 1 and not off:
957 if len(revisions) > 1 and not off:
958 if revisions[1] == (b"%d" % cs.id) or \
958 if revisions[1] == (b"%d" % cs.id) or \
959 revisions[1] in cs.tags:
959 revisions[1] in cs.tags:
960 break
960 break
General Comments 0
You need to be logged in to leave comments. Login now