Пример #1
0
def ParsePatchSet(patchset):
  """Patch a patch set into individual patches.

  Args:
    patchset: a models.PatchSet instance.

  Returns:
    A list of models.Patch instances.
  """
  patches = []
  for filename, text in SplitPatch(patchset.data):
    patches.append(models.Patch(patchset=patchset, text=utils.to_dbtext(text),
                                filename=filename, parent=patchset))
  return patches
Пример #2
0
def ParsePatchSet(patchset):
  """Patch a patch set into individual patches.

  Args:
    patchset: a models.PatchSet instance.

  Returns:
    A list of models.Patch instances.
  """
  patches = []
  ps_key = patchset.key()
  splitted = SplitPatch(patchset.data)
  if not splitted:
    return []
  first_id, last_id = db.allocate_ids(
    db.Key.from_path(models.Patch.kind(), 1, parent=ps_key), len(splitted))
  ids = range(first_id, last_id + 1)
  for filename, text in splitted:
    key = db.Key.from_path(models.Patch.kind(), ids.pop(0), parent=ps_key)
    patches.append(models.Patch(patchset=patchset, text=utils.to_dbtext(text),
                                filename=filename, key=key))
  return patches
Пример #3
0
def ParsePatchSet(patchset):
    """Patch a patch set into individual patches.

  Args:
    patchset: a models.PatchSet instance.

  Returns:
    A list of models.Patch instances.
  """
    patches = []
    ps_key = patchset.key
    splitted = engine_utils.SplitPatch(patchset.data)
    if not splitted:
        return []
    first_id, last_id = models.Patch.allocate_ids(len(splitted), parent=ps_key)
    ids = range(first_id, last_id + 1)
    for filename, text in splitted:
        key = ndb.Key(models.Patch, ids.pop(0), parent=ps_key)
        patches.append(
            models.Patch(patchset_key=patchset.key,
                         text=utils.to_dbtext(text),
                         filename=filename,
                         key=key))
    return patches
