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