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
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
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
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)