Пример #4
0
class Patch(db.Model):
  """A single patch, i.e. a set of changes to a single file.

  This is a descendant of a PatchSet.
  """

  patchset = db.ReferenceProperty(PatchSet)  # == parent
  filename = db.StringProperty()
  status = db.StringProperty()  # 'A', 'A  +', 'M', 'D' etc
  text = db.TextProperty()
  content = db.ReferenceProperty(Content)
  patched_content = db.ReferenceProperty(Content, collection_name='patch2_set')
  is_binary = db.BooleanProperty(default=False)
  # Ids of patchsets that have a different version of this file.
  delta = db.ListProperty(int)
  delta_calculated = db.BooleanProperty(default=False)

  _lines = None

  @property
  def lines(self):
    """The patch split into lines, retaining line endings.

    The value is cached.
    """
    if self._lines is not None:
      return self._lines
    if not self.text:
      lines = []
    else:
      lines = self.text.splitlines(True)
    self._lines = lines
    return lines

  _property_changes = None

  @property
  def property_changes(self):
    """The property changes split into lines.

    The value is cached.
    """
    if self._property_changes != None:
      return self._property_changes
    self._property_changes = []
    match = re.search('^Property changes on.*\n'+'_'*67+'$', self.text,
                      re.MULTILINE)
    if match:
      self._property_changes = self.text[match.end():].splitlines()
    return self._property_changes

  _num_added = None

  @property
  def num_added(self):
    """The number of line additions in this patch.

    The value is cached.
    """
    if self._num_added is None:
      self._num_added = self.count_startswith('+') - 1
    return self._num_added

  _num_removed = None

  @property
  def num_removed(self):
    """The number of line removals in this patch.

    The value is cached.
    """
    if self._num_removed is None:
      self._num_removed = self.count_startswith('-') - 1
    return self._num_removed

  _num_chunks = None

  @property
  def num_chunks(self):
    """The number of 'chunks' in this patch.

    A chunk is a block of lines starting with '@@'.

    The value is cached.
    """
    if self._num_chunks is None:
      self._num_chunks = self.count_startswith('@@')
    return self._num_chunks

  _num_comments = None

  @property
  def num_comments(self):
    """The number of non-draft comments for this patch.

    The value is cached.
    """
    if self._num_comments is None:
      self._num_comments = gql(Comment,
                               'WHERE patch = :1 AND draft = FALSE',
                               self).count()
    return self._num_comments

  _num_drafts = None

  @property
  def num_drafts(self):
    """The number of draft comments on this patch for the current user.

    The value is expensive to compute, so it is cached.
    """
    if self._num_drafts is None:
      account = Account.current_user_account
      if account is None:
        self._num_drafts = 0
      else:
        query = gql(Comment,
                    'WHERE patch = :1 AND draft = TRUE AND author = :2',
                    self, account.user)
        self._num_drafts = query.count()
    return self._num_drafts

  def count_startswith(self, prefix):
    """Returns the number of lines with the specified prefix."""
    return len([l for l in self.lines if l.startswith(prefix)])

  def get_content(self):
    """Get self.content, or fetch it if necessary.

    This is the content of the file to which this patch is relative.

    Returns:
      a Content instance.

    Raises:
      FetchError: If there was a problem fetching it.
    """
    try:
      if self.content is not None:
        if self.content.is_bad:
          msg = 'Bad content. Try to upload again.'
          logging.warn('Patch.get_content: %s', msg)
          raise FetchError(msg)
        if self.content.is_uploaded and self.content.text == None:
          msg = 'Upload in progress.'
          logging.warn('Patch.get_content: %s', msg)
          raise FetchError(msg)
        else:
          return self.content
    except db.Error:
      # This may happen when a Content entity was deleted behind our back.
      self.content = None

    content = self.fetch_base()
    content.put()
    self.content = content
    self.put()
    return content

  def get_patched_content(self):
    """Get self.patched_content, computing it if necessary.

    This is the content of the file after applying this patch.

    Returns:
      a Content instance.

    Raises:
      FetchError: If there was a problem fetching the old content.
    """
    try:
      if self.patched_content is not None:
        return self.patched_content
    except db.Error:
      # This may happen when a Content entity was deleted behind our back.
      self.patched_content = None

    old_lines = self.get_content().text.splitlines(True)
    logging.info('Creating patched_content for %s', self.filename)
    chunks = patching.ParsePatchToChunks(self.lines, self.filename)
    new_lines = []
    for _, _, new in patching.PatchChunks(old_lines, chunks):
      new_lines.extend(new)
    text = db.Text(''.join(new_lines))
    patched_content = Content(text=text, parent=self)
    patched_content.put()
    self.patched_content = patched_content
    self.put()
    return patched_content

  @property
  def no_base_file(self):
    """Returns True iff the base file is not available."""
    return self.content and self.content.file_too_large

  def fetch_base(self):
    """Fetch base file for the patch.

    Returns:
      A models.Content instance.

    Raises:
      FetchError: For any kind of problem fetching the content.
    """
    rev = patching.ParseRevision(self.lines)
    if rev is not None:
      if rev == 0:
        # rev=0 means it's a new file.
        return Content(text=db.Text(u''), parent=self)

    # AppEngine can only fetch URLs that db.Link() thinks are OK,
    # so try converting to a db.Link() here.
    try:
      base = db.Link(self.patchset.issue.base)
    except db.BadValueError:
      msg = 'Invalid base URL for fetching: %s' % self.patchset.issue.base
      logging.warn(msg)
      raise FetchError(msg)

    url = utils.make_url(base, self.filename, rev)
    logging.info('Fetching %s', url)
    try:
      result = urlfetch.fetch(url)
    except urlfetch.Error, err:
      msg = 'Error fetching %s: %s: %s' % (url, err.__class__.__name__, err)
      logging.warn('FetchBase: %s', msg)
      raise FetchError(msg)
    if result.status_code != 200:
      msg = 'Error fetching %s: HTTP status %s' % (url, result.status_code)
      logging.warn('FetchBase: %s', msg)
      raise FetchError(msg)
    return Content(text=utils.to_dbtext(utils.unify_linebreaks(result.content)),
                   parent=self)