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