def _diff_directories(self, old_dir, new_dir): """Return uniffied diff between two directories content. Function save two version's content of directory to temp files and treate them as casual diff between two files. """ old_content = self._directory_content(old_dir) new_content = self._directory_content(new_dir) old_tmp = make_tempfile(content=old_content) new_tmp = make_tempfile(content=new_content) diff_cmd = ['diff', '-uN', old_tmp, new_tmp] dl = execute(diff_cmd, extra_ignore_errors=(1, 2), results_unicode=False, split_lines=True) # Replace temporary filenames with real directory names and add ids if dl: dl[0] = dl[0].replace(old_tmp.encode('utf-8'), old_dir.encode('utf-8')) dl[1] = dl[1].replace(new_tmp.encode('utf-8'), new_dir.encode('utf-8')) old_oid = execute( ['cleartool', 'describe', '-fmt', '%On', old_dir], results_unicode=False) new_oid = execute( ['cleartool', 'describe', '-fmt', '%On', new_dir], results_unicode=False) dl.insert(2, b'==== %s %s ====\n' % (old_oid, new_oid)) return dl
def diff_directories(self, old_dir, new_dir): """Return uniffied diff between two directories content. Function save two version's content of directory to temp files and treate them as casual diff between two files. """ old_content = self._directory_content(old_dir) new_content = self._directory_content(new_dir) old_tmp = make_tempfile(content=old_content) new_tmp = make_tempfile(content=new_content) diff_cmd = ["diff", "-uN", old_tmp, new_tmp] dl = execute(diff_cmd, extra_ignore_errors=(1, 2), translate_newlines=False, split_lines=True) # Replacing temporary filenames to # real directory names and add ids if dl: dl[0] = dl[0].replace(old_tmp, old_dir) dl[1] = dl[1].replace(new_tmp, new_dir) old_oid = execute(["cleartool", "describe", "-fmt", "%On", old_dir]) new_oid = execute(["cleartool", "describe", "-fmt", "%On", new_dir]) dl.insert(2, "==== %s %s ====\n" % (old_oid, new_oid)) return dl
def _extract_delete_files(self, depot_file, revision): """Extract the 'old' and 'new' files for a delete operation. Returns a tuple of (old filename, new filename). This can raise a ValueError if extraction fails. """ # Get the old version out of perforce old_filename = make_tempfile() self._write_file('%s#%s' % (depot_file, revision), old_filename) # Make an empty tempfile for the new file new_filename = make_tempfile() return old_filename, new_filename
def _extract_delete_files(self, depot_file, revision, cl_is_shelved): """Extract the 'old' and 'new' files for a delete operation. Returns a tuple of (old filename, new filename). This can raise a ValueError if extraction fails. """ # Get the old version out of perforce old_filename = make_tempfile() self._write_file('%s#%s' % (depot_file, revision), old_filename) # Make an empty tempfile for the new file new_filename = make_tempfile() return old_filename, new_filename
def test_diff_with_pending_changelist(self): """Testing PerforceClient.diff with a pending changelist""" client = self._build_client() client.p4.repo_files = [ { 'depotFile': '//mydepot/test/README', 'rev': '2', 'action': 'edit', 'change': '12345', 'text': 'This is a test.\n', }, { 'depotFile': '//mydepot/test/README', 'rev': '3', 'action': 'edit', 'change': '', 'text': 'This is a mess.\n', }, { 'depotFile': '//mydepot/test/COPYING', 'rev': '1', 'action': 'add', 'change': '12345', 'text': 'Copyright 2013 Joe User.\n', }, { 'depotFile': '//mydepot/test/Makefile', 'rev': '3', 'action': 'delete', 'change': '12345', 'text': 'all: all\n', }, ] readme_file = make_tempfile() copying_file = make_tempfile() makefile_file = make_tempfile() client.p4.print_file('//mydepot/test/README#3', readme_file) client.p4.print_file('//mydepot/test/COPYING#1', copying_file) client.p4.where_files = { '//mydepot/test/README': readme_file, '//mydepot/test/COPYING': copying_file, '//mydepot/test/Makefile': makefile_file, } revisions = client.parse_revision_spec(['12345']) diff = client.diff(revisions) self._compare_diff(diff, '07aa18ff67f9aa615fcda7ecddcb354e')
def edit_text(content='', filename=None): """Run a user-configured editor to prompt for text. This will run a configured text editor (trying the :envvar:`VISUAL` or :envvar:`EDITOR` environment variables, falling back on :program:`vi`) to request text for use in a commit message or some other purpose. Args: content (unicode, optional): Existing content to edit. filename (unicode, optional): The optional name of the temp file to edit. This can be used to help the editor provide a proper editing environment for the file. Returns: unicode: The resulting content. Raises: rbcommons.utils.errors.EditorError: The configured editor could not be run, or it failed with an error. """ tempfile = make_tempfile(content.encode('utf8'), filename=filename) result = edit_file(tempfile) os.unlink(tempfile) return result
def test_make_tempfile(self): """Testing 'make_tempfile' method.""" fname = filesystem.make_tempfile() self.assertTrue(os.path.isfile(fname)) self.assertEqual(os.stat(fname).st_uid, os.geteuid()) self.assertTrue(os.access(fname, os.R_OK | os.W_OK))
def _extract_add_files(self, depot_file, revision, cl_is_shelved): """Extract the 'old' and 'new' files for an add operation. Returns a tuple of (old filename, new filename). This can raise a ValueError if the extraction fails. """ # Make an empty tempfile for the old file old_filename = make_tempfile() if cl_is_shelved: new_filename = make_tempfile() self._write_file('%s@=%s' % (depot_file, revision), new_filename) else: # Just reference the file within the client view new_filename = self._depot_to_local(depot_file) return old_filename, new_filename
def test_make_tempfile_with_prefix(self): """Testing make_tempfile with prefix""" filename = make_tempfile(prefix='supertest-') self.assertIn(filename, filesystem.tempfiles) self.assertTrue(os.path.isfile(filename)) self.assertTrue(os.path.basename(filename).startswith('supertest-')) self.assertEqual(os.stat(filename).st_uid, os.geteuid()) self.assertTrue(os.access(filename, os.R_OK | os.W_OK))
def test_make_tempfile(self): """Testing make_tempfile""" filename = make_tempfile() self.assertIn(filename, filesystem.tempfiles) self.assertTrue(os.path.isfile(filename)) self.assertTrue(os.path.basename(filename).startswith('rbtools.')) self.assertEqual(os.stat(filename).st_uid, os.geteuid()) self.assertTrue(os.access(filename, os.R_OK | os.W_OK))
def _diff_directories(self, old_dir, new_dir): """Return a unified diff between two directories' content. This function saves two version's content of directory to temp files and treats them as casual diff between two files. Args: old_dir (unicode): The path to a directory within a vob. new_dir (unicode): The path to a directory within a vob. Returns: list: The diff between the two directory trees, split into lines. """ old_content = self._directory_content(old_dir) new_content = self._directory_content(new_dir) old_tmp = make_tempfile(content=old_content) new_tmp = make_tempfile(content=new_content) diff_cmd = ['diff', '-uN', old_tmp, new_tmp] dl = execute(diff_cmd, extra_ignore_errors=(1, 2), results_unicode=False, split_lines=True) # Replace temporary filenames with real directory names and add ids if dl: dl[0] = dl[0].replace(old_tmp.encode('utf-8'), old_dir.encode('utf-8')) dl[1] = dl[1].replace(new_tmp.encode('utf-8'), new_dir.encode('utf-8')) old_oid = execute(['cleartool', 'describe', '-fmt', '%On', old_dir], results_unicode=False) new_oid = execute(['cleartool', 'describe', '-fmt', '%On', new_dir], results_unicode=False) dl.insert(2, b'==== %s %s ====\n' % (old_oid, new_oid)) return dl
def _extract_edit_files(self, depot_file, tip, base_revision, cl_is_shelved): """Extract the 'old' and 'new' files for an edit operation. Returns a tuple of (old filename, new filename). This can raise a ValueError if the extraction fails. """ # Get the old version out of perforce old_filename = make_tempfile() self._write_file('%s#%s' % (depot_file, base_revision), old_filename) if cl_is_shelved: new_filename = make_tempfile() self._write_file('%s@=%s' % (depot_file, tip), new_filename) else: # Just reference the file within the client view new_filename = self._depot_to_local(depot_file) return old_filename, new_filename
def main(self, request_id): """Run the command.""" repository_info, tool = self.initialize_scm_tool( client_name=self.options.repository_type) server_url = self.get_server_url(repository_info, tool) api_client, api_root = self.get_api(server_url) self.setup_tool(tool, api_root=api_root) # Check if repository info on reviewboard server match local ones. repository_info = repository_info.find_server_repository_info(api_root) # Get the patch, the used patch ID and base dir for the diff diff_body, diff_revision, base_dir = self.get_patch( request_id, api_root, self.options.diff_revision) if self.options.patch_stdout: print diff_body else: try: if tool.has_pending_changes(): message = 'Working directory is not clean.' if not self.options.commit: print 'Warning: %s' % message else: raise CommandError(message) except NotImplementedError: pass tmp_patch_file = make_tempfile(diff_body) success = self.apply_patch(repository_info, tool, request_id, diff_revision, tmp_patch_file, base_dir) if success and (self.options.commit or self.options.commit_no_edit): try: review_request = api_root.get_review_request( review_request_id=request_id, force_text_type='plain') except APIError, e: raise CommandError('Error getting review request %s: %s' % (request_id, e)) message = self._extract_commit_message(review_request) author = review_request.get_submitter() try: tool.create_commit(message, author, not self.options.commit_no_edit) print('Changes committed to current branch.') except NotImplementedError: raise CommandError('--commit is not supported with %s' % tool.name)
def test_edit_file_with_invalid_editor(self): """Testing edit_file with invalid filename""" message = ( 'The editor "./bad-rbtools-editor" was not found or could not ' 'be run. Make sure the EDITOR environment variable is set ' 'to your preferred editor.') os.environ[str('RBTOOLS_EDITOR')] = './bad-rbtools-editor' with self.assertRaisesMessage(EditorError, message): edit_file(make_tempfile(b'Test content'))
def test_edit_file_with_file_deleted(self): """Testing edit_file with file deleted during edit""" def _subprocess_call(*args, **kwargs): os.unlink(filename) filename = make_tempfile(b'Test content') message = 'The edited file "%s" was deleted during edit.' % filename self.spy_on(subprocess.call, call_fake=_subprocess_call) with self.assertRaisesMessage(EditorError, message): edit_file(filename)
def _extract_add_files(self, depot_file, local_file, revision, cl_is_shelved, cl_is_pending): """Extract the 'old' and 'new' files for an add operation. Returns a tuple of (old filename, new filename). This can raise a ValueError if the extraction fails. """ # Make an empty tempfile for the old file old_filename = make_tempfile() if cl_is_shelved: new_filename = make_tempfile() self._write_file('%s@=%s' % (depot_file, revision), new_filename) elif cl_is_pending: # Just reference the file within the client view new_filename = local_file else: new_filename = make_tempfile() self._write_file('%s#%s' % (depot_file, revision), new_filename) return old_filename, new_filename
def main(self, request_id): """Run the command.""" repository_info, tool = self.initialize_scm_tool( client_name=self.options.repository_type) server_url = self.get_server_url(repository_info, tool) api_client, api_root = self.get_api(server_url) self.setup_tool(tool, api_root=api_root) # Check if repository info on reviewboard server match local ones. repository_info = repository_info.find_server_repository_info(api_root) # Get the patch, the used patch ID and base dir for the diff diff_body, diff_revision, base_dir = self.get_patch( request_id, api_root, self.options.diff_revision) if self.options.patch_stdout: print(diff_body) else: try: if tool.has_pending_changes(): message = 'Working directory is not clean.' if not self.options.commit: print('Warning: %s' % message) else: raise CommandError(message) except NotImplementedError: pass tmp_patch_file = make_tempfile(diff_body) success = self.apply_patch(repository_info, tool, request_id, diff_revision, tmp_patch_file, base_dir) if success and (self.options.commit or self.options.commit_no_edit): try: review_request = api_root.get_review_request( review_request_id=request_id, force_text_type='plain') except APIError as e: raise CommandError('Error getting review request %s: %s' % (request_id, e)) message = extract_commit_message(review_request) author = review_request.get_submitter() try: tool.create_commit(message, author, not self.options.commit_no_edit) print('Changes committed to current branch.') except NotImplementedError: raise CommandError('--commit is not supported with %s' % tool.name)
def _extract_edit_files(self, depot_file, local_file, rev_a, rev_b, cl_is_shelved, cl_is_submitted): """Extract the 'old' and 'new' files for an edit operation. Returns a tuple of (old filename, new filename). This can raise a ValueError if the extraction fails. """ # Get the old version out of perforce old_filename = make_tempfile() self._write_file('%s#%s' % (depot_file, rev_a), old_filename) if cl_is_shelved: new_filename = make_tempfile() self._write_file('%s@=%s' % (depot_file, rev_b), new_filename) elif cl_is_submitted: new_filename = make_tempfile() self._write_file('%s#%s' % (depot_file, rev_b), new_filename) else: # Just reference the file within the client view new_filename = local_file return old_filename, new_filename
def edit_text(content): """Allows a user to edit a block of text and returns the saved result. The environment's default text editor is used if available, otherwise vim is used. """ tempfile = make_tempfile(content.encode('utf8')) editor = os.environ.get('EDITOR', 'vim') subprocess.call([editor, tempfile]) f = open(tempfile) result = f.read() f.close() return result.decode('utf8')
def test_make_tempfile_with_filename(self): """Testing make_tempfile with filename""" filename = make_tempfile(filename='TEST123') self.assertIn(filename, filesystem.tempfiles) self.assertEqual(os.path.basename(filename), 'TEST123') self.assertTrue(os.path.isfile(filename)) self.assertTrue(os.access(filename, os.R_OK | os.W_OK)) self.assertEqual(os.stat(filename).st_uid, os.geteuid()) parent_dir = os.path.dirname(filename) self.assertIn(parent_dir, filesystem.tempdirs) self.assertTrue(os.access(parent_dir, os.R_OK | os.W_OK | os.X_OK)) self.assertEqual(os.stat(parent_dir).st_uid, os.geteuid())
def _patch(self, content, patch): """Patch content with a patch. Returnes patched content. The content and the patch should be a list of lines with no endl.""" content_file = make_tempfile(content=os.linesep.join(content)) patch_file = make_tempfile(content=os.linesep.join(patch)) reject_file = make_tempfile() output_file = make_tempfile() patch_cmd = ["patch", "-r", reject_file, "-o", output_file, "-i", patch_file, content_file] output = execute(patch_cmd, extra_ignore_errors=(1,), translate_newlines=False) if "FAILED at" in output: logging.debug("patching content FAILED:") logging.debug(output) patched = open(output_file).read() eof_endl = patched.endswith('\n') patched = patched.splitlines() if eof_endl: patched.append('') try: os.unlink(content_file) os.unlink(patch_file) os.unlink(reject_file) os.unlink(output_file) except: pass return patched
def main(self, request_id): """Run the command.""" self.repository_info, self.tool = self.initialize_scm_tool() server_url = self.get_server_url(self.repository_info, self.tool) self.root_resource = self.get_root(server_url) # Get the patch, the used patch ID and base dir for the diff diff_body, diff_revision, base_dir = self.get_patch( request_id, self.options.diff_revision) tmp_patch_file = make_tempfile(diff_body) self.apply_patch(request_id, diff_revision, tmp_patch_file, base_dir) os.remove(tmp_patch_file)
def main(self, request_id): """Run the command.""" repository_info, tool = self.initialize_scm_tool() server_url = self.get_server_url(self.repository_info, self.tool) api_client, api_root = self.get_api(server_url) # Get the patch, the used patch ID and base dir for the diff diff_body, diff_revision, base_dir = self.get_patch( request_id, self.options.diff_revision) tmp_patch_file = make_tempfile(diff_body) self.apply_patch(tool, request_id, diff_revision, tmp_patch_file, base_dir)
def _content_diff(self, old_content, new_content, old_file, new_file, unified=True): """Returns unified diff as a list of lines with no end lines, uses temp files. The input content should be a list of lines without end lines.""" old_tmp = make_tempfile(content=os.linesep.join(old_content)) new_tmp = make_tempfile(content=os.linesep.join(new_content)) diff_cmd = ['diff'] if unified: diff_cmd.append('-uN') diff_cmd.extend((old_tmp, new_tmp)) dl = execute(diff_cmd, extra_ignore_errors=(1, 2), translate_newlines=False, split_lines=False) eof_endl = dl.endswith('\n') dl = dl.splitlines() if eof_endl: dl.append('') try: os.unlink(old_tmp) os.unlink(new_tmp) except: pass if unified and dl and len(dl) > 1: # Because the modification time is for temporary files here # replacing it with headers without modification time. if dl[0].startswith('---') and dl[1].startswith('+++'): dl[0] = '--- %s\t' % old_file dl[1] = '+++ %s\t' % new_file return dl
def main(self, request_id): """Run the command.""" repository_info, tool = self.initialize_scm_tool() server_url = self.get_server_url(repository_info, tool) api_client, api_root = self.get_api(server_url) # Get the patch, the used patch ID and base dir for the diff diff_body, diff_revision, base_dir = self.get_patch( request_id, api_root, self.options.diff_revision) tmp_patch_file = make_tempfile(diff_body) self.apply_patch(repository_info, tool, request_id, diff_revision, tmp_patch_file, base_dir)
def _exclude_files_not_in_tree(self, patch_file, base_path): """Process a diff and remove entries not in the current directory. The file at the location patch_file will be overwritten by the new patch. This function returns a tuple of two booleans. The first boolean indicates if any files have been excluded. The second boolean indicates if the resulting diff patch file is empty. """ excluded_files = False empty_patch = True # If our base path does not have a trailing slash (which it won't # unless we are at a checkout root), we append a slash so that we can # determine if files are under the base_path. We do this so that files # like /trunkish (which begins with /trunk) do not mistakenly get # placed in /trunk if that is the base_path. if not base_path.endswith('/'): base_path += '/' filtered_patch_name = make_tempfile() with open(filtered_patch_name, 'w') as filtered_patch: with open(patch_file, 'r') as original_patch: include_file = True for line in original_patch.readlines(): m = self.INDEX_FILE_RE.match(line) if m: filename = m.group(1).decode('utf-8') include_file = filename.startswith(base_path) if not include_file: excluded_files = True else: empty_patch = False if include_file: filtered_patch.write(line) os.rename(filtered_patch_name, patch_file) return (excluded_files, empty_patch)
def _extract_move_files(self, old_depot_file, tip, base_revision, cl_is_shelved): """Extract the 'old' and 'new' files for a move operation. Returns a tuple of (old filename, new filename, new depot path). This can raise a ValueError if extraction fails. """ # XXX: fstat *ought* to work, but perforce doesn't supply the movedFile # field in fstat (or apparently anywhere else) when a change is # shelved. For now, _diff_pending will avoid calling this method at all # for shelved changes, and instead treat them as deletes and adds. assert not cl_is_shelved # if cl_is_shelved: # fstat_path = '%s@=%s' % (depot_file, tip) # else: fstat_path = old_depot_file stat_info = self.p4.fstat(fstat_path, ['clientFile', 'movedFile']) if 'clientFile' not in stat_info or 'movedFile' not in stat_info: raise ValueError('Unable to get moved file information') old_filename = make_tempfile() self._write_file('%s#%s' % (old_depot_file, base_revision), old_filename) # if cl_is_shelved: # fstat_path = '%s@=%s' % (stat_info['movedFile'], tip) # else: fstat_path = stat_info['movedFile'] stat_info = self.p4.fstat(fstat_path, ['clientFile', 'depotFile']) if 'clientFile' not in stat_info or 'depotFile' not in stat_info: raise ValueError('Unable to get moved file information') # Grab the new depot path (to include in the diff index) new_depot_file = stat_info['depotFile'] # Reference the new file directly in the client view new_filename = stat_info['clientFile'] return old_filename, new_filename, new_depot_file
def edit_text(content): """Allows a user to edit a block of text and returns the saved result. The environment's default text editor is used if available, otherwise vi is used. """ tempfile = make_tempfile(content.encode('utf8')) editor = os.environ.get('VISUAL') or os.environ.get('EDITOR') or 'vi' try: subprocess.call([editor, tempfile]) except OSError: print 'No editor found. Set EDITOR environment variable or install vi.' raise f = open(tempfile) result = f.read() f.close() return result.decode('utf8')
def main(self, pull_request_id): repository_info, tool = self.initialize_scm_tool() configs = [load_config()] if self.options.owner is None: self.options.owner = get_config_value(configs, "GITHUB_OWNER", None) if self.options.repo is None: self.options.repo = get_config_value(configs, "GITHUB_REPO", None) if self.options.owner is None or self.options.repo is None: raise CommandError("No GITHUB_REPO or GITHUB_OWNER has been configured.") diff = self.get_patch(pull_request_id) if self.options.patch_stdout: print diff else: try: if tool.has_pending_changes(): message = "Working directory is not clean." if not self.options.commit: print "Warning: %s" % message else: raise CommandError(message) except NotImplementedError: pass tmp_patch_file = make_tempfile(diff) self.apply_patch(repository_info, tool, pull_request_id, tmp_patch_file) if self.options.commit: message = self.generate_commit_message(pull_request_id) author = self.get_author_from_patch(tmp_patch_file) try: tool.create_commit(message, author) print ("Changes committed to current branch.") except NotImplementedError: raise CommandError("--commit is not supported with %s" % tool.name)
def test_edit_file_with_editor_priority(self): """Testing edit_file editor priority""" self.spy_on(subprocess.call, call_original=False) # Save these so we can restore after the tests. We don't need to # save RBTOOLS_EDITOR, because this is taken care of in the base # TestCase class. old_visual = os.environ.get(str('VISUAL')) old_editor = os.environ.get(str('EDITOR')) filename = make_tempfile(b'Test content') try: os.environ[str('RBTOOLS_EDITOR')] = 'rbtools-editor' os.environ[str('VISUAL')] = 'visual' os.environ[str('EDITOR')] = 'editor' edit_file(filename) self.assertTrue( subprocess.call.last_called_with(['rbtools-editor', filename])) os.environ[str('RBTOOLS_EDITOR')] = '' edit_file(filename) self.assertTrue( subprocess.call.last_called_with(['visual', filename])) os.environ[str('VISUAL')] = '' edit_file(filename) self.assertTrue( subprocess.call.last_called_with(['editor', filename])) os.environ[str('EDITOR')] = '' edit_file(filename) self.assertTrue(subprocess.call.last_called_with(['vi', filename])) finally: if old_visual: os.environ[str('VISUAL')] = old_visual if old_editor: os.environ[str('EDITOR')] = old_editor
def precreate_tempfiles(self, count): """Pre-create a specific number of temporary files. This will call :py:func:`~rbtools.utils.filesystem.make_tempfile` the specified number of times, returning the list of generated temp file paths, and will then spy that function to return those temp files. Once each pre-created temp file is used up, any further calls to :py:func:`~rbtools.utils.filesystem.make_tempfile` will result in an error, failing the test. This is useful in unit tests that need to script a series of expected calls using :py:mod:`kgb` (such as through :py:class:`kgb.ops.SpyOpMatchInOrder`) that need to know the names of temporary filenames up-front. Unit test suites that use this must mix in :py:class:`kgb.SpyAgency`. Args: count (int): The number of temporary filenames to pre-create. Raises: AssertionError: The test suite class did not mix in :py:class:`kgb.SpyAgency`. """ assert hasattr(self, 'spy_on'), ( '%r must mix in kgb.SpyAgency in order to call this method.' % self.__class__) tmpfiles = [make_tempfile() for i in range(count)] self.spy_on(make_tempfile, op=kgb.SpyOpReturnInOrder(tmpfiles)) return tmpfiles
def main(self, review_request_id): """Run the command.""" repository_info, tool = self.initialize_scm_tool( client_name=self.options.repository_type) if self.options.patch_stdout and self.options.server: server_url = self.options.server else: server_url = self.get_server_url(repository_info, tool) if self.options.revert_patch and not tool.supports_patch_revert: raise CommandError('The %s backend does not support reverting ' 'patches.' % tool.name) api_client, api_root = self.get_api(server_url) if not self.options.patch_stdout: self.setup_tool(tool, api_root=api_root) # Check if repository info on reviewboard server match local ones. repository_info = repository_info.find_server_repository_info( api_root) # Get the patch, the used patch ID and base dir for the diff patch_data = self.get_patch(tool, api_root, review_request_id, self.options.diff_revision, self.options.commit_id) diff_body = patch_data['diff'] diff_revision = patch_data['revision'] base_dir = patch_data['base_dir'] if self.options.patch_stdout: if isinstance(diff_body, bytes): print(diff_body.decode('utf-8')) else: print(diff_body) else: try: if tool.has_pending_changes(): message = 'Working directory is not clean.' if not self.options.commit: print('Warning: %s' % message) else: raise CommandError(message) except NotImplementedError: pass tmp_patch_file = make_tempfile(diff_body) success = self.apply_patch(repository_info, tool, review_request_id, diff_revision, tmp_patch_file, base_dir, revert=self.options.revert_patch) if not success: raise CommandError('Could not apply patch') if self.options.commit or self.options.commit_no_edit: if patch_data['commit_meta'] is not None: # We are patching a commit so we already have the metadata # required without making additional HTTP requests. meta = patch_data['commit_meta'] message = meta['message'] # Fun fact: object does not have a __dict__ so you cannot # call setattr() on them. We need this ability so we are # creating a type that does. author = type('Author', (object, ), {})() author.fullname = meta['author_name'] author.email = meta['author_email'] else: try: review_request = api_root.get_review_request( review_request_id=review_request_id, force_text_type='plain') except APIError as e: raise CommandError( 'Error getting review request %s: %s' % (request_id, e)) message = extract_commit_message(review_request) author = review_request.get_submitter() try: tool.create_commit(message, author, not self.options.commit_no_edit) print('Changes committed to current branch.') except NotImplementedError: raise CommandError('--commit is not supported with %s' % tool.name)
def _process_diffs(self, diff_entries): """Process the given diff entries. Args: diff_entries (list): The list of diff entries. Returns: bytes: The processed diffs. """ diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for f in diff_entries: f = f.strip() if not f: continue m = re.search(br'(?P<type>[ACMD]) (?P<file>.*) ' br'(?P<revspec>rev:revid:[-\d]+) ' br'(?P<parentrevspec>rev:revid:[-\d]+) ' br'src:(?P<srcpath>.*) ' br'dst:(?P<dstpath>.*)$', f) if not m: raise SCMError('Could not parse "cm log" response: %s' % f) changetype = m.group('type') filename = m.group('file') if changetype == b'M': # Handle moved files as a delete followed by an add. # Clunky, but at least it works oldfilename = m.group('srcpath') oldspec = m.group('revspec') newfilename = m.group('dstpath') newspec = m.group('revspec') self._write_file(oldfilename, oldspec, tmp_diff_from_filename) dl = self._diff_files(tmp_diff_from_filename, empty_filename, oldfilename, 'rev:revid:-1', oldspec, changetype) diff_lines += dl self._write_file(newfilename, newspec, tmp_diff_to_filename) dl = self._diff_files(empty_filename, tmp_diff_to_filename, newfilename, newspec, 'rev:revid:-1', changetype) diff_lines += dl else: newrevspec = m.group('revspec') parentrevspec = m.group('parentrevspec') logging.debug('Type %s File %s Old %s New %s', changetype, filename, parentrevspec, newrevspec) old_file = new_file = empty_filename if (changetype in [b'A'] or (changetype in [b'C'] and parentrevspec == b'rev:revid:-1')): # There's only one content to show self._write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in [b'C']: self._write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename self._write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in [b'D']: self._write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename else: raise SCMError('Unknown change type "%s" for %s' % (changetype, filename)) dl = self._diff_files(old_file, new_file, filename, newrevspec, parentrevspec, changetype) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return b''.join(diff_lines)
def _process_diffs(self, diff_entries): """Process the given diff entries. Args: diff_entries (list): The list of diff entries. Returns: bytes: The processed diffs. """ diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for f in diff_entries: f = f.strip() if not f: continue m = re.search( br'(?P<type>[ACMD]) (?P<file>.*) ' br'(?P<revspec>rev:revid:[-\d]+) ' br'(?P<parentrevspec>rev:revid:[-\d]+) ' br'src:(?P<srcpath>.*) ' br'dst:(?P<dstpath>.*)$', f) if not m: raise SCMError('Could not parse "cm log" response: %s' % f) changetype = m.group('type') filename = m.group('file') if changetype == b'M': # Handle moved files as a delete followed by an add. # Clunky, but at least it works oldfilename = m.group('srcpath') oldspec = m.group('revspec') newfilename = m.group('dstpath') newspec = m.group('revspec') self._write_file(oldfilename, oldspec, tmp_diff_from_filename) dl = self._diff_files(tmp_diff_from_filename, empty_filename, oldfilename, 'rev:revid:-1', oldspec, changetype) diff_lines += dl self._write_file(newfilename, newspec, tmp_diff_to_filename) dl = self._diff_files(empty_filename, tmp_diff_to_filename, newfilename, newspec, 'rev:revid:-1', changetype) diff_lines += dl else: newrevspec = m.group('revspec') parentrevspec = m.group('parentrevspec') logging.debug('Type %s File %s Old %s New %s', changetype, filename, parentrevspec, newrevspec) old_file = new_file = empty_filename if (changetype in [b'A'] or (changetype in [b'C'] and parentrevspec == b'rev:revid:-1')): # There's only one content to show self._write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in [b'C']: self._write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename self._write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in [b'D']: self._write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename else: raise SCMError('Unknown change type "%s" for %s' % (changetype, filename)) dl = self._diff_files(old_file, new_file, filename, newrevspec, parentrevspec, changetype) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return b''.join(diff_lines)
def _path_diff(self, args): """ Process a path-style diff. This allows people to post individual files in various ways. Multiple paths may be specified in `args`. The path styles supported are: //path/to/file Upload file as a "new" file. //path/to/dir/... Upload all files as "new" files. //path/to/file[@#]rev Upload file from that rev as a "new" file. //path/to/file[@#]rev,[@#]rev Upload a diff between revs. //path/to/dir/...[@#]rev,[@#]rev Upload a diff of all files between revs in that directory. """ r_revision_range = re.compile(r'^(?P<path>//[^@#]+)' + r'(?P<revision1>[#@][^,]+)?' + r'(?P<revision2>,[#@][^,]+)?$') empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() diff_lines = [] for path in args: m = r_revision_range.match(path) if not m: die('Path %r does not match a valid Perforce path.' % (path,)) revision1 = m.group('revision1') revision2 = m.group('revision2') first_rev_path = m.group('path') if revision1: first_rev_path += revision1 records = self.p4.files(first_rev_path) # Make a map for convenience. files = {} # Records are: # 'rev': '1' # 'func': '...' # 'time': '1214418871' # 'action': 'edit' # 'type': 'ktext' # 'depotFile': '...' # 'change': '123456' for record in records: if record['action'] not in ('delete', 'move/delete'): if revision2: files[record['depotFile']] = [record, None] else: files[record['depotFile']] = [None, record] if revision2: # [1:] to skip the comma. second_rev_path = m.group('path') + revision2[1:] records = self.p4.files(second_rev_path) for record in records: if record['action'] not in ('delete', 'move/delete'): try: m = files[record['depotFile']] m[1] = record except KeyError: files[record['depotFile']] = [None, record] old_file = new_file = empty_filename changetype_short = None for depot_path, (first_record, second_record) in files.items(): old_file = new_file = empty_filename if first_record is None: new_path = '%s#%s' % (depot_path, second_record['rev']) self._write_file(new_path, tmp_diff_to_filename) new_file = tmp_diff_to_filename changetype_short = 'A' base_revision = 0 elif second_record is None: old_path = '%s#%s' % (depot_path, first_record['rev']) self._write_file(new_path, tmp_diff_from_filename) old_file = tmp_diff_from_filename changetype_short = 'D' base_revision = int(first_record['rev']) elif first_record['rev'] == second_record['rev']: # We when we know the revisions are the same, we don't need # to do any diffing. This speeds up large revision-range # diffs quite a bit. continue else: old_path = '%s#%s' % (depot_path, first_record['rev']) new_path = '%s#%s' % (depot_path, second_record['rev']) self._write_file(old_path, tmp_diff_from_filename) self._write_file(new_path, tmp_diff_to_filename) new_file = tmp_diff_to_filename old_file = tmp_diff_from_filename changetype_short = 'M' base_revision = int(first_record['rev']) # TODO: We're passing new_depot_file='' here just to make # things work like they did before the moved file change was # added (58ccae27). This section of code needs to be updated # to properly work with moved files. dl = self._do_diff(old_file, new_file, depot_path, base_revision, '', changetype_short, ignore_unmodified=True) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return { 'diff': ''.join(diff_lines), }
def _diff_pending(self, changenum): logging.info('Generating diff for pending changeset %s' % changenum) opened_files = self.p4.opened(changenum) if not opened_files: die("Couldn't find any affected files for this change.") diff_lines = [] action_mapping = { 'edit': 'M', 'integrate': 'M', 'add': 'A', 'branch': 'A', 'delete': 'D', } if (self.capabilities and self.capabilities.has_capability('scmtools', 'perforce', 'moved_files')): action_mapping['move/add'] = 'MV-a' action_mapping['move/delete'] = 'MV' else: # The Review Board server doesn't support moved files for # perforce--create a diff that shows moved files as adds and # deletes. action_mapping['move/add'] = 'A' action_mapping['move/delete'] = 'D' for f in opened_files: depot_path = f['depotFile'] new_depot_path = '' try: base_revision = int(f['rev']) except ValueError: # For actions like deletes, there won't be any "current # revision". Just pass through whatever was there before base_revision = f['rev'] action = f['action'] empty_file = make_tempfile() old_file = make_tempfile() logging.debug('Processing %s of %s', action, depot_path) try: changetype_short = action_mapping[action] except KeyError: die('Unknown action type "%s" for %s' % (action, depot_path)) if changetype_short == 'M': # Get the old version out of perforce and stick it in # 'old_file' try: old_depot_path = '%s#%s' % (depot_path, base_revision) self._write_file(old_depot_path, old_file) except ValueError, e: logging.warning('Skipping file %s: %s', old_depot_path, e) continue # Just reference the file within the client view for the new # file new_file = self._depot_to_local(depot_path) elif changetype_short == 'A': # Just reference the file within the client view for the new # file new_file = self._depot_to_local(depot_path) if os.path.islink(new_file): logging.warning('Skipping symlink %s', new_file) continue
def changenum_diff(self, changenum): logging.debug("changenum_diff: %s" % (changenum)) files = execute(["cm", "log", "cs:" + changenum, "--csFormat={items}", "--itemFormat={shortstatus} {path} " "rev:revid:{revid} rev:revid:{parentrevid} " "src:{srccmpath} rev:revid:{srcdirrevid} " "dst:{dstcmpath} rev:revid:{dstdirrevid}{newline}"], split_lines = True) logging.debug("got files: %s" % (files)) # Diff generation based on perforce client diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for f in files: f = f.strip() if not f: continue m = re.search(r'(?P<type>[ACIMR]) (?P<file>.*) ' r'(?P<revspec>rev:revid:[-\d]+) ' r'(?P<parentrevspec>rev:revid:[-\d]+) ' r'src:(?P<srcpath>.*) ' r'(?P<srcrevspec>rev:revid:[-\d]+) ' r'dst:(?P<dstpath>.*) ' r'(?P<dstrevspec>rev:revid:[-\d]+)$', f) if not m: die("Could not parse 'cm log' response: %s" % f) changetype = m.group("type") filename = m.group("file") if changetype == "M": # Handle moved files as a delete followed by an add. # Clunky, but at least it works oldfilename = m.group("srcpath") oldspec = m.group("srcrevspec") newfilename = m.group("dstpath") newspec = m.group("dstrevspec") self.write_file(oldfilename, oldspec, tmp_diff_from_filename) dl = self.diff_files(tmp_diff_from_filename, empty_filename, oldfilename, "rev:revid:-1", oldspec, changetype) diff_lines += dl self.write_file(newfilename, newspec, tmp_diff_to_filename) dl = self.diff_files(empty_filename, tmp_diff_to_filename, newfilename, newspec, "rev:revid:-1", changetype) diff_lines += dl else: newrevspec = m.group("revspec") parentrevspec = m.group("parentrevspec") logging.debug("Type %s File %s Old %s New %s" % (changetype, filename, parentrevspec, newrevspec)) old_file = new_file = empty_filename if (changetype in ['A'] or (changetype in ['C', 'I'] and parentrevspec == "rev:revid:-1")): # File was Added, or a Change or Merge (type I) and there # is no parent revision self.write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in ['C', 'I']: # File was Changed or Merged (type I) self.write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename self.write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in ['R']: # File was Removed self.write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename else: die("Don't know how to handle change type '%s' for %s" % (changetype, filename)) dl = self.diff_files(old_file, new_file, filename, newrevspec, parentrevspec, changetype) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return ''.join(diff_lines)
def _diff_files(self, old_file, new_file): """Return unified diff for file. Most effective and reliable way is use gnu diff. """ # In snapshot view, diff can't access history clearcase file version # so copy cc files to tempdir by 'cleartool get -to dest-pname pname', # and compare diff with the new temp ones. if self.viewtype == 'snapshot': # Create temporary file first. tmp_old_file = make_tempfile() tmp_new_file = make_tempfile() # Delete so cleartool can write to them. try: os.remove(tmp_old_file) except OSError: pass try: os.remove(tmp_new_file) except OSError: pass execute(["cleartool", "get", "-to", tmp_old_file, old_file]) execute(["cleartool", "get", "-to", tmp_new_file, new_file]) diff_cmd = ["diff", "-uN", tmp_old_file, tmp_new_file] else: diff_cmd = ["diff", "-uN", old_file, new_file] dl = execute(diff_cmd, extra_ignore_errors=(1, 2), translate_newlines=False) # Replace temporary file name in diff with the one in snapshot view. if self.viewtype == "snapshot": dl = dl.replace(tmp_old_file, old_file) dl = dl.replace(tmp_new_file, new_file) # If the input file has ^M characters at end of line, lets ignore them. dl = dl.replace('\r\r\n', '\r\n') dl = dl.splitlines(True) # Special handling for the output of the diff tool on binary files: # diff outputs "Files a and b differ" # and the code below expects the output to start with # "Binary files " if (len(dl) == 1 and dl[0].startswith('Files %s and %s differ' % (old_file, new_file))): dl = ['Binary files %s and %s differ\n' % (old_file, new_file)] # We need oids of files to translate them to paths on reviewboard # repository. old_oid = execute(["cleartool", "describe", "-fmt", "%On", old_file]) new_oid = execute(["cleartool", "describe", "-fmt", "%On", new_file]) if dl == [] or dl[0].startswith("Binary files "): if dl == []: dl = ["File %s in your changeset is unmodified\n" % new_file] dl.insert(0, "==== %s %s ====\n" % (old_oid, new_oid)) dl.append('\n') else: dl.insert(2, "==== %s %s ====\n" % (old_oid, new_oid)) return dl
def _diff_files(self, old_file, new_file): """Return a unified diff for file. Args: old_file (unicode): The name and version of the old file. new_file (unicode): The name and version of the new file. Returns: bytes: The diff between the two files. """ # In snapshot view, diff can't access history clearcase file version # so copy cc files to tempdir by 'cleartool get -to dest-pname pname', # and compare diff with the new temp ones. if self.viewtype == 'snapshot': # Create temporary file first. tmp_old_file = make_tempfile() tmp_new_file = make_tempfile() # Delete so cleartool can write to them. try: os.remove(tmp_old_file) except OSError: pass try: os.remove(tmp_new_file) except OSError: pass execute(['cleartool', 'get', '-to', tmp_old_file, old_file]) execute(['cleartool', 'get', '-to', tmp_new_file, new_file]) diff_cmd = ['diff', '-uN', tmp_old_file, tmp_new_file] else: diff_cmd = ['diff', '-uN', old_file, new_file] dl = execute(diff_cmd, extra_ignore_errors=(1, 2), results_unicode=False) # Replace temporary file name in diff with the one in snapshot view. if self.viewtype == 'snapshot': dl = dl.replace(tmp_old_file.encode('utf-8'), old_file.encode('utf-8')) dl = dl.replace(tmp_new_file.encode('utf-8'), new_file.encode('utf-8')) # If the input file has ^M characters at end of line, lets ignore them. dl = dl.replace(b'\r\r\n', b'\r\n') dl = dl.splitlines(True) # Special handling for the output of the diff tool on binary files: # diff outputs "Files a and b differ" # and the code below expects the output to start with # "Binary files " if (len(dl) == 1 and dl[0].startswith(b'Files %s and %s differ' % (old_file.encode('utf-8'), new_file.encode('utf-8')))): dl = [b'Binary files %s and %s differ\n' % (old_file.encode('utf-8'), new_file.encode('utf-8'))] # We need oids of files to translate them to paths on reviewboard # repository. old_oid = execute(['cleartool', 'describe', '-fmt', '%On', old_file], results_unicode=False) new_oid = execute(['cleartool', 'describe', '-fmt', '%On', new_file], results_unicode=False) if dl == [] or dl[0].startswith(b'Binary files '): if dl == []: dl = [b'File %s in your changeset is unmodified\n' % new_file.encode('utf-8')] dl.insert(0, b'==== %s %s ====\n' % (old_oid, new_oid)) dl.append(b'\n') else: dl.insert(2, b'==== %s %s ====\n' % (old_oid, new_oid)) return dl
def _path_diff(self, args): """ Process a path-style diff. See _changenum_diff for the alternate version that handles specific change numbers. Multiple paths may be specified in `args`. The path styles supported are: //path/to/file Upload file as a "new" file. //path/to/dir/... Upload all files as "new" files. //path/to/file[@#]rev Upload file from that rev as a "new" file. //path/to/file[@#]rev,[@#]rev Upload a diff between revs. //path/to/dir/...[@#]rev,[@#]rev Upload a diff of all files between revs in that directory. """ r_revision_range = re.compile(r'^(?P<path>//[^@#]+)' + r'(?P<revision1>[#@][^,]+)?' + r'(?P<revision2>,[#@][^,]+)?$') empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() diff_lines = [] for path in args: m = r_revision_range.match(path) if not m: die('Path %r does not match a valid Perforce path.' % (path,)) revision1 = m.group('revision1') revision2 = m.group('revision2') first_rev_path = m.group('path') if revision1: first_rev_path += revision1 records = self._run_p4(['files', first_rev_path]) # Make a map for convenience. files = {} # Records are: # 'rev': '1' # 'func': '...' # 'time': '1214418871' # 'action': 'edit' # 'type': 'ktext' # 'depotFile': '...' # 'change': '123456' for record in records: if record['action'] not in ('delete', 'move/delete'): if revision2: files[record['depotFile']] = [record, None] else: files[record['depotFile']] = [None, record] if revision2: # [1:] to skip the comma. second_rev_path = m.group('path') + revision2[1:] records = self._run_p4(['files', second_rev_path]) for record in records: if record['action'] not in ('delete', 'move/delete'): try: m = files[record['depotFile']] m[1] = record except KeyError: files[record['depotFile']] = [None, record] old_file = new_file = empty_filename changetype_short = None for depot_path, (first_record, second_record) in files.items(): old_file = new_file = empty_filename if first_record is None: self._write_file(depot_path + '#' + second_record['rev'], tmp_diff_to_filename) new_file = tmp_diff_to_filename changetype_short = 'A' base_revision = 0 elif second_record is None: self._write_file(depot_path + '#' + first_record['rev'], tmp_diff_from_filename) old_file = tmp_diff_from_filename changetype_short = 'D' base_revision = int(first_record['rev']) elif first_record['rev'] == second_record['rev']: # We when we know the revisions are the same, we don't need # to do any diffing. This speeds up large revision-range # diffs quite a bit. continue else: self._write_file(depot_path + '#' + first_record['rev'], tmp_diff_from_filename) self._write_file(depot_path + '#' + second_record['rev'], tmp_diff_to_filename) new_file = tmp_diff_to_filename old_file = tmp_diff_from_filename changetype_short = 'M' base_revision = int(first_record['rev']) dl = self._do_diff(old_file, new_file, depot_path, base_revision, changetype_short, ignore_unmodified=True) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return (''.join(diff_lines), None)
def _process_diffs(self, my_diff_entries): # Diff generation based on perforce client diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for f in my_diff_entries: f = f.strip() if not f: continue m = re.search( r'(?P<type>[ACMD]) (?P<file>.*) ' r'(?P<revspec>rev:revid:[-\d]+) ' r'(?P<parentrevspec>rev:revid:[-\d]+) ' r'src:(?P<srcpath>.*) ' r'dst:(?P<dstpath>.*)$', f) if not m: raise SCMError('Could not parse "cm log" response: %s' % f) changetype = m.group("type") filename = m.group("file") if changetype == "M": # Handle moved files as a delete followed by an add. # Clunky, but at least it works oldfilename = m.group("srcpath") oldspec = m.group("revspec") newfilename = m.group("dstpath") newspec = m.group("revspec") self._write_file(oldfilename, oldspec, tmp_diff_from_filename) dl = self._diff_files(tmp_diff_from_filename, empty_filename, oldfilename, "rev:revid:-1", oldspec, changetype) diff_lines += dl self._write_file(newfilename, newspec, tmp_diff_to_filename) dl = self._diff_files(empty_filename, tmp_diff_to_filename, newfilename, newspec, "rev:revid:-1", changetype) diff_lines += dl else: newrevspec = m.group("revspec") parentrevspec = m.group("parentrevspec") logging.debug( "Type %s File %s Old %s New %s" % (changetype, filename, parentrevspec, newrevspec)) old_file = new_file = empty_filename if (changetype in ['A'] or (changetype in ['C'] and parentrevspec == "rev:revid:-1")): # There's only one content to show self._write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in ['C']: self._write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename self._write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in ['D']: self._write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename else: raise SCMError("Don't know how to handle change type " "'%s' for %s" % (changetype, filename)) dl = self._diff_files(old_file, new_file, filename, newrevspec, parentrevspec, changetype) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return ''.join(diff_lines)
def _changenum_diff(self, changenum): """ Process a diff for a particular change number. This handles both pending and submitted changelists. See _path_diff for the alternate version that does diffs of depot paths. """ # TODO: It might be a good idea to enhance PerforceDiffParser to # understand that newFile could include a revision tag for post-submit # reviewing. cl_is_pending = False logging.info("Generating diff for changenum %s" % changenum) description = [] if changenum == "default": cl_is_pending = True else: description = self.p4.describe(changenum=changenum, password=self.options.p4_passwd) if re.search("no such changelist", description[0]): die("CLN %s does not exist." % changenum) # Some P4 wrappers are addding an extra line before the description if '*pending*' in description[0] or '*pending*' in description[1]: cl_is_pending = True v = self.p4d_version if cl_is_pending and (v[0] < 2002 or (v[0] == "2002" and v[1] < 2) or changenum == "default"): # Pre-2002.2 doesn't give file list in pending changelists, # or we don't have a description for a default changeset, # so we have to get it a different way. info = self.p4.opened(changenum) if (len(info) == 1 and info[0].startswith("File(s) not opened on this client.")): die("Couldn't find any affected files for this change.") for line in info: data = line.split(" ") description.append("... %s %s" % (data[0], data[2])) else: # Get the file list for line_num, line in enumerate(description): if 'Affected files ...' in line: break else: # Got to the end of all the description lines and didn't find # what we were looking for. die("Couldn't find any affected files for this change.") description = description[line_num + 2:] diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for line in description: line = line.strip() if not line: continue m = re.search(r'\.\.\. ([^#]+)#(\d+) ' r'(add|edit|delete|integrate|branch|move/add' r'|move/delete)', line) if not m: die("Unsupported line from p4 opened: %s" % line) depot_path = m.group(1) base_revision = int(m.group(2)) if not cl_is_pending: # If the changelist is pending our base revision is the one # that's currently in the depot. If we're not pending the base # revision is actually the revision prior to this one. base_revision -= 1 changetype = m.group(3) logging.debug('Processing %s of %s' % (changetype, depot_path)) old_file = new_file = empty_filename old_depot_path = new_depot_path = None new_depot_path = '' changetype_short = None supports_moves = ( self.capabilities and self.capabilities.has_capability('scmtools', 'perforce', 'moved_files')) if changetype in ['edit', 'integrate']: # A big assumption new_revision = base_revision + 1 # We have an old file, get p4 to take this old version from the # depot and put it into a plain old temp file for us old_depot_path = "%s#%s" % (depot_path, base_revision) self._write_file(old_depot_path, tmp_diff_from_filename) old_file = tmp_diff_from_filename # Also print out the new file into a tmpfile if cl_is_pending: new_file = self._depot_to_local(depot_path) else: new_depot_path = "%s#%s" % (depot_path, new_revision) self._write_file(new_depot_path, tmp_diff_to_filename) new_file = tmp_diff_to_filename changetype_short = "M" elif (changetype in ('add', 'branch') or (changetype == 'move/add' and not supports_moves)): # We have a new file, get p4 to put this new file into a pretty # temp file for us. No old file to worry about here. if cl_is_pending: new_file = self._depot_to_local(depot_path) else: new_depot_path = "%s#%s" % (depot_path, 1) self._write_file(new_depot_path, tmp_diff_to_filename) new_file = tmp_diff_to_filename changetype_short = "A" elif (changetype == 'delete' or (changetype == 'move/delete' and not supports_moves)): # We've deleted a file, get p4 to put the deleted file into a # temp file for us. The new file remains the empty file. old_depot_path = "%s#%s" % (depot_path, base_revision) self._write_file(old_depot_path, tmp_diff_from_filename) old_file = tmp_diff_from_filename changetype_short = "D" elif changetype == 'move/add': # The server supports move information. We can ignore this, # though, since there should be a "move/delete" that's handled # at some point. continue elif changetype == 'move/delete': # The server supports move information, and we found a moved # file. We'll figure out where we moved to and represent # that information. stat_info = self.p4.fstat(depot_path, ['clientFile', 'movedFile']) if ('clientFile' not in stat_info or 'movedFile' not in stat_info): die('Unable to get necessary fstat information on %s' % depot_path) old_depot_path = '%s#%s' % (depot_path, base_revision) self._write_file(old_depot_path, tmp_diff_from_filename) old_file = tmp_diff_from_filename # Get information on the new file. moved_stat_info = self.p4.fstat(stat_info['movedFile'], ['clientFile', 'depotFile']) if ('clientFile' not in moved_stat_info or 'depotFile' not in moved_stat_info): die('Unable to get necessary fstat information on %s' % stat_info['movedFile']) # This is a new file and we can just access it directly. # There's no revision 1 like with an add. new_file = moved_stat_info['clientFile'] new_depot_path = moved_stat_info['depotFile'] changetype_short = 'MV' else: die("Unknown change type '%s' for %s" % (changetype, depot_path)) dl = self._do_diff(old_file, new_file, depot_path, base_revision, new_depot_path, changetype_short) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return (''.join(diff_lines), None)
def test_diff_for_submitted_changelist(self): """Testing PerforceClient.diff with a submitted changelist""" class TestWrapper(self.P4DiffTestWrapper): def change(self, changelist): return [{ 'Change': '12345', 'Date': '2013/12/19 11:32:45', 'User': '******', 'Status': 'submitted', 'Description': 'My change description\n', }] def filelog(self, path): return [{ 'change0': '12345', 'action0': 'edit', 'rev0': '3', 'depotFile': '//mydepot/test/README', }] client = PerforceClient(TestWrapper) client.p4.repo_files = [ { 'depotFile': '//mydepot/test/README', 'rev': '2', 'action': 'edit', 'change': '12345', 'text': 'This is a test.\n', }, { 'depotFile': '//mydepot/test/README', 'rev': '3', 'action': 'edit', 'change': '', 'text': 'This is a mess.\n', }, ] readme_file = make_tempfile() client.p4.print_file('//mydepot/test/README#3', readme_file) client.p4.where_files = { '//mydepot/test/README': readme_file, } client.p4.repo_files = [ { 'depotFile': '//mydepot/test/README', 'rev': '2', 'action': 'edit', 'change': '12345', 'text': 'This is a test.\n', }, { 'depotFile': '//mydepot/test/README', 'rev': '3', 'action': 'edit', 'change': '', 'text': 'This is a mess.\n', }, ] revisions = client.parse_revision_spec(['12345']) diff = client.diff(revisions) self._compare_diff(diff, '8af5576f5192ca87731673030efb5f39', expect_changenum=False)
def process_diffs(self, my_diff_entries): # Diff generation based on perforce client diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for f in my_diff_entries: f = f.strip() if not f: continue m = re.search( r"(?P<type>[ACMD]) (?P<file>.*) " r"(?P<revspec>rev:revid:[-\d]+) " r"(?P<parentrevspec>rev:revid:[-\d]+) " r"src:(?P<srcpath>.*) " r"dst:(?P<dstpath>.*)$", f, ) if not m: die("Could not parse 'cm log' response: %s" % f) changetype = m.group("type") filename = m.group("file") if changetype == "M": # Handle moved files as a delete followed by an add. # Clunky, but at least it works oldfilename = m.group("srcpath") oldspec = m.group("revspec") newfilename = m.group("dstpath") newspec = m.group("revspec") self.write_file(oldfilename, oldspec, tmp_diff_from_filename) dl = self.diff_files( tmp_diff_from_filename, empty_filename, oldfilename, "rev:revid:-1", oldspec, changetype ) diff_lines += dl self.write_file(newfilename, newspec, tmp_diff_to_filename) dl = self.diff_files( empty_filename, tmp_diff_to_filename, newfilename, newspec, "rev:revid:-1", changetype ) diff_lines += dl else: newrevspec = m.group("revspec") parentrevspec = m.group("parentrevspec") logging.debug("Type %s File %s Old %s New %s" % (changetype, filename, parentrevspec, newrevspec)) old_file = new_file = empty_filename if changetype in ["A"] or (changetype in ["C"] and parentrevspec == "rev:revid:-1"): # There's only one content to show self.write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in ["C"]: self.write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename self.write_file(filename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype in ["D"]: self.write_file(filename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename else: die("Don't know how to handle change type '%s' for %s" % (changetype, filename)) dl = self.diff_files(old_file, new_file, filename, newrevspec, parentrevspec, changetype) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return "".join(diff_lines)
def _test_diff_with_moved_files(self, expected_diff_hash, caps={}): client = self._build_client() client.capabilities = Capabilities(caps) client.p4.repo_files = [ { 'depotFile': '//mydepot/test/README', 'rev': '2', 'action': 'move/delete', 'change': '12345', 'text': 'This is a test.\n', }, { 'depotFile': '//mydepot/test/README-new', 'rev': '1', 'action': 'move/add', 'change': '12345', 'text': 'This is a mess.\n', }, { 'depotFile': '//mydepot/test/COPYING', 'rev': '2', 'action': 'move/delete', 'change': '12345', 'text': 'Copyright 2013 Joe User.\n', }, { 'depotFile': '//mydepot/test/COPYING-new', 'rev': '1', 'action': 'move/add', 'change': '12345', 'text': 'Copyright 2013 Joe User.\n', }, ] readme_file = make_tempfile() copying_file = make_tempfile() readme_file_new = make_tempfile() copying_file_new = make_tempfile() client.p4.print_file('//mydepot/test/README#2', readme_file) client.p4.print_file('//mydepot/test/COPYING#2', copying_file) client.p4.print_file('//mydepot/test/README-new#1', readme_file_new) client.p4.print_file('//mydepot/test/COPYING-new#1', copying_file_new) client.p4.where_files = { '//mydepot/test/README': readme_file, '//mydepot/test/COPYING': copying_file, '//mydepot/test/README-new': readme_file_new, '//mydepot/test/COPYING-new': copying_file_new, } client.p4.fstat_files = { '//mydepot/test/README': { 'clientFile': readme_file, 'movedFile': '//mydepot/test/README-new', }, '//mydepot/test/README-new': { 'clientFile': readme_file_new, 'depotFile': '//mydepot/test/README-new', }, '//mydepot/test/COPYING': { 'clientFile': copying_file, 'movedFile': '//mydepot/test/COPYING-new', }, '//mydepot/test/COPYING-new': { 'clientFile': copying_file_new, 'depotFile': '//mydepot/test/COPYING-new', }, } revisions = client.parse_revision_spec(['12345']) diff = client.diff(revisions) self._compare_diff(diff, expected_diff_hash)
def branch_diff(self, args): logging.debug("branch diff: %s" % (args)) if len(args) > 0: branch = args[0] else: branch = args if not branch.startswith("br:"): return None if not self.options.branch: self.options.branch = branch files = execute(["cm", "fbc", branch, "--format={3} {4}"], split_lines = True) logging.debug("got files: %s" % (files)) diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for f in files: f = f.strip() if not f: continue m = re.search(r'^(?P<branch>.*)#(?P<revno>\d+) (?P<file>.*)$', f) if not m: die("Could not parse 'cm fbc' response: %s" % f) filename = m.group("file") branch = m.group("branch") revno = m.group("revno") # Get the base revision with a cm find basefiles = execute(["cm", "find", "revs", "where", "item='" + filename + "'", "and", "branch='" + branch + "'", "and", "revno=" + revno, "--format={item} rev:revid:{id} " "rev:revid:{parent}", "--nototal"], split_lines = True) # We only care about the first line m = re.search(r'^(?P<filename>.*) ' r'(?P<revspec>rev:revid:[-\d]+) ' r'(?P<parentrevspec>rev:revid:[-\d]+)$', basefiles[0]) basefilename = m.group("filename") newrevspec = m.group("revspec") parentrevspec = m.group("parentrevspec") # Cope with adds/removes changetype = "C" if parentrevspec == "rev:revid:-1": changetype = "A" elif newrevspec == "rev:revid:-1": changetype = "R" logging.debug("Type %s File %s Old %s New %s" % (changetype, basefilename, parentrevspec, newrevspec)) old_file = new_file = empty_filename if changetype == "A": # File Added self.write_file(basefilename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename elif changetype == "R": # File Removed self.write_file(basefilename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename else: self.write_file(basefilename, parentrevspec, tmp_diff_from_filename) old_file = tmp_diff_from_filename self.write_file(basefilename, newrevspec, tmp_diff_to_filename) new_file = tmp_diff_to_filename dl = self.diff_files(old_file, new_file, basefilename, newrevspec, parentrevspec, changetype) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return ''.join(diff_lines)
def test_edit_file(self): """Testing edit_file""" result = edit_file(make_tempfile(b'Test content')) self.assertEqual(result, 'TEST CONTENT')
def test_diff_for_submitted_changelist(self): """Testing PerforceClient.diff with a submitted changelist""" class TestWrapper(self.P4DiffTestWrapper): def change(self, changelist): return [{ 'Change': '12345', 'Date': '2013/12/19 11:32:45', 'User': '******', 'Status': 'submitted', 'Description': 'My change description\n', }] def filelog(self, path): return [ { 'change0': '12345', 'action0': 'edit', 'rev0': '3', 'depotFile': '//mydepot/test/README', } ] client = PerforceClient(TestWrapper) client.p4.repo_files = [ { 'depotFile': '//mydepot/test/README', 'rev': '2', 'action': 'edit', 'change': '12345', 'text': 'This is a test.\n', }, { 'depotFile': '//mydepot/test/README', 'rev': '3', 'action': 'edit', 'change': '', 'text': 'This is a mess.\n', }, ] readme_file = make_tempfile() client.p4.print_file('//mydepot/test/README#3', readme_file) client.p4.where_files = { '//mydepot/test/README': readme_file, } client.p4.repo_files = [ { 'depotFile': '//mydepot/test/README', 'rev': '2', 'action': 'edit', 'change': '12345', 'text': 'This is a test.\n', }, { 'depotFile': '//mydepot/test/README', 'rev': '3', 'action': 'edit', 'change': '', 'text': 'This is a mess.\n', }, ] revisions = client.parse_revision_spec(['12345']) diff = client.diff(revisions) self._compare_diff(diff, '8af5576f5192ca87731673030efb5f39', expect_changenum=False)
def _changenum_diff(self, changenum): """ Process a diff for a particular change number. This handles both pending and submitted changelists. See _path_diff for the alternate version that does diffs of depot paths. """ # TODO: It might be a good idea to enhance PerforceDiffParser to # understand that newFile could include a revision tag for post-submit # reviewing. cl_is_pending = False logging.info("Generating diff for changenum %s" % changenum) description = [] if changenum == "default": cl_is_pending = True else: describeCmd = ["p4"] if self.options.p4_passwd: describeCmd.append("-P") describeCmd.append(self.options.p4_passwd) describeCmd = describeCmd + ["describe", "-s", changenum] description = execute(describeCmd, split_lines=True) if re.search("no such changelist", description[0]): die("CLN %s does not exist." % changenum) # Some P4 wrappers are addding an extra line before the description if '*pending*' in description[0] or '*pending*' in description[1]: cl_is_pending = True v = self.p4d_version if cl_is_pending and (v[0] < 2002 or (v[0] == "2002" and v[1] < 2) or changenum == "default"): # Pre-2002.2 doesn't give file list in pending changelists, # or we don't have a description for a default changeset, # so we have to get it a different way. info = execute(["p4", "opened", "-c", str(changenum)], split_lines=True) if (len(info) == 1 and info[0].startswith("File(s) not opened on this client.")): die("Couldn't find any affected files for this change.") for line in info: data = line.split(" ") description.append("... %s %s" % (data[0], data[2])) else: # Get the file list for line_num, line in enumerate(description): if 'Affected files ...' in line: break else: # Got to the end of all the description lines and didn't find # what we were looking for. die("Couldn't find any affected files for this change.") description = description[line_num + 2:] diff_lines = [] empty_filename = make_tempfile() tmp_diff_from_filename = make_tempfile() tmp_diff_to_filename = make_tempfile() for line in description: line = line.strip() if not line: continue m = re.search(r'\.\.\. ([^#]+)#(\d+) ' r'(add|edit|delete|integrate|branch|move/add' r'|move/delete)', line) if not m: die("Unsupported line from p4 opened: %s" % line) depot_path = m.group(1) base_revision = int(m.group(2)) if not cl_is_pending: # If the changelist is pending our base revision is the one # that's currently in the depot. If we're not pending the base # revision is actually the revision prior to this one. base_revision -= 1 changetype = m.group(3) logging.debug('Processing %s of %s' % (changetype, depot_path)) old_file = new_file = empty_filename old_depot_path = new_depot_path = None changetype_short = None if changetype in ['edit', 'integrate']: # A big assumption new_revision = base_revision + 1 # We have an old file, get p4 to take this old version from the # depot and put it into a plain old temp file for us old_depot_path = "%s#%s" % (depot_path, base_revision) self._write_file(old_depot_path, tmp_diff_from_filename) old_file = tmp_diff_from_filename # Also print out the new file into a tmpfile if cl_is_pending: new_file = self._depot_to_local(depot_path) else: new_depot_path = "%s#%s" % (depot_path, new_revision) self._write_file(new_depot_path, tmp_diff_to_filename) new_file = tmp_diff_to_filename changetype_short = "M" elif changetype in ['add', 'branch', 'move/add']: # We have a new file, get p4 to put this new file into a pretty # temp file for us. No old file to worry about here. if cl_is_pending: new_file = self._depot_to_local(depot_path) else: new_depot_path = "%s#%s" % (depot_path, 1) self._write_file(new_depot_path, tmp_diff_to_filename) new_file = tmp_diff_to_filename changetype_short = "A" elif changetype in ['delete', 'move/delete']: # We've deleted a file, get p4 to put the deleted file into a # temp file for us. The new file remains the empty file. old_depot_path = "%s#%s" % (depot_path, base_revision) self._write_file(old_depot_path, tmp_diff_from_filename) old_file = tmp_diff_from_filename changetype_short = "D" else: die("Unknown change type '%s' for %s" % (changetype, depot_path)) dl = self._do_diff(old_file, new_file, depot_path, base_revision, changetype_short) diff_lines += dl os.unlink(empty_filename) os.unlink(tmp_diff_from_filename) os.unlink(tmp_diff_to_filename) return (''.join(diff_lines), None)
def main(self, review_request_id): """Run the command. Args: review_request_id (int): The ID of the review request to patch from. Raises: rbtools.command.CommandError: Patching the tree has failed. """ patch_stdout = self.options.patch_stdout revert = self.options.revert_patch if patch_stdout and revert: raise CommandError(_('--print and --revert cannot both be used.')) repository_info, tool = self.initialize_scm_tool( client_name=self.options.repository_type, require_repository_info=not patch_stdout) if revert and not tool.supports_patch_revert: raise CommandError( _('The %s backend does not support reverting patches.') % tool.name) server_url = self.get_server_url(repository_info, tool) api_client, api_root = self.get_api(server_url) self.setup_tool(tool, api_root=api_root) if not patch_stdout: # Check if the repository info on the Review Board server matches # the local checkout. repository_info = repository_info.find_server_repository_info( api_root) # Get the patch, the used patch ID and base dir for the diff patch_data = self.get_patch( tool, api_root, review_request_id, self.options.diff_revision, self.options.commit_id) diff_body = patch_data['diff'] diff_revision = patch_data['revision'] base_dir = patch_data['base_dir'] if self.options.patch_stdout: if isinstance(diff_body, bytes): print(diff_body.decode('utf-8')) else: print(diff_body) else: try: if tool.has_pending_changes(): message = 'Working directory is not clean.' if not self.options.commit: print('Warning: %s' % message) else: raise CommandError(message) except NotImplementedError: pass tmp_patch_file = make_tempfile(diff_body) success = self.apply_patch( repository_info, tool, review_request_id, diff_revision, tmp_patch_file, base_dir, revert=self.options.revert_patch) if not success: raise CommandError('Could not apply patch') if self.options.commit or self.options.commit_no_edit: if patch_data['commit_meta'] is not None: # We are patching a commit so we already have the metadata # required without making additional HTTP requests. meta = patch_data['commit_meta'] message = meta['message'] # Fun fact: object does not have a __dict__ so you cannot # call setattr() on them. We need this ability so we are # creating a type that does. author = type('Author', (object,), {})() author.fullname = meta['author_name'] author.email = meta['author_email'] else: try: review_request = api_root.get_review_request( review_request_id=review_request_id, force_text_type='plain') except APIError as e: raise CommandError('Error getting review request %s: %s' % (request_id, e)) message = extract_commit_message(review_request) author = review_request.get_submitter() try: tool.create_commit(message, author, not self.options.commit_no_edit) print('Changes committed to current branch.') except NotImplementedError: raise CommandError('--commit is not supported with %s' % tool.name)