| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 # | 5 # |
| 6 # Wrapper script around Rietveld's upload.py that groups files into | 6 # Wrapper script around Rietveld's upload.py that groups files into |
| 7 # changelists. | 7 # changelists. |
| 8 | 8 |
| 9 import getpass | 9 import getpass |
| 10 import os | 10 import os |
| 11 import random | 11 import random |
| 12 import re | 12 import re |
| 13 import string | 13 import string |
| 14 import subprocess | 14 import subprocess |
| 15 import sys | 15 import sys |
| 16 import tempfile | 16 import tempfile |
| 17 import upload | 17 import upload |
| 18 import urllib2 | 18 import urllib2 |
| 19 import xml.dom.minidom |
| 19 | 20 |
| 20 | 21 |
| 21 __version__ = '1.0' | 22 __version__ = '1.0' |
| 22 | 23 |
| 23 | 24 |
| 24 CODEREVIEW_SETTINGS = { | 25 CODEREVIEW_SETTINGS = { |
| 25 # Default values. | 26 # Default values. |
| 26 "CODE_REVIEW_SERVER": "codereview.chromium.org", | 27 "CODE_REVIEW_SERVER": "codereview.chromium.org", |
| 27 "CC_LIST": "chromium-reviews@googlegroups.com", | 28 "CC_LIST": "chromium-reviews@googlegroups.com", |
| 28 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", | 29 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", |
| (...skipping 11 matching lines...) Expand all Loading... |
| 40 # Filename where we store repository specific information for gcl. | 41 # Filename where we store repository specific information for gcl. |
| 41 CODEREVIEW_SETTINGS_FILE = "codereview.settings" | 42 CODEREVIEW_SETTINGS_FILE = "codereview.settings" |
| 42 | 43 |
| 43 # Warning message when the change appears to be missing tests. | 44 # Warning message when the change appears to be missing tests. |
| 44 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" | 45 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" |
| 45 | 46 |
| 46 # Caches whether we read the codereview.settings file yet or not. | 47 # Caches whether we read the codereview.settings file yet or not. |
| 47 read_gcl_info = False | 48 read_gcl_info = False |
| 48 | 49 |
| 49 | 50 |
| 51 ### Simplified XML processing functions. |
| 52 |
| 53 def ParseXML(output): |
| 54 try: |
| 55 return xml.dom.minidom.parseString(output) |
| 56 except xml.parsers.expat.ExpatError: |
| 57 return None |
| 58 |
| 59 def GetNamedNodeText(node, node_name): |
| 60 child_nodes = node.getElementsByTagName(node_name) |
| 61 if not child_nodes: |
| 62 return None |
| 63 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1 |
| 64 return child_nodes[0].firstChild.nodeValue |
| 65 |
| 66 |
| 67 def GetNodeNamedAttributeText(node, node_name, attribute_name): |
| 68 child_nodes = node.getElementsByTagName(node_name) |
| 69 if not child_nodes: |
| 70 return None |
| 71 assert len(child_nodes) == 1 |
| 72 return child_nodes[0].getAttribute(attribute_name) |
| 73 |
| 74 |
| 75 ### SVN Functions |
| 76 |
| 50 def IsSVNMoved(filename): | 77 def IsSVNMoved(filename): |
| 51 """Determine if a file has been added through svn mv""" | 78 """Determine if a file has been added through svn mv""" |
| 52 info = GetSVNFileInfo(filename) | 79 info = GetSVNFileInfo(filename) |
| 53 return (info.get('Copied From URL') and | 80 return (info.get('Copied From URL') and |
| 54 info.get('Copied From Rev') and | 81 info.get('Copied From Rev') and |
| 55 info.get('Schedule') == 'add') | 82 info.get('Schedule') == 'add') |
| 56 | 83 |
| 57 | 84 |
| 58 def GetSVNFileInfo(file): | 85 def GetSVNFileInfo(file): |
| 59 """Returns a dictionary from the svn info output for the given file.""" | 86 """Returns a dictionary from the svn info output for the given file.""" |
| 60 output = RunShell(["svn", "info", file]) | 87 dom = ParseXML(RunShell(["svn", "info", "--xml", file])) |
| 61 result = {} | 88 result = {} |
| 62 re_key_value_pair = re.compile('^(.*)\: (.*)$') | 89 if dom: |
| 63 for line in output.splitlines(): | 90 # /info/entry/ |
| 64 key_value_pair = re_key_value_pair.match(line) | 91 # url |
| 65 if key_value_pair: | 92 # reposityory/(root|uuid) |
| 66 result[key_value_pair.group(1)] = key_value_pair.group(2) | 93 # wc-info/(schedule|depth) |
| 94 # commit/(author|date) |
| 95 result['Node Kind'] = GetNodeNamedAttributeText(dom, 'entry', 'kind') |
| 96 result['Repository Root'] = GetNamedNodeText(dom, 'root') |
| 97 result['Schedule'] = GetNamedNodeText(dom, 'schedule') |
| 98 result['URL'] = GetNamedNodeText(dom, 'url') |
| 99 result['Path'] = GetNodeNamedAttributeText(dom, 'entry', 'path') |
| 100 result['Copied From URL'] = GetNamedNodeText(dom, 'copy-from-url') |
| 101 result['Copied From Rev'] = GetNamedNodeText(dom, 'copy-from-rev') |
| 67 return result | 102 return result |
| 68 | 103 |
| 69 | 104 |
| 70 def GetSVNFileProperty(file, property_name): | 105 def GetSVNFileProperty(file, property_name): |
| 71 """Returns the value of an SVN property for the given file. | 106 """Returns the value of an SVN property for the given file. |
| 72 | 107 |
| 73 Args: | 108 Args: |
| 74 file: The file to check | 109 file: The file to check |
| 75 property_name: The name of the SVN property, e.g. "svn:mime-type" | 110 property_name: The name of the SVN property, e.g. "svn:mime-type" |
| 76 | 111 |
| 77 Returns: | 112 Returns: |
| 78 The value of the property, which will be the empty string if the property | 113 The value of the property, which will be the empty string if the property |
| 79 is not set on the file. If the file is not under version control, the | 114 is not set on the file. If the file is not under version control, the |
| 80 empty string is also returned. | 115 empty string is also returned. |
| 81 """ | 116 """ |
| 82 output = RunShell(["svn", "propget", property_name, file]) | 117 output = RunShell(["svn", "propget", property_name, file]) |
| 83 if (output.startswith("svn: ") and | 118 if (output.startswith("svn: ") and |
| 84 output.endswith("is not under version control")): | 119 output.endswith("is not under version control")): |
| 85 return "" | 120 return "" |
| 86 else: | 121 else: |
| 87 return output | 122 return output |
| 88 | 123 |
| 89 | 124 |
| 125 def GetSVNStatus(file): |
| 126 """Returns the svn 1.5 svn status emulated output. |
| 127 |
| 128 @file can be a string (one file) or a list of files.""" |
| 129 command = ["svn", "status", "--xml"] |
| 130 if file is None: |
| 131 pass |
| 132 elif isinstance(file, basestring): |
| 133 command.append(file) |
| 134 else: |
| 135 command.extend(file) |
| 136 |
| 137 status_letter = { |
| 138 '': ' ', |
| 139 'unversioned': '?', |
| 140 'modified': 'M', |
| 141 'added': 'A', |
| 142 'conflicted': 'C', |
| 143 'deleted': 'D', |
| 144 'ignored': 'I', |
| 145 'replaced': 'R', |
| 146 # TODO(maruel): Find the corresponding strings for X, !, ~ |
| 147 } |
| 148 dom = ParseXML(RunShell(command)) |
| 149 results = [] |
| 150 if dom: |
| 151 # /status/target/entry/(wc-status|commit|author|date) |
| 152 for target in dom.getElementsByTagName('target'): |
| 153 base_path = target.getAttribute('path') |
| 154 for entry in target.getElementsByTagName('entry'): |
| 155 file = entry.getAttribute('path') |
| 156 wc_status = entry.getElementsByTagName('wc-status') |
| 157 assert len(wc_status) == 1 |
| 158 # Emulate svn 1.5 status ouput... |
| 159 statuses = [' ' for i in range(7)] |
| 160 # Col 0 |
| 161 xml_item_status = wc_status[0].getAttribute('item') |
| 162 if xml_item_status in status_letter: |
| 163 statuses[0] = status_letter[xml_item_status] |
| 164 else: |
| 165 raise Exception('Unknown item status "%s"; please implement me!' % |
| 166 xml_item_status) |
| 167 # Col 1 |
| 168 xml_props_status = wc_status[0].getAttribute('props') |
| 169 if xml_props_status == 'modified': |
| 170 statuses[1] = 'M' |
| 171 elif xml_props_status == 'conflicted': |
| 172 statuses[1] = 'C' |
| 173 elif (not xml_props_status or xml_props_status == 'none' or |
| 174 xml_props_status == 'normal'): |
| 175 pass |
| 176 else: |
| 177 raise Exception('Unknown props status "%s"; please implement me!' % |
| 178 xml_props_status) |
| 179 # Col 3 |
| 180 if wc_status[0].getAttribute('copied') == 'true': |
| 181 statuses[3] = '+' |
| 182 item = (''.join(statuses), file) |
| 183 results.append(item) |
| 184 return results |
| 185 |
| 186 |
| 187 def UnknownFiles(extra_args): |
| 188 """Runs svn status and prints unknown files. |
| 189 |
| 190 Any args in |extra_args| are passed to the tool to support giving alternate |
| 191 code locations. |
| 192 """ |
| 193 return [item[1] for item in GetSVNStatus(extra_args) if item[0][0] == '?'] |
| 194 |
| 195 |
| 90 def GetRepositoryRoot(): | 196 def GetRepositoryRoot(): |
| 91 """Returns the top level directory of the current repository. | 197 """Returns the top level directory of the current repository. |
| 92 | 198 |
| 93 The directory is returned as an absolute path. | 199 The directory is returned as an absolute path. |
| 94 """ | 200 """ |
| 95 global repository_root | 201 global repository_root |
| 96 if not repository_root: | 202 if not repository_root: |
| 97 cur_dir_repo_root = GetSVNFileInfo(os.getcwd()).get("Repository Root") | 203 cur_dir_repo_root = GetSVNFileInfo(os.getcwd()).get("Repository Root") |
| 98 if not cur_dir_repo_root: | 204 if not cur_dir_repo_root: |
| 99 raise Exception("gcl run outside of repository") | 205 raise Exception("gcl run outside of repository") |
| (...skipping 311 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 411 files = [] | 517 files = [] |
| 412 for line in split_data[1].splitlines(): | 518 for line in split_data[1].splitlines(): |
| 413 status = line[:7] | 519 status = line[:7] |
| 414 file = line[7:] | 520 file = line[7:] |
| 415 files.append((status, file)) | 521 files.append((status, file)) |
| 416 description = split_data[2] | 522 description = split_data[2] |
| 417 save = False | 523 save = False |
| 418 if update_status: | 524 if update_status: |
| 419 for file in files: | 525 for file in files: |
| 420 filename = os.path.join(GetRepositoryRoot(), file[1]) | 526 filename = os.path.join(GetRepositoryRoot(), file[1]) |
| 421 status = RunShell(["svn", "status", filename])[:7] | 527 status_result = GetSVNStatus(filename) |
| 422 if not status: # File has been reverted. | 528 if not status_result or not status_result[0][0]: |
| 529 # File has been reverted. |
| 423 save = True | 530 save = True |
| 424 files.remove(file) | 531 files.remove(file) |
| 425 elif status != file[0]: | 532 elif status != file[0]: |
| 426 save = True | 533 save = True |
| 427 files[files.index(file)] = (status, file[1]) | 534 files[files.index(file)] = (status, file[1]) |
| 428 change_info = ChangeInfo(changename, issue, description, files) | 535 change_info = ChangeInfo(changename, issue, description, files) |
| 429 if save: | 536 if save: |
| 430 change_info.Save() | 537 change_info.Save() |
| 431 return change_info | 538 return change_info |
| 432 | 539 |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 467 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep) | 574 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep) |
| 468 | 575 |
| 469 # Get a list of all files in changelists. | 576 # Get a list of all files in changelists. |
| 470 files_in_cl = {} | 577 files_in_cl = {} |
| 471 for cl in GetCLs(): | 578 for cl in GetCLs(): |
| 472 change_info = LoadChangelistInfo(cl) | 579 change_info = LoadChangelistInfo(cl) |
| 473 for status, filename in change_info.files: | 580 for status, filename in change_info.files: |
| 474 files_in_cl[filename] = change_info.name | 581 files_in_cl[filename] = change_info.name |
| 475 | 582 |
| 476 # Get all the modified files. | 583 # Get all the modified files. |
| 477 status = RunShell(["svn", "status"]) | 584 status_result = GetSVNStatus(None) |
| 478 for line in status.splitlines(): | 585 for line in status_result: |
| 479 if not len(line) or line[0] == "?": | 586 status = line[0] |
| 587 filename = line[1] |
| 588 if status[0] == "?": |
| 480 continue | 589 continue |
| 481 status = line[:7] | |
| 482 filename = line[7:].strip() | |
| 483 if dir_prefix: | 590 if dir_prefix: |
| 484 filename = os.path.join(dir_prefix, filename) | 591 filename = os.path.join(dir_prefix, filename) |
| 485 change_list_name = "" | 592 change_list_name = "" |
| 486 if filename in files_in_cl: | 593 if filename in files_in_cl: |
| 487 change_list_name = files_in_cl[filename] | 594 change_list_name = files_in_cl[filename] |
| 488 files.setdefault(change_list_name, []).append((status, filename)) | 595 files.setdefault(change_list_name, []).append((status, filename)) |
| 489 | 596 |
| 490 return files | 597 return files |
| 491 | 598 |
| 492 | 599 |
| (...skipping 30 matching lines...) Expand all Loading... |
| 523 ErrorExit("Error accessing url %s" % request_path) | 630 ErrorExit("Error accessing url %s" % request_path) |
| 524 else: | 631 else: |
| 525 return None | 632 return None |
| 526 | 633 |
| 527 | 634 |
| 528 def GetIssueDescription(issue): | 635 def GetIssueDescription(issue): |
| 529 """Returns the issue description from Rietveld.""" | 636 """Returns the issue description from Rietveld.""" |
| 530 return SendToRietveld("/" + issue + "/description") | 637 return SendToRietveld("/" + issue + "/description") |
| 531 | 638 |
| 532 | 639 |
| 533 def UnknownFiles(extra_args): | |
| 534 """Runs svn status and prints unknown files. | |
| 535 | |
| 536 Any args in |extra_args| are passed to the tool to support giving alternate | |
| 537 code locations. | |
| 538 """ | |
| 539 args = ["svn", "status"] | |
| 540 args += extra_args | |
| 541 p = subprocess.Popen(args, stdout = subprocess.PIPE, | |
| 542 stderr = subprocess.STDOUT, shell = use_shell) | |
| 543 while 1: | |
| 544 line = p.stdout.readline() | |
| 545 if not line: | |
| 546 break | |
| 547 if line[0] != '?': | |
| 548 continue # Not an unknown file to svn. | |
| 549 # The lines look like this: | |
| 550 # "? foo.txt" | |
| 551 # and we want just "foo.txt" | |
| 552 print line[7:].strip() | |
| 553 p.wait() | |
| 554 p.stdout.close() | |
| 555 | |
| 556 | |
| 557 def Opened(): | 640 def Opened(): |
| 558 """Prints a list of modified files in the current directory down.""" | 641 """Prints a list of modified files in the current directory down.""" |
| 559 files = GetModifiedFiles() | 642 files = GetModifiedFiles() |
| 560 cl_keys = files.keys() | 643 cl_keys = files.keys() |
| 561 cl_keys.sort() | 644 cl_keys.sort() |
| 562 for cl_name in cl_keys: | 645 for cl_name in cl_keys: |
| 563 if cl_name: | 646 if cl_name: |
| 564 note = "" | 647 note = "" |
| 565 if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]): | 648 if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]): |
| 566 note = " (Note: this changelist contains files outside this directory)" | 649 note = " (Note: this changelist contains files outside this directory)" |
| (...skipping 530 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1097 # the files. This allows commands such as 'gcl diff xxx' to work. | 1180 # the files. This allows commands such as 'gcl diff xxx' to work. |
| 1098 args =["svn", command] | 1181 args =["svn", command] |
| 1099 root = GetRepositoryRoot() | 1182 root = GetRepositoryRoot() |
| 1100 args.extend([os.path.join(root, x) for x in change_info.FileList()]) | 1183 args.extend([os.path.join(root, x) for x in change_info.FileList()]) |
| 1101 RunShell(args, True) | 1184 RunShell(args, True) |
| 1102 return 0 | 1185 return 0 |
| 1103 | 1186 |
| 1104 | 1187 |
| 1105 if __name__ == "__main__": | 1188 if __name__ == "__main__": |
| 1106 sys.exit(main()) | 1189 sys.exit(main()) |
| OLD | NEW |