##// END OF EJS Templates
cvsps: cvs log loop uses lookahead to avoid misleading text...
David Champion -
r7593:9811cc67 default
parent child Browse files
Show More
@@ -1,678 +1,684 b''
1 #
1 #
2 # Mercurial built-in replacement for cvsps.
2 # Mercurial built-in replacement for cvsps.
3 #
3 #
4 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os
9 import os
10 import re
10 import re
11 import cPickle as pickle
11 import cPickle as pickle
12 from mercurial import util
12 from mercurial import util
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14
14
15 def listsort(list, key):
15 def listsort(list, key):
16 "helper to sort by key in Python 2.3"
16 "helper to sort by key in Python 2.3"
17 try:
17 try:
18 list.sort(key=key)
18 list.sort(key=key)
19 except TypeError:
19 except TypeError:
20 list.sort(lambda l, r: cmp(key(l), key(r)))
20 list.sort(lambda l, r: cmp(key(l), key(r)))
21
21
22 class logentry(object):
22 class logentry(object):
23 '''Class logentry has the following attributes:
23 '''Class logentry has the following attributes:
24 .author - author name as CVS knows it
24 .author - author name as CVS knows it
25 .branch - name of branch this revision is on
25 .branch - name of branch this revision is on
26 .branches - revision tuple of branches starting at this revision
26 .branches - revision tuple of branches starting at this revision
27 .comment - commit message
27 .comment - commit message
28 .date - the commit date as a (time, tz) tuple
28 .date - the commit date as a (time, tz) tuple
29 .dead - true if file revision is dead
29 .dead - true if file revision is dead
30 .file - Name of file
30 .file - Name of file
31 .lines - a tuple (+lines, -lines) or None
31 .lines - a tuple (+lines, -lines) or None
32 .parent - Previous revision of this entry
32 .parent - Previous revision of this entry
33 .rcs - name of file as returned from CVS
33 .rcs - name of file as returned from CVS
34 .revision - revision number as tuple
34 .revision - revision number as tuple
35 .tags - list of tags on the file
35 .tags - list of tags on the file
36 '''
36 '''
37 def __init__(self, **entries):
37 def __init__(self, **entries):
38 self.__dict__.update(entries)
38 self.__dict__.update(entries)
39
39
40 class logerror(Exception):
40 class logerror(Exception):
41 pass
41 pass
42
42
43 def getrepopath(cvspath):
43 def getrepopath(cvspath):
44 """Return the repository path from a CVS path.
44 """Return the repository path from a CVS path.
45
45
46 >>> getrepopath('/foo/bar')
46 >>> getrepopath('/foo/bar')
47 '/foo/bar'
47 '/foo/bar'
48 >>> getrepopath('c:/foo/bar')
48 >>> getrepopath('c:/foo/bar')
49 'c:/foo/bar'
49 'c:/foo/bar'
50 >>> getrepopath(':pserver:10/foo/bar')
50 >>> getrepopath(':pserver:10/foo/bar')
51 '/foo/bar'
51 '/foo/bar'
52 >>> getrepopath(':pserver:10c:/foo/bar')
52 >>> getrepopath(':pserver:10c:/foo/bar')
53 '/foo/bar'
53 '/foo/bar'
54 >>> getrepopath(':pserver:/foo/bar')
54 >>> getrepopath(':pserver:/foo/bar')
55 '/foo/bar'
55 '/foo/bar'
56 >>> getrepopath(':pserver:c:/foo/bar')
56 >>> getrepopath(':pserver:c:/foo/bar')
57 'c:/foo/bar'
57 'c:/foo/bar'
58 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
58 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
59 '/foo/bar'
59 '/foo/bar'
60 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
60 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
61 'c:/foo/bar'
61 'c:/foo/bar'
62 """
62 """
63 # According to CVS manual, CVS paths are expressed like:
63 # According to CVS manual, CVS paths are expressed like:
64 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
64 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
65 #
65 #
66 # Unfortunately, Windows absolute paths start with a drive letter
66 # Unfortunately, Windows absolute paths start with a drive letter
67 # like 'c:' making it harder to parse. Here we assume that drive
67 # like 'c:' making it harder to parse. Here we assume that drive
68 # letters are only one character long and any CVS component before
68 # letters are only one character long and any CVS component before
69 # the repository path is at least 2 characters long, and use this
69 # the repository path is at least 2 characters long, and use this
70 # to disambiguate.
70 # to disambiguate.
71 parts = cvspath.split(':')
71 parts = cvspath.split(':')
72 if len(parts) == 1:
72 if len(parts) == 1:
73 return parts[0]
73 return parts[0]
74 # Here there is an ambiguous case if we have a port number
74 # Here there is an ambiguous case if we have a port number
75 # immediately followed by a Windows driver letter. We assume this
75 # immediately followed by a Windows driver letter. We assume this
76 # never happens and decide it must be CVS path component,
76 # never happens and decide it must be CVS path component,
77 # therefore ignoring it.
77 # therefore ignoring it.
78 if len(parts[-2]) > 1:
78 if len(parts[-2]) > 1:
79 return parts[-1].lstrip('0123456789')
79 return parts[-1].lstrip('0123456789')
80 return parts[-2] + ':' + parts[-1]
80 return parts[-2] + ':' + parts[-1]
81
81
82 def createlog(ui, directory=None, root="", rlog=True, cache=None):
82 def createlog(ui, directory=None, root="", rlog=True, cache=None):
83 '''Collect the CVS rlog'''
83 '''Collect the CVS rlog'''
84
84
85 # Because we store many duplicate commit log messages, reusing strings
85 # Because we store many duplicate commit log messages, reusing strings
86 # saves a lot of memory and pickle storage space.
86 # saves a lot of memory and pickle storage space.
87 _scache = {}
87 _scache = {}
88 def scache(s):
88 def scache(s):
89 "return a shared version of a string"
89 "return a shared version of a string"
90 return _scache.setdefault(s, s)
90 return _scache.setdefault(s, s)
91
91
92 ui.status(_('collecting CVS rlog\n'))
92 ui.status(_('collecting CVS rlog\n'))
93
93
94 log = [] # list of logentry objects containing the CVS state
94 log = [] # list of logentry objects containing the CVS state
95
95
96 # patterns to match in CVS (r)log output, by state of use
96 # patterns to match in CVS (r)log output, by state of use
97 re_00 = re.compile('RCS file: (.+)$')
97 re_00 = re.compile('RCS file: (.+)$')
98 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
98 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
99 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
99 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
100 re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
100 re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
101 re_10 = re.compile('Working file: (.+)$')
101 re_10 = re.compile('Working file: (.+)$')
102 re_20 = re.compile('symbolic names:')
102 re_20 = re.compile('symbolic names:')
103 re_30 = re.compile('\t(.+): ([\\d.]+)$')
103 re_30 = re.compile('\t(.+): ([\\d.]+)$')
104 re_31 = re.compile('----------------------------$')
104 re_31 = re.compile('----------------------------$')
105 re_32 = re.compile('=============================================================================$')
105 re_32 = re.compile('=============================================================================$')
106 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
106 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
107 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?')
107 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?')
108 re_70 = re.compile('branches: (.+);$')
108 re_70 = re.compile('branches: (.+);$')
109
109
110 prefix = '' # leading path to strip of what we get from CVS
110 prefix = '' # leading path to strip of what we get from CVS
111
111
112 if directory is None:
112 if directory is None:
113 # Current working directory
113 # Current working directory
114
114
115 # Get the real directory in the repository
115 # Get the real directory in the repository
116 try:
116 try:
117 prefix = file(os.path.join('CVS','Repository')).read().strip()
117 prefix = file(os.path.join('CVS','Repository')).read().strip()
118 if prefix == ".":
118 if prefix == ".":
119 prefix = ""
119 prefix = ""
120 directory = prefix
120 directory = prefix
121 except IOError:
121 except IOError:
122 raise logerror('Not a CVS sandbox')
122 raise logerror('Not a CVS sandbox')
123
123
124 if prefix and not prefix.endswith(os.sep):
124 if prefix and not prefix.endswith(os.sep):
125 prefix += os.sep
125 prefix += os.sep
126
126
127 # Use the Root file in the sandbox, if it exists
127 # Use the Root file in the sandbox, if it exists
128 try:
128 try:
129 root = file(os.path.join('CVS','Root')).read().strip()
129 root = file(os.path.join('CVS','Root')).read().strip()
130 except IOError:
130 except IOError:
131 pass
131 pass
132
132
133 if not root:
133 if not root:
134 root = os.environ.get('CVSROOT', '')
134 root = os.environ.get('CVSROOT', '')
135
135
136 # read log cache if one exists
136 # read log cache if one exists
137 oldlog = []
137 oldlog = []
138 date = None
138 date = None
139
139
140 if cache:
140 if cache:
141 cachedir = os.path.expanduser('~/.hg.cvsps')
141 cachedir = os.path.expanduser('~/.hg.cvsps')
142 if not os.path.exists(cachedir):
142 if not os.path.exists(cachedir):
143 os.mkdir(cachedir)
143 os.mkdir(cachedir)
144
144
145 # The cvsps cache pickle needs a uniquified name, based on the
145 # The cvsps cache pickle needs a uniquified name, based on the
146 # repository location. The address may have all sort of nasties
146 # repository location. The address may have all sort of nasties
147 # in it, slashes, colons and such. So here we take just the
147 # in it, slashes, colons and such. So here we take just the
148 # alphanumerics, concatenated in a way that does not mix up the
148 # alphanumerics, concatenated in a way that does not mix up the
149 # various components, so that
149 # various components, so that
150 # :pserver:user@server:/path
150 # :pserver:user@server:/path
151 # and
151 # and
152 # /pserver/user/server/path
152 # /pserver/user/server/path
153 # are mapped to different cache file names.
153 # are mapped to different cache file names.
154 cachefile = root.split(":") + [directory, "cache"]
154 cachefile = root.split(":") + [directory, "cache"]
155 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
155 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
156 cachefile = os.path.join(cachedir,
156 cachefile = os.path.join(cachedir,
157 '.'.join([s for s in cachefile if s]))
157 '.'.join([s for s in cachefile if s]))
158
158
159 if cache == 'update':
159 if cache == 'update':
160 try:
160 try:
161 ui.note(_('reading cvs log cache %s\n') % cachefile)
161 ui.note(_('reading cvs log cache %s\n') % cachefile)
162 oldlog = pickle.load(file(cachefile))
162 oldlog = pickle.load(file(cachefile))
163 ui.note(_('cache has %d log entries\n') % len(oldlog))
163 ui.note(_('cache has %d log entries\n') % len(oldlog))
164 except Exception, e:
164 except Exception, e:
165 ui.note(_('error reading cache: %r\n') % e)
165 ui.note(_('error reading cache: %r\n') % e)
166
166
167 if oldlog:
167 if oldlog:
168 date = oldlog[-1].date # last commit date as a (time,tz) tuple
168 date = oldlog[-1].date # last commit date as a (time,tz) tuple
169 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
169 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
170
170
171 # build the CVS commandline
171 # build the CVS commandline
172 cmd = ['cvs', '-q']
172 cmd = ['cvs', '-q']
173 if root:
173 if root:
174 cmd.append('-d%s' % root)
174 cmd.append('-d%s' % root)
175 p = util.normpath(getrepopath(root))
175 p = util.normpath(getrepopath(root))
176 if not p.endswith('/'):
176 if not p.endswith('/'):
177 p += '/'
177 p += '/'
178 prefix = p + util.normpath(prefix)
178 prefix = p + util.normpath(prefix)
179 cmd.append(['log', 'rlog'][rlog])
179 cmd.append(['log', 'rlog'][rlog])
180 if date:
180 if date:
181 # no space between option and date string
181 # no space between option and date string
182 cmd.append('-d>%s' % date)
182 cmd.append('-d>%s' % date)
183 cmd.append(directory)
183 cmd.append(directory)
184
184
185 # state machine begins here
185 # state machine begins here
186 tags = {} # dictionary of revisions on current file with their tags
186 tags = {} # dictionary of revisions on current file with their tags
187 state = 0
187 state = 0
188 store = False # set when a new record can be appended
188 store = False # set when a new record can be appended
189
189
190 cmd = [util.shellquote(arg) for arg in cmd]
190 cmd = [util.shellquote(arg) for arg in cmd]
191 ui.note(_("running %s\n") % (' '.join(cmd)))
191 ui.note(_("running %s\n") % (' '.join(cmd)))
192 ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
192 ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
193
193
194 for line in util.popen(' '.join(cmd)):
194 pfp = util.popen(' '.join(cmd))
195 peek = pfp.readline()
196 while True:
197 line = peek
198 if line == '':
199 break
200 peek = pfp.readline()
195 if line.endswith('\n'):
201 if line.endswith('\n'):
196 line = line[:-1]
202 line = line[:-1]
197 #ui.debug('state=%d line=%r\n' % (state, line))
203 #ui.debug('state=%d line=%r\n' % (state, line))
198
204
199 if state == 0:
205 if state == 0:
200 # initial state, consume input until we see 'RCS file'
206 # initial state, consume input until we see 'RCS file'
201 match = re_00.match(line)
207 match = re_00.match(line)
202 if match:
208 if match:
203 rcs = match.group(1)
209 rcs = match.group(1)
204 tags = {}
210 tags = {}
205 if rlog:
211 if rlog:
206 filename = util.normpath(rcs[:-2])
212 filename = util.normpath(rcs[:-2])
207 if filename.startswith(prefix):
213 if filename.startswith(prefix):
208 filename = filename[len(prefix):]
214 filename = filename[len(prefix):]
209 if filename.startswith('/'):
215 if filename.startswith('/'):
210 filename = filename[1:]
216 filename = filename[1:]
211 if filename.startswith('Attic/'):
217 if filename.startswith('Attic/'):
212 filename = filename[6:]
218 filename = filename[6:]
213 else:
219 else:
214 filename = filename.replace('/Attic/', '/')
220 filename = filename.replace('/Attic/', '/')
215 state = 2
221 state = 2
216 continue
222 continue
217 state = 1
223 state = 1
218 continue
224 continue
219 match = re_01.match(line)
225 match = re_01.match(line)
220 if match:
226 if match:
221 raise Exception(match.group(1))
227 raise Exception(match.group(1))
222 match = re_02.match(line)
228 match = re_02.match(line)
223 if match:
229 if match:
224 raise Exception(match.group(2))
230 raise Exception(match.group(2))
225 if re_03.match(line):
231 if re_03.match(line):
226 raise Exception(line)
232 raise Exception(line)
227
233
228 elif state == 1:
234 elif state == 1:
229 # expect 'Working file' (only when using log instead of rlog)
235 # expect 'Working file' (only when using log instead of rlog)
230 match = re_10.match(line)
236 match = re_10.match(line)
231 assert match, _('RCS file must be followed by working file')
237 assert match, _('RCS file must be followed by working file')
232 filename = util.normpath(match.group(1))
238 filename = util.normpath(match.group(1))
233 state = 2
239 state = 2
234
240
235 elif state == 2:
241 elif state == 2:
236 # expect 'symbolic names'
242 # expect 'symbolic names'
237 if re_20.match(line):
243 if re_20.match(line):
238 state = 3
244 state = 3
239
245
240 elif state == 3:
246 elif state == 3:
241 # read the symbolic names and store as tags
247 # read the symbolic names and store as tags
242 match = re_30.match(line)
248 match = re_30.match(line)
243 if match:
249 if match:
244 rev = [int(x) for x in match.group(2).split('.')]
250 rev = [int(x) for x in match.group(2).split('.')]
245
251
246 # Convert magic branch number to an odd-numbered one
252 # Convert magic branch number to an odd-numbered one
247 revn = len(rev)
253 revn = len(rev)
248 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
254 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
249 rev = rev[:-2] + rev[-1:]
255 rev = rev[:-2] + rev[-1:]
250 rev = tuple(rev)
256 rev = tuple(rev)
251
257
252 if rev not in tags:
258 if rev not in tags:
253 tags[rev] = []
259 tags[rev] = []
254 tags[rev].append(match.group(1))
260 tags[rev].append(match.group(1))
255
261
256 elif re_31.match(line):
262 elif re_31.match(line):
257 state = 5
263 state = 5
258 elif re_32.match(line):
264 elif re_32.match(line):
259 state = 0
265 state = 0
260
266
261 elif state == 4:
267 elif state == 4:
262 # expecting '------' separator before first revision
268 # expecting '------' separator before first revision
263 if re_31.match(line):
269 if re_31.match(line):
264 state = 5
270 state = 5
265 else:
271 else:
266 assert not re_32.match(line), _('Must have at least some revisions')
272 assert not re_32.match(line), _('Must have at least some revisions')
267
273
268 elif state == 5:
274 elif state == 5:
269 # expecting revision number and possibly (ignored) lock indication
275 # expecting revision number and possibly (ignored) lock indication
270 # we create the logentry here from values stored in states 0 to 4,
276 # we create the logentry here from values stored in states 0 to 4,
271 # as this state is re-entered for subsequent revisions of a file.
277 # as this state is re-entered for subsequent revisions of a file.
272 match = re_50.match(line)
278 match = re_50.match(line)
273 assert match, _('expected revision number')
279 assert match, _('expected revision number')
274 e = logentry(rcs=scache(rcs), file=scache(filename),
280 e = logentry(rcs=scache(rcs), file=scache(filename),
275 revision=tuple([int(x) for x in match.group(1).split('.')]),
281 revision=tuple([int(x) for x in match.group(1).split('.')]),
276 branches=[], parent=None)
282 branches=[], parent=None)
277 state = 6
283 state = 6
278
284
279 elif state == 6:
285 elif state == 6:
280 # expecting date, author, state, lines changed
286 # expecting date, author, state, lines changed
281 match = re_60.match(line)
287 match = re_60.match(line)
282 assert match, _('revision must be followed by date line')
288 assert match, _('revision must be followed by date line')
283 d = match.group(1)
289 d = match.group(1)
284 if d[2] == '/':
290 if d[2] == '/':
285 # Y2K
291 # Y2K
286 d = '19' + d
292 d = '19' + d
287
293
288 if len(d.split()) != 3:
294 if len(d.split()) != 3:
289 # cvs log dates always in GMT
295 # cvs log dates always in GMT
290 d = d + ' UTC'
296 d = d + ' UTC'
291 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'])
297 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'])
292 e.author = scache(match.group(2))
298 e.author = scache(match.group(2))
293 e.dead = match.group(3).lower() == 'dead'
299 e.dead = match.group(3).lower() == 'dead'
294
300
295 if match.group(5):
301 if match.group(5):
296 if match.group(6):
302 if match.group(6):
297 e.lines = (int(match.group(5)), int(match.group(6)))
303 e.lines = (int(match.group(5)), int(match.group(6)))
298 else:
304 else:
299 e.lines = (int(match.group(5)), 0)
305 e.lines = (int(match.group(5)), 0)
300 elif match.group(6):
306 elif match.group(6):
301 e.lines = (0, int(match.group(6)))
307 e.lines = (0, int(match.group(6)))
302 else:
308 else:
303 e.lines = None
309 e.lines = None
304 e.comment = []
310 e.comment = []
305 state = 7
311 state = 7
306
312
307 elif state == 7:
313 elif state == 7:
308 # read the revision numbers of branches that start at this revision
314 # read the revision numbers of branches that start at this revision
309 # or store the commit log message otherwise
315 # or store the commit log message otherwise
310 m = re_70.match(line)
316 m = re_70.match(line)
311 if m:
317 if m:
312 e.branches = [tuple([int(y) for y in x.strip().split('.')])
318 e.branches = [tuple([int(y) for y in x.strip().split('.')])
313 for x in m.group(1).split(';')]
319 for x in m.group(1).split(';')]
314 state = 8
320 state = 8
315 elif re_31.match(line):
321 elif re_31.match(line) and re_50.match(peek):
316 state = 5
322 state = 5
317 store = True
323 store = True
318 elif re_32.match(line):
324 elif re_32.match(line):
319 state = 0
325 state = 0
320 store = True
326 store = True
321 else:
327 else:
322 e.comment.append(line)
328 e.comment.append(line)
323
329
324 elif state == 8:
330 elif state == 8:
325 # store commit log message
331 # store commit log message
326 if re_31.match(line):
332 if re_31.match(line):
327 state = 5
333 state = 5
328 store = True
334 store = True
329 elif re_32.match(line):
335 elif re_32.match(line):
330 state = 0
336 state = 0
331 store = True
337 store = True
332 else:
338 else:
333 e.comment.append(line)
339 e.comment.append(line)
334
340
335 if store:
341 if store:
336 # clean up the results and save in the log.
342 # clean up the results and save in the log.
337 store = False
343 store = False
338 e.tags = util.sort([scache(x) for x in tags.get(e.revision, [])])
344 e.tags = util.sort([scache(x) for x in tags.get(e.revision, [])])
339 e.comment = scache('\n'.join(e.comment))
345 e.comment = scache('\n'.join(e.comment))
340
346
341 revn = len(e.revision)
347 revn = len(e.revision)
342 if revn > 3 and (revn % 2) == 0:
348 if revn > 3 and (revn % 2) == 0:
343 e.branch = tags.get(e.revision[:-1], [None])[0]
349 e.branch = tags.get(e.revision[:-1], [None])[0]
344 else:
350 else:
345 e.branch = None
351 e.branch = None
346
352
347 log.append(e)
353 log.append(e)
348
354
349 if len(log) % 100 == 0:
355 if len(log) % 100 == 0:
350 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
356 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
351
357
352 listsort(log, key=lambda x:(x.rcs, x.revision))
358 listsort(log, key=lambda x:(x.rcs, x.revision))
353
359
354 # find parent revisions of individual files
360 # find parent revisions of individual files
355 versions = {}
361 versions = {}
356 for e in log:
362 for e in log:
357 branch = e.revision[:-1]
363 branch = e.revision[:-1]
358 p = versions.get((e.rcs, branch), None)
364 p = versions.get((e.rcs, branch), None)
359 if p is None:
365 if p is None:
360 p = e.revision[:-2]
366 p = e.revision[:-2]
361 e.parent = p
367 e.parent = p
362 versions[(e.rcs, branch)] = e.revision
368 versions[(e.rcs, branch)] = e.revision
363
369
364 # update the log cache
370 # update the log cache
365 if cache:
371 if cache:
366 if log:
372 if log:
367 # join up the old and new logs
373 # join up the old and new logs
368 listsort(log, key=lambda x:x.date)
374 listsort(log, key=lambda x:x.date)
369
375
370 if oldlog and oldlog[-1].date >= log[0].date:
376 if oldlog and oldlog[-1].date >= log[0].date:
371 raise logerror('Log cache overlaps with new log entries,'
377 raise logerror('Log cache overlaps with new log entries,'
372 ' re-run without cache.')
378 ' re-run without cache.')
373
379
374 log = oldlog + log
380 log = oldlog + log
375
381
376 # write the new cachefile
382 # write the new cachefile
377 ui.note(_('writing cvs log cache %s\n') % cachefile)
383 ui.note(_('writing cvs log cache %s\n') % cachefile)
378 pickle.dump(log, file(cachefile, 'w'))
384 pickle.dump(log, file(cachefile, 'w'))
379 else:
385 else:
380 log = oldlog
386 log = oldlog
381
387
382 ui.status(_('%d log entries\n') % len(log))
388 ui.status(_('%d log entries\n') % len(log))
383
389
384 return log
390 return log
385
391
386
392
387 class changeset(object):
393 class changeset(object):
388 '''Class changeset has the following attributes:
394 '''Class changeset has the following attributes:
389 .author - author name as CVS knows it
395 .author - author name as CVS knows it
390 .branch - name of branch this changeset is on, or None
396 .branch - name of branch this changeset is on, or None
391 .comment - commit message
397 .comment - commit message
392 .date - the commit date as a (time,tz) tuple
398 .date - the commit date as a (time,tz) tuple
393 .entries - list of logentry objects in this changeset
399 .entries - list of logentry objects in this changeset
394 .parents - list of one or two parent changesets
400 .parents - list of one or two parent changesets
395 .tags - list of tags on this changeset
401 .tags - list of tags on this changeset
396 '''
402 '''
397 def __init__(self, **entries):
403 def __init__(self, **entries):
398 self.__dict__.update(entries)
404 self.__dict__.update(entries)
399
405
400 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
406 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
401 '''Convert log into changesets.'''
407 '''Convert log into changesets.'''
402
408
403 ui.status(_('creating changesets\n'))
409 ui.status(_('creating changesets\n'))
404
410
405 # Merge changesets
411 # Merge changesets
406
412
407 listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date))
413 listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date))
408
414
409 changesets = []
415 changesets = []
410 files = {}
416 files = {}
411 c = None
417 c = None
412 for i, e in enumerate(log):
418 for i, e in enumerate(log):
413
419
414 # Check if log entry belongs to the current changeset or not.
420 # Check if log entry belongs to the current changeset or not.
415 if not (c and
421 if not (c and
416 e.comment == c.comment and
422 e.comment == c.comment and
417 e.author == c.author and
423 e.author == c.author and
418 e.branch == c.branch and
424 e.branch == c.branch and
419 ((c.date[0] + c.date[1]) <=
425 ((c.date[0] + c.date[1]) <=
420 (e.date[0] + e.date[1]) <=
426 (e.date[0] + e.date[1]) <=
421 (c.date[0] + c.date[1]) + fuzz) and
427 (c.date[0] + c.date[1]) + fuzz) and
422 e.file not in files):
428 e.file not in files):
423 c = changeset(comment=e.comment, author=e.author,
429 c = changeset(comment=e.comment, author=e.author,
424 branch=e.branch, date=e.date, entries=[])
430 branch=e.branch, date=e.date, entries=[])
425 changesets.append(c)
431 changesets.append(c)
426 files = {}
432 files = {}
427 if len(changesets) % 100 == 0:
433 if len(changesets) % 100 == 0:
428 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
434 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
429 ui.status(util.ellipsis(t, 80) + '\n')
435 ui.status(util.ellipsis(t, 80) + '\n')
430
436
431 c.entries.append(e)
437 c.entries.append(e)
432 files[e.file] = True
438 files[e.file] = True
433 c.date = e.date # changeset date is date of latest commit in it
439 c.date = e.date # changeset date is date of latest commit in it
434
440
435 # Sort files in each changeset
441 # Sort files in each changeset
436
442
437 for c in changesets:
443 for c in changesets:
438 def pathcompare(l, r):
444 def pathcompare(l, r):
439 'Mimic cvsps sorting order'
445 'Mimic cvsps sorting order'
440 l = l.split('/')
446 l = l.split('/')
441 r = r.split('/')
447 r = r.split('/')
442 nl = len(l)
448 nl = len(l)
443 nr = len(r)
449 nr = len(r)
444 n = min(nl, nr)
450 n = min(nl, nr)
445 for i in range(n):
451 for i in range(n):
446 if i + 1 == nl and nl < nr:
452 if i + 1 == nl and nl < nr:
447 return -1
453 return -1
448 elif i + 1 == nr and nl > nr:
454 elif i + 1 == nr and nl > nr:
449 return +1
455 return +1
450 elif l[i] < r[i]:
456 elif l[i] < r[i]:
451 return -1
457 return -1
452 elif l[i] > r[i]:
458 elif l[i] > r[i]:
453 return +1
459 return +1
454 return 0
460 return 0
455 def entitycompare(l, r):
461 def entitycompare(l, r):
456 return pathcompare(l.file, r.file)
462 return pathcompare(l.file, r.file)
457
463
458 c.entries.sort(entitycompare)
464 c.entries.sort(entitycompare)
459
465
460 # Sort changesets by date
466 # Sort changesets by date
461
467
462 def cscmp(l, r):
468 def cscmp(l, r):
463 d = sum(l.date) - sum(r.date)
469 d = sum(l.date) - sum(r.date)
464 if d:
470 if d:
465 return d
471 return d
466
472
467 # detect vendor branches and initial commits on a branch
473 # detect vendor branches and initial commits on a branch
468 le = {}
474 le = {}
469 for e in l.entries:
475 for e in l.entries:
470 le[e.rcs] = e.revision
476 le[e.rcs] = e.revision
471 re = {}
477 re = {}
472 for e in r.entries:
478 for e in r.entries:
473 re[e.rcs] = e.revision
479 re[e.rcs] = e.revision
474
480
475 d = 0
481 d = 0
476 for e in l.entries:
482 for e in l.entries:
477 if re.get(e.rcs, None) == e.parent:
483 if re.get(e.rcs, None) == e.parent:
478 assert not d
484 assert not d
479 d = 1
485 d = 1
480 break
486 break
481
487
482 for e in r.entries:
488 for e in r.entries:
483 if le.get(e.rcs, None) == e.parent:
489 if le.get(e.rcs, None) == e.parent:
484 assert not d
490 assert not d
485 d = -1
491 d = -1
486 break
492 break
487
493
488 return d
494 return d
489
495
490 changesets.sort(cscmp)
496 changesets.sort(cscmp)
491
497
492 # Collect tags
498 # Collect tags
493
499
494 globaltags = {}
500 globaltags = {}
495 for c in changesets:
501 for c in changesets:
496 tags = {}
502 tags = {}
497 for e in c.entries:
503 for e in c.entries:
498 for tag in e.tags:
504 for tag in e.tags:
499 # remember which is the latest changeset to have this tag
505 # remember which is the latest changeset to have this tag
500 globaltags[tag] = c
506 globaltags[tag] = c
501
507
502 for c in changesets:
508 for c in changesets:
503 tags = {}
509 tags = {}
504 for e in c.entries:
510 for e in c.entries:
505 for tag in e.tags:
511 for tag in e.tags:
506 tags[tag] = True
512 tags[tag] = True
507 # remember tags only if this is the latest changeset to have it
513 # remember tags only if this is the latest changeset to have it
508 c.tags = util.sort([tag for tag in tags if globaltags[tag] is c])
514 c.tags = util.sort([tag for tag in tags if globaltags[tag] is c])
509
515
510 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
516 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
511 # by inserting dummy changesets with two parents, and handle
517 # by inserting dummy changesets with two parents, and handle
512 # {{mergefrombranch BRANCHNAME}} by setting two parents.
518 # {{mergefrombranch BRANCHNAME}} by setting two parents.
513
519
514 if mergeto is None:
520 if mergeto is None:
515 mergeto = r'{{mergetobranch ([-\w]+)}}'
521 mergeto = r'{{mergetobranch ([-\w]+)}}'
516 if mergeto:
522 if mergeto:
517 mergeto = re.compile(mergeto)
523 mergeto = re.compile(mergeto)
518
524
519 if mergefrom is None:
525 if mergefrom is None:
520 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
526 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
521 if mergefrom:
527 if mergefrom:
522 mergefrom = re.compile(mergefrom)
528 mergefrom = re.compile(mergefrom)
523
529
524 versions = {} # changeset index where we saw any particular file version
530 versions = {} # changeset index where we saw any particular file version
525 branches = {} # changeset index where we saw a branch
531 branches = {} # changeset index where we saw a branch
526 n = len(changesets)
532 n = len(changesets)
527 i = 0
533 i = 0
528 while i<n:
534 while i<n:
529 c = changesets[i]
535 c = changesets[i]
530
536
531 for f in c.entries:
537 for f in c.entries:
532 versions[(f.rcs, f.revision)] = i
538 versions[(f.rcs, f.revision)] = i
533
539
534 p = None
540 p = None
535 if c.branch in branches:
541 if c.branch in branches:
536 p = branches[c.branch]
542 p = branches[c.branch]
537 else:
543 else:
538 for f in c.entries:
544 for f in c.entries:
539 p = max(p, versions.get((f.rcs, f.parent), None))
545 p = max(p, versions.get((f.rcs, f.parent), None))
540
546
541 c.parents = []
547 c.parents = []
542 if p is not None:
548 if p is not None:
543 c.parents.append(changesets[p])
549 c.parents.append(changesets[p])
544
550
545 if mergefrom:
551 if mergefrom:
546 m = mergefrom.search(c.comment)
552 m = mergefrom.search(c.comment)
547 if m:
553 if m:
548 m = m.group(1)
554 m = m.group(1)
549 if m == 'HEAD':
555 if m == 'HEAD':
550 m = None
556 m = None
551 if m in branches and c.branch != m:
557 if m in branches and c.branch != m:
552 c.parents.append(changesets[branches[m]])
558 c.parents.append(changesets[branches[m]])
553
559
554 if mergeto:
560 if mergeto:
555 m = mergeto.search(c.comment)
561 m = mergeto.search(c.comment)
556 if m:
562 if m:
557 try:
563 try:
558 m = m.group(1)
564 m = m.group(1)
559 if m == 'HEAD':
565 if m == 'HEAD':
560 m = None
566 m = None
561 except:
567 except:
562 m = None # if no group found then merge to HEAD
568 m = None # if no group found then merge to HEAD
563 if m in branches and c.branch != m:
569 if m in branches and c.branch != m:
564 # insert empty changeset for merge
570 # insert empty changeset for merge
565 cc = changeset(author=c.author, branch=m, date=c.date,
571 cc = changeset(author=c.author, branch=m, date=c.date,
566 comment='convert-repo: CVS merge from branch %s' % c.branch,
572 comment='convert-repo: CVS merge from branch %s' % c.branch,
567 entries=[], tags=[], parents=[changesets[branches[m]], c])
573 entries=[], tags=[], parents=[changesets[branches[m]], c])
568 changesets.insert(i + 1, cc)
574 changesets.insert(i + 1, cc)
569 branches[m] = i + 1
575 branches[m] = i + 1
570
576
571 # adjust our loop counters now we have inserted a new entry
577 # adjust our loop counters now we have inserted a new entry
572 n += 1
578 n += 1
573 i += 2
579 i += 2
574 continue
580 continue
575
581
576 branches[c.branch] = i
582 branches[c.branch] = i
577 i += 1
583 i += 1
578
584
579 # Number changesets
585 # Number changesets
580
586
581 for i, c in enumerate(changesets):
587 for i, c in enumerate(changesets):
582 c.id = i + 1
588 c.id = i + 1
583
589
584 ui.status(_('%d changeset entries\n') % len(changesets))
590 ui.status(_('%d changeset entries\n') % len(changesets))
585
591
586 return changesets
592 return changesets
587
593
588
594
589 def debugcvsps(ui, *args, **opts):
595 def debugcvsps(ui, *args, **opts):
590 '''Read CVS rlog for current directory or named path in repository, and
596 '''Read CVS rlog for current directory or named path in repository, and
591 convert the log to changesets based on matching commit log entries and dates.'''
597 convert the log to changesets based on matching commit log entries and dates.'''
592
598
593 if opts["new_cache"]:
599 if opts["new_cache"]:
594 cache = "write"
600 cache = "write"
595 elif opts["update_cache"]:
601 elif opts["update_cache"]:
596 cache = "update"
602 cache = "update"
597 else:
603 else:
598 cache = None
604 cache = None
599
605
600 revisions = opts["revisions"]
606 revisions = opts["revisions"]
601
607
602 try:
608 try:
603 if args:
609 if args:
604 log = []
610 log = []
605 for d in args:
611 for d in args:
606 log += createlog(ui, d, root=opts["root"], cache=cache)
612 log += createlog(ui, d, root=opts["root"], cache=cache)
607 else:
613 else:
608 log = createlog(ui, root=opts["root"], cache=cache)
614 log = createlog(ui, root=opts["root"], cache=cache)
609 except logerror, e:
615 except logerror, e:
610 ui.write("%r\n"%e)
616 ui.write("%r\n"%e)
611 return
617 return
612
618
613 changesets = createchangeset(ui, log, opts["fuzz"])
619 changesets = createchangeset(ui, log, opts["fuzz"])
614 del log
620 del log
615
621
616 # Print changesets (optionally filtered)
622 # Print changesets (optionally filtered)
617
623
618 off = len(revisions)
624 off = len(revisions)
619 branches = {} # latest version number in each branch
625 branches = {} # latest version number in each branch
620 ancestors = {} # parent branch
626 ancestors = {} # parent branch
621 for cs in changesets:
627 for cs in changesets:
622
628
623 if opts["ancestors"]:
629 if opts["ancestors"]:
624 if cs.branch not in branches and cs.parents and cs.parents[0].id:
630 if cs.branch not in branches and cs.parents and cs.parents[0].id:
625 ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
631 ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
626 branches[cs.branch] = cs.id
632 branches[cs.branch] = cs.id
627
633
628 # limit by branches
634 # limit by branches
629 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
635 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
630 continue
636 continue
631
637
632 if not off:
638 if not off:
633 # Note: trailing spaces on several lines here are needed to have
639 # Note: trailing spaces on several lines here are needed to have
634 # bug-for-bug compatibility with cvsps.
640 # bug-for-bug compatibility with cvsps.
635 ui.write('---------------------\n')
641 ui.write('---------------------\n')
636 ui.write('PatchSet %d \n' % cs.id)
642 ui.write('PatchSet %d \n' % cs.id)
637 ui.write('Date: %s\n' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2'))
643 ui.write('Date: %s\n' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2'))
638 ui.write('Author: %s\n' % cs.author)
644 ui.write('Author: %s\n' % cs.author)
639 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
645 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
640 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
646 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
641 ','.join(cs.tags) or '(none)'))
647 ','.join(cs.tags) or '(none)'))
642 if opts["parents"] and cs.parents:
648 if opts["parents"] and cs.parents:
643 if len(cs.parents)>1:
649 if len(cs.parents)>1:
644 ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
650 ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
645 else:
651 else:
646 ui.write('Parent: %d\n' % cs.parents[0].id)
652 ui.write('Parent: %d\n' % cs.parents[0].id)
647
653
648 if opts["ancestors"]:
654 if opts["ancestors"]:
649 b = cs.branch
655 b = cs.branch
650 r = []
656 r = []
651 while b:
657 while b:
652 b, c = ancestors[b]
658 b, c = ancestors[b]
653 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
659 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
654 if r:
660 if r:
655 ui.write('Ancestors: %s\n' % (','.join(r)))
661 ui.write('Ancestors: %s\n' % (','.join(r)))
656
662
657 ui.write('Log:\n')
663 ui.write('Log:\n')
658 ui.write('%s\n\n' % cs.comment)
664 ui.write('%s\n\n' % cs.comment)
659 ui.write('Members: \n')
665 ui.write('Members: \n')
660 for f in cs.entries:
666 for f in cs.entries:
661 fn = f.file
667 fn = f.file
662 if fn.startswith(opts["prefix"]):
668 if fn.startswith(opts["prefix"]):
663 fn = fn[len(opts["prefix"]):]
669 fn = fn[len(opts["prefix"]):]
664 ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
670 ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
665 '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
671 '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
666 ui.write('\n')
672 ui.write('\n')
667
673
668 # have we seen the start tag?
674 # have we seen the start tag?
669 if revisions and off:
675 if revisions and off:
670 if revisions[0] == str(cs.id) or \
676 if revisions[0] == str(cs.id) or \
671 revisions[0] in cs.tags:
677 revisions[0] in cs.tags:
672 off = False
678 off = False
673
679
674 # see if we reached the end tag
680 # see if we reached the end tag
675 if len(revisions)>1 and not off:
681 if len(revisions)>1 and not off:
676 if revisions[1] == str(cs.id) or \
682 if revisions[1] == str(cs.id) or \
677 revisions[1] in cs.tags:
683 revisions[1] in cs.tags:
678 break
684 break
General Comments 0
You need to be logged in to leave comments. Login now