def get_patch(self, issue, patchset): """Returns a PatchSet object containing the details to apply this patch.""" props = self.get_patchset_properties(issue, patchset) or {} out = [] for filename, state in props.get('files', {}).iteritems(): logging.debug('%s' % filename) # If not status, just assume it's a 'M'. Rietveld often gets it wrong and # just has status: null. Oh well. status = state.get('status') or 'M' if status[0] not in ('A', 'D', 'M', 'R'): raise patch.UnsupportedPatchFormat( filename, 'Change with status \'%s\' is not supported.' % status) svn_props = self.parse_svn_properties( state.get('property_changes', ''), filename) if state.get('is_binary'): if status[0] == 'D': if status[0] != status.strip(): raise patch.UnsupportedPatchFormat( filename, 'Deleted file shouldn\'t have property change.') out.append( patch.FilePatchDelete(filename, state['is_binary'])) else: content = self.get_file_content(issue, patchset, state['id']) if not content or content == 'None': # As a precaution due to a bug in upload.py for git checkout, refuse # empty files. If it's empty, it's not a binary file. raise patch.UnsupportedPatchFormat( filename, 'Binary file is empty. Maybe the file wasn\'t uploaded in the ' 'first place?') out.append( patch.FilePatchBinary(filename, content, svn_props, is_new=(status[0] == 'A'))) continue try: diff = self.get_file_diff(issue, patchset, state['id']) except urllib2.HTTPError, e: if e.code == 404: raise patch.UnsupportedPatchFormat( filename, 'File doesn\'t have a diff.') raise # FilePatchDiff() will detect file deletion automatically. p = patch.FilePatchDiff(filename, diff, svn_props) out.append(p) if status[0] == 'A': # It won't be set for empty file. p.is_new = True if (len(status) > 1 and status[1] == '+' and not (p.source_filename or p.svn_properties)): raise patch.UnsupportedPatchFormat( filename, 'Failed to process the svn properties')
def get_patch(self, issue, patchset): """Returns a PatchSet object containing the details to apply this patch.""" props = self.get_patchset_properties(issue, patchset) or {} out = [] for filename, state in props.get('files', {}).iteritems(): logging.debug('%s' % filename) status = state.get('status') if not status: raise patch.UnsupportedPatchFormat( filename, 'File\'s status is None, patchset upload is incomplete.') if status[0] not in ('A', 'D', 'M'): raise patch.UnsupportedPatchFormat( filename, 'Change with status \'%s\' is not supported.' % status) svn_props = self.parse_svn_properties( state.get('property_changes', ''), filename) if state.get('is_binary'): if status[0] == 'D': if status[0] != status.strip(): raise patch.UnsupportedPatchFormat( filename, 'Deleted file shouldn\'t have property change.') out.append( patch.FilePatchDelete(filename, state['is_binary'])) else: out.append( patch.FilePatchBinary(filename, self.get_file_content( issue, patchset, state['id']), svn_props, is_new=(status[0] == 'A'))) continue try: diff = self.get_file_diff(issue, patchset, state['id']) except urllib2.HTTPError, e: if e.code == 404: raise patch.UnsupportedPatchFormat( filename, 'File doesn\'t have a diff.') raise # FilePatchDiff() will detect file deletion automatically. p = patch.FilePatchDiff(filename, diff, svn_props) out.append(p) if status[0] == 'A': # It won't be set for empty file. p.is_new = True if (len(status) > 1 and status[1] == '+' and not (p.source_filename or p.svn_properties)): raise patch.UnsupportedPatchFormat( filename, 'Failed to process the svn properties')
def parse_svn_properties(rietveld_svn_props, filename): """Returns a list of tuple [('property', 'newvalue')]. rietveld_svn_props is the exact format from 'svn diff'. """ rietveld_svn_props = rietveld_svn_props.splitlines() svn_props = [] if not rietveld_svn_props: return svn_props # 1. Ignore svn:mergeinfo. # 2. Accept svn:eol-style and svn:executable. # 3. Refuse any other. # \n # Added: svn:ignore\n # + LF\n spacer = rietveld_svn_props.pop(0) if spacer or not rietveld_svn_props: # svn diff always put a spacer between the unified diff and property # diff raise patch.UnsupportedPatchFormat( filename, 'Failed to parse svn properties.') while rietveld_svn_props: # Something like 'Added: svn:eol-style'. Note the action is localized. # *sigh*. action = rietveld_svn_props.pop(0) match = re.match(r'^(\w+): (.+)$', action) if not match or not rietveld_svn_props: raise patch.UnsupportedPatchFormat( filename, 'Failed to parse svn properties: %s, %s' % (action, svn_props)) if match.group(2) == 'svn:mergeinfo': # Silently ignore the content. rietveld_svn_props.pop(0) continue if match.group(1) not in ('Added', 'Modified'): # Will fail for our French friends. raise patch.UnsupportedPatchFormat( filename, 'Unsupported svn property operation.') if match.group(2) in ('svn:eol-style', 'svn:executable', 'svn:mime-type'): # ' + foo' where foo is the new value. That's fragile. content = rietveld_svn_props.pop(0) match2 = re.match(r'^ \+ (.*)$', content) if not match2: raise patch.UnsupportedPatchFormat( filename, 'Unsupported svn property format.') svn_props.append((match.group(2), match2.group(1))) return svn_props
def apply_patch(self, patches, post_processors=None): """Applies a patch on 'working_branch' and switch to it. Also commits the changes on the local branch. Ignores svn properties and raise an exception on unexpected ones. """ post_processors = post_processors or self.post_processors or [] # It this throws, the checkout is corrupted. Maybe worth deleting it and # trying again? if self.remote_branch: self._check_call_git( ['checkout', '-b', self.working_branch, '%s/%s' % (self.remote, self.remote_branch), '--quiet']) for index, p in enumerate(patches): try: stdout = '' if p.is_delete: if (not os.path.exists(p.filename) and any(p1.source_filename == p.filename for p1 in patches[0:index])): # The file could already be deleted if a prior patch with file # rename was already processed. To be sure, look at all the previous # patches to see if they were a file rename. pass else: stdout += self._check_output_git(['rm', p.filename]) else: dirname = os.path.dirname(p.filename) full_dir = os.path.join(self.project_path, dirname) if dirname and not os.path.isdir(full_dir): os.makedirs(full_dir) if p.is_binary: with open(os.path.join(self.project_path, p.filename), 'wb') as f: f.write(p.get()) stdout += self._check_output_git(['add', p.filename]) else: # No need to do anything special with p.is_new or if not # p.diff_hunks. git apply manages all that already. stdout += self._check_output_git( ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get(True)) for prop in p.svn_properties: # Ignore some known auto-props flags through .subversion/config, # bails out on the other ones. # TODO(maruel): Read ~/.subversion/config and detect the rules that # applies here to figure out if the property will be correctly # handled. if not prop[0] in ( 'svn:eol-style', 'svn:executable', 'svn:mime-type'): raise patch.UnsupportedPatchFormat( p.filename, 'Cannot apply svn property %s to file %s.' % ( prop[0], p.filename)) for post in post_processors: post(self, p) except OSError, e: raise PatchApplicationFailed(p, '%s%s' % (stdout, e)) except subprocess.CalledProcessError, e: raise PatchApplicationFailed( p, '%s%s' % (stdout, getattr(e, 'stdout', None)))
def apply_patch(self, patches, post_processors=None, verbose=False, name=None, email=None): """Applies a patch on 'working_branch' and switches to it. Also commits the changes on the local branch. Ignores svn properties and raise an exception on unexpected ones. """ post_processors = post_processors or self.post_processors or [] # It this throws, the checkout is corrupted. Maybe worth deleting it and # trying again? if self.remote_branch: self._check_call_git([ 'checkout', '-b', self.working_branch, '-t', self.remote_branch, '--quiet' ]) for index, p in enumerate(patches): stdout = [] try: filepath = os.path.join(self.project_path, p.filename) if p.is_delete: if (not os.path.exists(filepath) and any(p1.source_filename == p.filename for p1 in patches[0:index])): # The file was already deleted if a prior patch with file rename # was already processed because 'git apply' did it for us. pass else: stdout.append( self._check_output_git(['rm', p.filename])) stdout.append('Deleted.') else: dirname = os.path.dirname(p.filename) full_dir = os.path.join(self.project_path, dirname) if dirname and not os.path.isdir(full_dir): os.makedirs(full_dir) stdout.append('Created missing directory %s.' % dirname) if p.is_binary: content = p.get() with open(filepath, 'wb') as f: f.write(content) stdout.append('Added binary file %d bytes' % len(content)) cmd = ['add', p.filename] if verbose: cmd.append('--verbose') stdout.append(self._check_output_git(cmd)) else: # No need to do anything special with p.is_new or if not # p.diff_hunks. git apply manages all that already. cmd = ['apply', '--index', '-p%s' % p.patchlevel] if verbose: cmd.append('--verbose') stdout.append( self._check_output_git(cmd, stdin=p.get(True))) for name, value in p.svn_properties: # Ignore some known auto-props flags through .subversion/config, # bails out on the other ones. # TODO(maruel): Read ~/.subversion/config and detect the rules that # applies here to figure out if the property will be correctly # handled. stdout.append('Property %s=%s' % (name, value)) if not name in ('svn:eol-style', 'svn:executable', 'svn:mime-type'): raise patch.UnsupportedPatchFormat( p.filename, 'Cannot apply svn property %s to file %s.' % (name, p.filename)) for post in post_processors: post(self, p) if verbose: print p.filename print align_stdout(stdout) except OSError, e: raise PatchApplicationFailed( p, '%s%s' % (align_stdout(stdout), e)) except subprocess.CalledProcessError, e: raise PatchApplicationFailed( p, 'While running %s;\n%s%s' % (' '.join(e.cmd), align_stdout(stdout), align_stdout([getattr(e, 'stdout', '')])))