diff options
-rwxr-xr-x | scripts/post-review | 343 |
1 files changed, 258 insertions, 85 deletions
diff --git a/scripts/post-review b/scripts/post-review index a502cda..f2a0102 100755 --- a/scripts/post-review +++ b/scripts/post-review @@ -2,11 +2,11 @@ import cookielib import difflib import getpass +import marshal import mimetools import ntpath import os import re -import shutil import simplejson import socket import subprocess @@ -1321,27 +1321,20 @@ class PerforceClient(SCMClient): return None + def get_changenum(self, args): + if len(args) == 1: + try: + return str(int(args[0])) + except ValueError: + pass + return None + def diff(self, args): """ Goes through the hard work of generating a diff on Perforce in order to take into account adds/deletes and to provide the necessary revision information. """ - if len(args) != 1: - print "Specify the change number of a pending changeset" - sys.exit(1) - - changenum = args[0] - - cl_is_pending = False - - try: - changenum = int(changenum) - except ValueError: - die("You must enter a valid change number") - - debug("Generating diff for changenum %s" % changenum) - # set the P4 enviroment: if options.p4_client: os.environ['P4CLIENT'] = options.p4_client @@ -1349,8 +1342,174 @@ class PerforceClient(SCMClient): if options.p4_port: os.environ['P4PORT'] = options.p4_port + changenum = self.get_changenum(args) + if changenum is None: + return self._path_diff(args) + else: + return self._changenum_diff(changenum) + + + 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'] != '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'] != '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']) + 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 _run_p4(self, command): + """Execute a perforce command using the python marshal API. - description = execute(["p4", "describe", "-s", str(changenum)], + - command: A list of strings of the command to execute. + + The return type depends on the command being run. + """ + command = ['p4', '-G'] + command + p = subprocess.Popen(command, stdout=subprocess.PIPE) + result = [] + has_error = False + + while 1: + try: + data = marshal.load(p.stdout) + except EOFError: + break + else: + result.append(data) + if data.get('code', None) == 'error': + has_error = True + + rc = p.wait() + + if rc or has_error: + for record in result: + if 'data' in record: + print record['data'] + die('Failed to execute command: %s\n' % (command,)) + + return result + + 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 + + debug("Generating diff for changenum %s" % changenum) + + description = execute(["p4", "describe", "-s", changenum], split_lines=True) if '*pending*' in description[0]: @@ -1367,7 +1526,6 @@ class PerforceClient(SCMClient): description = description[line_num+2:] - cwd = os.getcwd() diff_lines = [] empty_filename = make_tempfile() @@ -1395,8 +1553,6 @@ class PerforceClient(SCMClient): debug('Processing %s of %s' % (changetype, depot_path)) - local_name = m.group(1) - old_file = new_file = empty_filename old_depot_path = new_depot_path = None changetype_short = None @@ -1441,73 +1597,94 @@ class PerforceClient(SCMClient): else: die("Unknown change type '%s' for %s" % (changetype, depot_path)) - diff_cmd = ["diff", "-urNp", old_file, new_file] - # Diff returns "1" if differences were found. - dl = execute(diff_cmd, extra_ignore_errors=(1,2)).splitlines(True) - - if local_name.startswith(cwd): - local_path = local_name[len(cwd) + 1:] - else: - local_path = local_name - - # 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 outptu 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'% (old_file, new_file)] - - if dl == [] or dl[0].startswith("Binary files "): - if dl == []: - print "Warning: %s in your changeset is unmodified" % \ - local_path - - dl.insert(0, "==== %s#%s ==%s== %s ====\n" % \ - (depot_path, base_revision, changetype_short, local_path)) - else: - m = re.search(r'(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)', dl[1]) - if m: - timestamp = m.group(1) - else: - # Thu Sep 3 11:24:48 2007 - m = re.search(r'(\w+)\s+(\w+)\s+(\d+)\s+(\d\d:\d\d:\d\d)\s+(\d\d\d\d)', dl[1]) - if not m: - die("Unable to parse diff header: %s" % dl[1]) - - month_map = { - "Jan": "01", - "Feb": "02", - "Mar": "03", - "Apr": "04", - "May": "05", - "Jun": "06", - "Jul": "07", - "Aug": "08", - "Sep": "09", - "Oct": "10", - "Nov": "11", - "Dec": "12", - } - month = month_map[m.group(2)] - day = m.group(3) - timestamp = m.group(4) - year = m.group(5) - - timestamp = "%s-%s-%s %s" % (year, month, day, timestamp) - - dl[0] = "--- %s\t%s#%s\n" % (local_path, depot_path, base_revision) - dl[1] = "+++ %s\t%s\n" % (local_path, timestamp) - + 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 _do_diff(self, old_file, new_file, depot_path, base_revision, + changetype_short, ignore_unmodified=False): + """ + Do the work of producing a diff for Perforce. + + old_file - The absolute path to the "old" file. + new_file - The absolute path to the "new" file. + depot_path - The depot path in Perforce for this file. + base_revision - The base perforce revision number of the old file as + an integer. + changetype_short - The change type as a single character string. + ignore_unmodified - If True, will return an empty list if the file + is not changed. + + Returns a list of strings of diff lines. + """ + diff_cmd = ["diff", "-urNp", old_file, new_file] + # Diff returns "1" if differences were found. + dl = execute(diff_cmd, extra_ignore_errors=(1,2)).splitlines(True) + + cwd = os.getcwd() + if depot_path.startswith(cwd): + local_path = depot_path[len(cwd) + 1:] + else: + local_path = depot_path + + # 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] == ('Files %s and %s differ'% (old_file, new_file)): + dl = ['Binary files %s and %s differ'% (old_file, new_file)] + + if dl == [] or dl[0].startswith("Binary files "): + if dl == []: + if ignore_unmodified: + return [] + else: + print "Warning: %s in your changeset is unmodified" % \ + local_path + + dl.insert(0, "==== %s#%s ==%s== %s ====\n" % \ + (depot_path, base_revision, changetype_short, local_path)) + else: + m = re.search(r'(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)', dl[1]) + if m: + timestamp = m.group(1) + else: + # Thu Sep 3 11:24:48 2007 + m = re.search(r'(\w+)\s+(\w+)\s+(\d+)\s+(\d\d:\d\d:\d\d)\s+(\d\d\d\d)', dl[1]) + if not m: + die("Unable to parse diff header: %s" % dl[1]) + + month_map = { + "Jan": "01", + "Feb": "02", + "Mar": "03", + "Apr": "04", + "May": "05", + "Jun": "06", + "Jul": "07", + "Aug": "08", + "Sep": "09", + "Oct": "10", + "Nov": "11", + "Dec": "12", + } + month = month_map[m.group(2)] + day = m.group(3) + timestamp = m.group(4) + year = m.group(5) + + timestamp = "%s-%s-%s %s" % (year, month, day, timestamp) + + dl[0] = "--- %s\t%s#%s\n" % (local_path, depot_path, base_revision) + dl[1] = "+++ %s\t%s\n" % (local_path, timestamp) + + return dl + def _write_file(self, depot_path, tmpfile): """ Grabs a file from Perforce and writes it to a temp file. We do this @@ -2266,11 +2443,7 @@ def main(args): server = ReviewBoardServer(server_url, repository_info, cookie_file) if repository_info.supports_changesets: - if len(args) < 1: - print "You must include a change set number" - sys.exit(1) - - changenum = args[0] + changenum = tool.get_changenum(args) else: changenum = None |