1# -*-python-*- 2# 3# Copyright (C) 1999-2020 The ViewCVS Group. All Rights Reserved. 4# 5# By using this file, you agree to the terms and conditions set forth in 6# the LICENSE.html file which can be found at the top level of the ViewVC 7# distribution or at http://viewvc.org/license-1.html. 8# 9# For more information, visit http://viewvc.org/ 10# 11# ----------------------------------------------------------------------- 12 13import sys 14import os 15import re 16import tempfile 17from io import StringIO, BytesIO 18import functools 19import vclib 20from . import rcsparse 21from . import blame 22 23### The functionality shared with bincvs should probably be moved to a 24### separate module 25from .bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp, _path_join 26 27 28class CCVSRepository(BaseCVSRepository): 29 def dirlogs(self, path_parts, rev, entries, options): 30 """see vclib.Repository.dirlogs docstring 31 32 rev can be a tag name or None. if set only information from revisions 33 matching the tag will be retrieved 34 35 Option values recognized by this implementation: 36 37 cvs_subdirs 38 boolean. true to fetch logs of the most recently modified file in each 39 subdirectory 40 41 Option values returned by this implementation: 42 43 cvs_tags, cvs_branches 44 lists of tag and branch names encountered in the directory 45 """ 46 if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check 47 raise vclib.Error("Path '%s' is not a directory." 48 % (part2path(path_parts))) 49 entries_to_fetch = [] 50 for entry in entries: 51 if vclib.check_path_access(self, path_parts + [entry.name], None, rev): 52 entries_to_fetch.append(entry) 53 54 subdirs = options.get('cvs_subdirs', 0) 55 56 dirpath = self._getpath(path_parts) 57 alltags = { # all the tags seen in the files of this dir 58 'MAIN' : '', 59 'HEAD' : '1.1' 60 } 61 62 for entry in entries_to_fetch: 63 entry.rev = entry.date = entry.author = None 64 entry.dead = entry.absent = entry.log = entry.lockinfo = None 65 path = _log_path(entry, dirpath, subdirs) 66 if path: 67 entry.path = path 68 try: 69 rcsparse.parse(open(path, 'rb'), 70 InfoSink(entry, rev, alltags, self.encoding)) 71 except IOError as e: 72 entry.errors.append("rcsparse error: %s" % e) 73 except RuntimeError as e: 74 entry.errors.append("rcsparse error: %s" % e) 75 except rcsparse.RCSStopParser: 76 pass 77 78 branches = options['cvs_branches'] = [] 79 tags = options['cvs_tags'] = [] 80 for name, rev in alltags.items(): 81 if Tag(None, rev).is_branch: 82 branches.append(name) 83 else: 84 tags.append(name) 85 86 def itemlog(self, path_parts, rev, sortby, first, limit, options): 87 """see vclib.Repository.itemlog docstring 88 89 rev parameter can be a revision number, a branch number, a tag name, 90 or None. If None, will return information about all revisions, otherwise, 91 will only return information about the specified revision or branch. 92 93 Option values returned by this implementation: 94 95 cvs_tags 96 dictionary of Tag objects for all tags encountered 97 """ 98 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check 99 raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) 100 101 path = self.rcsfile(path_parts, 1) 102 sink = TreeSink(self.encoding) 103 rcsparse.parse(open(path, 'rb'), sink) 104 filtered_revs = _file_log(list(sink.revs.values()), sink.tags, 105 sink.lockinfo, sink.default_branch, rev) 106 for rev in filtered_revs: 107 if rev.prev and len(rev.number) == 2: 108 rev.changed = rev.prev.next_changed 109 options['cvs_tags'] = sink.tags 110 111 if sortby == vclib.SORTBY_DATE: 112 filtered_revs.sort(key=functools.cmp_to_key(_logsort_date_cmp)) 113 elif sortby == vclib.SORTBY_REV: 114 filtered_revs.sort(key=functools.cmp_to_key(_logsort_rev_cmp)) 115 116 if len(filtered_revs) < first: 117 return [] 118 if limit: 119 return filtered_revs[first:first+limit] 120 return filtered_revs 121 122 def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): 123 if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check 124 raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1))) 125 if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check 126 raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2))) 127 128 fd1, temp1 = tempfile.mkstemp() 129 os.fdopen(fd1, 'wb').write(self.openfile(path_parts1, rev1, {})[0].getvalue()) 130 fd2, temp2 = tempfile.mkstemp() 131 os.fdopen(fd2, 'wb').write(self.openfile(path_parts2, rev2, {})[0].getvalue()) 132 133 r1 = self.itemlog(path_parts1, rev1, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] 134 r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] 135 136 info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string) 137 info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string) 138 139 diff_args = vclib._diff_args(type, options) 140 141 return vclib._diff_fp(temp1, temp2, info1, info2, 142 self.utilities.diff or 'diff', diff_args) 143 144 def annotate(self, path_parts, rev=None, include_text=False): 145 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check 146 raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) 147 source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, 148 include_text, self.encoding) 149 return source, source.revision 150 151 def revinfo(self, rev): 152 raise vclib.UnsupportedFeature 153 154 def openfile(self, path_parts, rev, options): 155 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check 156 raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) 157 path = self.rcsfile(path_parts, 1) 158 sink = COSink(rev, self.encoding) 159 rcsparse.parse(open(path, 'rb'), sink) 160 revision = sink.last and sink.last.string 161 return BytesIO(b'\n'.join(sink.sstext.text)), revision 162 163class MatchingSink(rcsparse.Sink): 164 """Superclass for sinks that search for revisions based on tag or number""" 165 166 def __init__(self, find, encoding): 167 """Initialize with tag name or revision number string to match against""" 168 if not find or find == 'MAIN' or find == 'HEAD': 169 self.find = None 170 else: 171 self.find = find 172 173 self.find_tag = None 174 self.encoding = encoding 175 176 def set_principal_branch(self, branch_number): 177 if self.find is None: 178 self.find_tag = Tag(None, self._to_str(branch_number)) 179 180 def define_tag(self, name, revision): 181 if self._to_str(name) == self.find: 182 self.find_tag = Tag(None, self._to_str(revision)) 183 184 def admin_completed(self): 185 if self.find_tag is None: 186 if self.find is None: 187 self.find_tag = Tag(None, '') 188 else: 189 try: 190 self.find_tag = Tag(None, self.find) 191 except ValueError: 192 pass 193 194 def _to_str(self, b): 195 if isinstance(b, bytes): 196 return b.decode(self.encoding, 'backslashreplace') 197 return b 198 199class InfoSink(MatchingSink): 200 def __init__(self, entry, tag, alltags, encoding): 201 MatchingSink.__init__(self, tag, encoding) 202 self.entry = entry 203 self.alltags = alltags 204 self.matching_rev = None 205 self.perfect_match = 0 206 self.lockinfo = { } 207 self.saw_revision = False 208 209 def define_tag(self, name, revision): 210 MatchingSink.define_tag(self, name, revision) 211 self.alltags[self._to_str(name)] = self._to_str(revision) 212 213 def admin_completed(self): 214 MatchingSink.admin_completed(self) 215 if self.find_tag is None: 216 # tag we're looking for doesn't exist 217 if self.entry.kind == vclib.FILE: 218 self.entry.absent = 1 219 raise rcsparse.RCSStopParser 220 221 def parse_completed(self): 222 if not self.saw_revision: 223 #self.entry.errors.append("No revisions exist on %s" % (view_tag or "MAIN")) 224 self.entry.absent = 1 225 226 def set_locker(self, rev, locker): 227 self.lockinfo[self._to_str(rev)] = self._to_str(locker) 228 229 def define_revision(self, revision, date, author, state, branches, next): 230 self.saw_revision = True 231 232 if self.perfect_match: 233 return 234 235 tag = self.find_tag 236 rev = Revision(self._to_str(revision), date, self._to_str(author), 237 state == "dead") 238 rev.lockinfo = self.lockinfo.get(self._to_str(revision)) 239 240 # perfect match if revision number matches tag number or if 241 # revision is on trunk and tag points to trunk. imperfect match 242 # if tag refers to a branch and either a) this revision is the 243 # highest revision so far found on that branch, or b) this 244 # revision is the branchpoint. 245 perfect = ((rev.number == tag.number) or 246 (not tag.number and len(rev.number) == 2)) 247 if perfect or (tag.is_branch and \ 248 ((tag.number == rev.number[:-1] and 249 (not self.matching_rev or 250 rev.number > self.matching_rev.number)) or 251 (rev.number == tag.number[:-1]))): 252 self.matching_rev = rev 253 self.perfect_match = perfect 254 255 def set_revision_info(self, revision, log, text): 256 if self.matching_rev: 257 if self._to_str(revision) == self.matching_rev.string: 258 self.entry.rev = self.matching_rev.string 259 self.entry.date = self.matching_rev.date 260 self.entry.author = self.matching_rev.author 261 self.entry.dead = self.matching_rev.dead 262 self.entry.lockinfo = self.matching_rev.lockinfo 263 self.entry.absent = 0 264 self.entry.log = self._to_str(log) 265 raise rcsparse.RCSStopParser 266 else: 267 raise rcsparse.RCSStopParser 268 269class TreeSink(rcsparse.Sink): 270 d_command = re.compile(br'^d(\d+)\s(\d+)') 271 a_command = re.compile(br'^a(\d+)\s(\d+)') 272 273 def __init__(self, encoding): 274 self.revs = { } 275 self.tags = { } 276 self.head = None 277 self.default_branch = None 278 self.lockinfo = { } 279 self.encoding = encoding 280 281 def set_head_revision(self, revision): 282 self.head = self._to_str(revision) 283 284 def set_principal_branch(self, branch_number): 285 self.default_branch = self._to_str(branch_number) 286 287 def set_locker(self, rev, locker): 288 self.lockinfo[self._to_str(rev)] = self._to_str(locker) 289 290 def define_tag(self, name, revision): 291 # check !tags.has_key(tag_name) 292 self.tags[self._to_str(name)] = self._to_str(revision) 293 294 def define_revision(self, revision, date, author, state, branches, next): 295 # check !revs.has_key(revision) 296 self.revs[self._to_str(revision)] = \ 297 Revision(self._to_str(revision), date, self._to_str(author), 298 state == "dead") 299 300 def set_revision_info(self, revision, log, text): 301 # check revs.has_key(revision) 302 rev = self.revs[self._to_str(revision)] 303 rev.log = self._to_str(log) 304 305 changed = None 306 added = 0 307 deled = 0 308 if self.head != self._to_str(revision): 309 changed = 1 310 lines = text.split(b'\n') 311 idx = 0 312 while idx < len(lines): 313 command = lines[idx] 314 dmatch = self.d_command.match(command) 315 idx = idx + 1 316 if dmatch: 317 deled = deled + int(dmatch.group(2)) 318 else: 319 amatch = self.a_command.match(command) 320 if amatch: 321 count = int(amatch.group(2)) 322 added = added + count 323 idx = idx + count 324 elif command: 325 raise vclib.Error("error while parsing deltatext: %s" 326 % self._to_str(command)) 327 328 if len(rev.number) == 2: 329 rev.next_changed = changed and "+%i -%i" % (deled, added) 330 else: 331 rev.changed = changed and "+%i -%i" % (added, deled) 332 333 def _to_str(self, b): 334 if isinstance(b, bytes): 335 return b.decode(self.encoding, 'backslashreplace') 336 return b 337 338 339class StreamText: 340 d_command = re.compile(br'^d(\d+)\s(\d+)') 341 a_command = re.compile(br'^a(\d+)\s(\d+)') 342 343 def __init__(self, text): 344 self.text = text.split(b'\n') 345 346 def command(self, cmd): 347 adjust = 0 348 add_lines_remaining = 0 349 diffs = cmd.split(b'\n') 350 if diffs[-1] == b"": 351 del diffs[-1] 352 if len(diffs) == 0: 353 return 354 if diffs[0] == b"": 355 del diffs[0] 356 for command in diffs: 357 if add_lines_remaining > 0: 358 # Insertion lines from a prior "a" command 359 self.text.insert(start_line + adjust, command) 360 add_lines_remaining = add_lines_remaining - 1 361 adjust = adjust + 1 362 continue 363 dmatch = self.d_command.match(command) 364 amatch = self.a_command.match(command) 365 if dmatch: 366 # "d" - Delete command 367 start_line = int(dmatch.group(1)) 368 count = int(dmatch.group(2)) 369 begin = start_line + adjust - 1 370 del self.text[begin:begin + count] 371 adjust = adjust - count 372 elif amatch: 373 # "a" - Add command 374 start_line = int(amatch.group(1)) 375 count = int(amatch.group(2)) 376 add_lines_remaining = count 377 else: 378 raise RuntimeError('Error parsing diff commands') 379 380def secondnextdot(s, start): 381 # find the position the second dot after the start index. 382 return s.find('.', s.find('.', start) + 1) 383 384 385class COSink(MatchingSink): 386 def __init__(self, rev, encoding): 387 MatchingSink.__init__(self, rev, encoding) 388 389 def set_head_revision(self, revision): 390 self.head = Revision(self._to_str(revision)) 391 self.last = None 392 self.sstext = None 393 394 def admin_completed(self): 395 MatchingSink.admin_completed(self) 396 if self.find_tag is None: 397 raise vclib.InvalidRevision(self.find) 398 399 def set_revision_info(self, revision, log, text): 400 tag = self.find_tag 401 rev = Revision(self._to_str(revision)) 402 403 if rev.number == tag.number: 404 self.log = self._to_str(log) 405 406 depth = len(rev.number) 407 408 if rev.number == self.head.number: 409 assert self.sstext is None 410 self.sstext = StreamText(text) 411 elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]): 412 assert len(self.last.number) == 2 413 assert rev.number < self.last.number 414 self.sstext.command(text) 415 elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and 416 (rev.number <= tag.number or len(tag.number) == depth-1)): 417 assert len(rev.number) - len(self.last.number) in (0, 2) 418 assert rev.number > self.last.number 419 self.sstext.command(text) 420 else: 421 rev = None 422 423 if rev: 424 #print "tag =", tag.number, "rev =", rev.number, "<br>" 425 self.last = rev 426