| 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 shutil |
| 13 import string | 14 import string |
| 14 import subprocess | 15 import subprocess |
| 15 import sys | 16 import sys |
| 16 import tempfile | 17 import tempfile |
| 17 import upload | 18 import upload |
| 18 import urllib2 | 19 import urllib2 |
| 19 import xml.dom.minidom | 20 import xml.dom.minidom |
| 20 | 21 |
| 21 # gcl now depends on gclient. | 22 # gcl now depends on gclient. |
| 22 import gclient | 23 import gclient |
| 23 | 24 |
| 24 __version__ = '1.0' | 25 __version__ = '1.1.1' |
| 25 | 26 |
| 26 | 27 |
| 27 CODEREVIEW_SETTINGS = { | 28 CODEREVIEW_SETTINGS = { |
| 28 # Default values. | 29 # Default values. |
| 29 "CODE_REVIEW_SERVER": "codereview.chromium.org", | 30 "CODE_REVIEW_SERVER": "codereview.chromium.org", |
| 30 "CC_LIST": "chromium-reviews@googlegroups.com", | 31 "CC_LIST": "chromium-reviews@googlegroups.com", |
| 31 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", | 32 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", |
| 32 } | 33 } |
| 33 | 34 |
| 34 # globals that store the root of the current repository and the directory where | 35 # globals that store the root of the current repository and the directory where |
| 35 # we store information about changelists. | 36 # we store information about changelists. |
| 36 repository_root = "" | 37 REPOSITORY_ROOT = "" |
| 37 gcl_info_dir = "" | |
| 38 | 38 |
| 39 # Filename where we store repository specific information for gcl. | 39 # Filename where we store repository specific information for gcl. |
| 40 CODEREVIEW_SETTINGS_FILE = "codereview.settings" | 40 CODEREVIEW_SETTINGS_FILE = "codereview.settings" |
| 41 | 41 |
| 42 # Warning message when the change appears to be missing tests. | 42 # Warning message when the change appears to be missing tests. |
| 43 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" | 43 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" |
| 44 | 44 |
| 45 # Caches whether we read the codereview.settings file yet or not. | 45 # Global cache of files cached in GetCacheDir(). |
| 46 read_gcl_info = False | 46 FILES_CACHE = {} |
| 47 | 47 |
| 48 | 48 |
| 49 ### SVN Functions | 49 ### SVN Functions |
| 50 | 50 |
| 51 def IsSVNMoved(filename): | 51 def IsSVNMoved(filename): |
| 52 """Determine if a file has been added through svn mv""" | 52 """Determine if a file has been added through svn mv""" |
| 53 info = gclient.CaptureSVNInfo(filename) | 53 info = gclient.CaptureSVNInfo(filename) |
| 54 return (info.get('Copied From URL') and | 54 return (info.get('Copied From URL') and |
| 55 info.get('Copied From Rev') and | 55 info.get('Copied From Rev') and |
| 56 info.get('Schedule') == 'add') | 56 info.get('Schedule') == 'add') |
| (...skipping 27 matching lines...) Expand all Loading... |
| 84 """ | 84 """ |
| 85 return [item[1] for item in gclient.CaptureSVNStatus(extra_args) | 85 return [item[1] for item in gclient.CaptureSVNStatus(extra_args) |
| 86 if item[0][0] == '?'] | 86 if item[0][0] == '?'] |
| 87 | 87 |
| 88 | 88 |
| 89 def GetRepositoryRoot(): | 89 def GetRepositoryRoot(): |
| 90 """Returns the top level directory of the current repository. | 90 """Returns the top level directory of the current repository. |
| 91 | 91 |
| 92 The directory is returned as an absolute path. | 92 The directory is returned as an absolute path. |
| 93 """ | 93 """ |
| 94 global repository_root | 94 global REPOSITORY_ROOT |
| 95 if not repository_root: | 95 if not REPOSITORY_ROOT: |
| 96 infos = gclient.CaptureSVNInfo(os.getcwd(), print_error=False) | 96 infos = gclient.CaptureSVNInfo(os.getcwd(), print_error=False) |
| 97 cur_dir_repo_root = infos.get("Repository Root") | 97 cur_dir_repo_root = infos.get("Repository Root") |
| 98 if not cur_dir_repo_root: | 98 if not cur_dir_repo_root: |
| 99 raise Exception("gcl run outside of repository") | 99 raise Exception("gcl run outside of repository") |
| 100 | 100 |
| 101 repository_root = os.getcwd() | 101 REPOSITORY_ROOT = os.getcwd() |
| 102 while True: | 102 while True: |
| 103 parent = os.path.dirname(repository_root) | 103 parent = os.path.dirname(REPOSITORY_ROOT) |
| 104 if (gclient.CaptureSVNInfo(parent, print_error=False).get( | 104 if (gclient.CaptureSVNInfo(parent, print_error=False).get( |
| 105 "Repository Root") != cur_dir_repo_root): | 105 "Repository Root") != cur_dir_repo_root): |
| 106 break | 106 break |
| 107 repository_root = parent | 107 REPOSITORY_ROOT = parent |
| 108 return repository_root | 108 return REPOSITORY_ROOT |
| 109 | 109 |
| 110 | 110 |
| 111 def GetInfoDir(): | 111 def GetInfoDir(): |
| 112 """Returns the directory where gcl info files are stored.""" | 112 """Returns the directory where gcl info files are stored.""" |
| 113 global gcl_info_dir | 113 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info') |
| 114 if not gcl_info_dir: | 114 |
| 115 gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info') | 115 |
| 116 return gcl_info_dir | 116 def GetChangesDir(): |
| 117 """Returns the directory where gcl change files are stored.""" |
| 118 return os.path.join(GetInfoDir(), 'changes') |
| 119 |
| 120 |
| 121 def GetCacheDir(): |
| 122 """Returns the directory where gcl change files are stored.""" |
| 123 return os.path.join(GetInfoDir(), 'cache') |
| 124 |
| 125 |
| 126 def GetCachedFile(filename, max_age=60*60*24*3, use_root=False): |
| 127 """Retrieves a file from the repository and caches it in GetCacheDir() for |
| 128 max_age seconds. |
| 129 |
| 130 use_root: If False, look up the arborescence for the first match, otherwise go |
| 131 directory to the root repository. |
| 132 |
| 133 Note: The cache will be inconsistent if the same file is retrieved with both |
| 134 use_root=True and use_root=False on the same file. Don't be stupid. |
| 135 """ |
| 136 global FILES_CACHE |
| 137 if filename not in FILES_CACHE: |
| 138 # Don't try to look up twice. |
| 139 FILES_CACHE[filename] = None |
| 140 # First we check if we have a cached version. |
| 141 cached_file = os.path.join(GetCacheDir(), filename) |
| 142 if (not os.path.exists(cached_file) or |
| 143 os.stat(cached_file).st_mtime > max_age): |
| 144 dir_info = gclient.CaptureSVNInfo(".") |
| 145 repo_root = dir_info["Repository Root"] |
| 146 if use_root: |
| 147 url_path = repo_root |
| 148 else: |
| 149 url_path = dir_info["URL"] |
| 150 content = "" |
| 151 while True: |
| 152 # Look for the codereview.settings file at the current level. |
| 153 svn_path = url_path + "/" + filename |
| 154 content, rc = RunShellWithReturnCode(["svn", "cat", svn_path]) |
| 155 if not rc: |
| 156 # Exit the loop if the file was found. Override content. |
| 157 break |
| 158 # Make sure to mark settings as empty if not found. |
| 159 content = "" |
| 160 if url_path == repo_root: |
| 161 # Reached the root. Abandoning search. |
| 162 break |
| 163 # Go up one level to try again. |
| 164 url_path = os.path.dirname(url_path) |
| 165 # Write a cached version even if there isn't a file, so we don't try to |
| 166 # fetch it each time. |
| 167 WriteFile(cached_file, content) |
| 168 else: |
| 169 content = ReadFile(cached_settings_file) |
| 170 # Keep the content cached in memory. |
| 171 FILES_CACHE[filename] = content |
| 172 return FILES_CACHE[filename] |
| 117 | 173 |
| 118 | 174 |
| 119 def GetCodeReviewSetting(key): | 175 def GetCodeReviewSetting(key): |
| 120 """Returns a value for the given key for this repository.""" | 176 """Returns a value for the given key for this repository.""" |
| 121 global read_gcl_info | 177 # Use '__just_initialized' as a flag to determine if the settings were |
| 122 if not read_gcl_info: | 178 # already initialized. |
| 123 read_gcl_info = True | 179 global CODEREVIEW_SETTINGS |
| 124 # First we check if we have a cached version. | 180 if '__just_initialized' not in CODEREVIEW_SETTINGS: |
| 125 cached_settings_file = os.path.join(GetInfoDir(), CODEREVIEW_SETTINGS_FILE) | 181 for line in GetCachedFile(CODEREVIEW_SETTINGS_FILE).splitlines(): |
| 126 if (not os.path.exists(cached_settings_file) or | |
| 127 os.stat(cached_settings_file).st_mtime > 60*60*24*3): | |
| 128 dir_info = gclient.CaptureSVNInfo(".") | |
| 129 repo_root = dir_info["Repository Root"] | |
| 130 url_path = dir_info["URL"] | |
| 131 settings = "" | |
| 132 while True: | |
| 133 # Look for the codereview.settings file at the current level. | |
| 134 svn_path = url_path + "/" + CODEREVIEW_SETTINGS_FILE | |
| 135 settings, rc = RunShellWithReturnCode(["svn", "cat", svn_path]) | |
| 136 if not rc: | |
| 137 # Exit the loop if the file was found. | |
| 138 break | |
| 139 # Make sure to mark settings as empty if not found. | |
| 140 settings = "" | |
| 141 if url_path == repo_root: | |
| 142 # Reached the root. Abandoning search. | |
| 143 break; | |
| 144 # Go up one level to try again. | |
| 145 url_path = os.path.dirname(url_path) | |
| 146 | |
| 147 # Write a cached version even if there isn't a file, so we don't try to | |
| 148 # fetch it each time. | |
| 149 WriteFile(cached_settings_file, settings) | |
| 150 | |
| 151 output = ReadFile(cached_settings_file) | |
| 152 for line in output.splitlines(): | |
| 153 if not line or line.startswith("#"): | 182 if not line or line.startswith("#"): |
| 154 continue | 183 continue |
| 155 k, v = line.split(": ", 1) | 184 k, v = line.split(": ", 1) |
| 156 CODEREVIEW_SETTINGS[k] = v | 185 CODEREVIEW_SETTINGS[k] = v |
| 186 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None) |
| 157 return CODEREVIEW_SETTINGS.get(key, "") | 187 return CODEREVIEW_SETTINGS.get(key, "") |
| 158 | 188 |
| 159 | 189 |
| 160 def IsTreeOpen(): | 190 def IsTreeOpen(): |
| 161 """Fetches the tree status and returns either True or False.""" | 191 """Fetches the tree status and returns either True or False.""" |
| 162 url = GetCodeReviewSetting('STATUS') | 192 url = GetCodeReviewSetting('STATUS') |
| 163 status = "" | 193 status = "" |
| 164 if url: | 194 if url: |
| 165 status = urllib2.urlopen(url).read() | 195 status = urllib2.urlopen(url).read() |
| 166 return status.find('0') == -1 | 196 return status.find('0') == -1 |
| (...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 366 # . | 396 # . |
| 367 # filepathn\n | 397 # filepathn\n |
| 368 # SEPARATOR\n | 398 # SEPARATOR\n |
| 369 # description | 399 # description |
| 370 | 400 |
| 371 | 401 |
| 372 def GetChangelistInfoFile(changename): | 402 def GetChangelistInfoFile(changename): |
| 373 """Returns the file that stores information about a changelist.""" | 403 """Returns the file that stores information about a changelist.""" |
| 374 if not changename or re.search(r'[^\w-]', changename): | 404 if not changename or re.search(r'[^\w-]', changename): |
| 375 ErrorExit("Invalid changelist name: " + changename) | 405 ErrorExit("Invalid changelist name: " + changename) |
| 376 return os.path.join(GetInfoDir(), changename) | 406 return os.path.join(GetChangesDir(), changename) |
| 377 | 407 |
| 378 | 408 |
| 379 def LoadChangelistInfoForMultiple(changenames, fail_on_not_found=True, | 409 def LoadChangelistInfoForMultiple(changenames, fail_on_not_found=True, |
| 380 update_status=False): | 410 update_status=False): |
| 381 """Loads many changes and merge their files list into one pseudo change. | 411 """Loads many changes and merge their files list into one pseudo change. |
| 382 | 412 |
| 383 This is mainly usefull to concatenate many changes into one for a 'gcl try'. | 413 This is mainly usefull to concatenate many changes into one for a 'gcl try'. |
| 384 """ | 414 """ |
| 385 changes = changenames.split(',') | 415 changes = changenames.split(',') |
| 386 aggregate_change_info = ChangeInfo(name=changenames) | 416 aggregate_change_info = ChangeInfo(name=changenames) |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 435 save = True | 465 save = True |
| 436 files[files.index(file)] = (status, file[1]) | 466 files[files.index(file)] = (status, file[1]) |
| 437 change_info = ChangeInfo(changename, issue, description, files) | 467 change_info = ChangeInfo(changename, issue, description, files) |
| 438 if save: | 468 if save: |
| 439 change_info.Save() | 469 change_info.Save() |
| 440 return change_info | 470 return change_info |
| 441 | 471 |
| 442 | 472 |
| 443 def GetCLs(): | 473 def GetCLs(): |
| 444 """Returns a list of all the changelists in this repository.""" | 474 """Returns a list of all the changelists in this repository.""" |
| 445 cls = os.listdir(GetInfoDir()) | 475 cls = os.listdir(GetChangesDir()) |
| 446 if CODEREVIEW_SETTINGS_FILE in cls: | 476 if CODEREVIEW_SETTINGS_FILE in cls: |
| 447 cls.remove(CODEREVIEW_SETTINGS_FILE) | 477 cls.remove(CODEREVIEW_SETTINGS_FILE) |
| 448 return cls | 478 return cls |
| 449 | 479 |
| 450 | 480 |
| 451 def GenerateChangeName(): | 481 def GenerateChangeName(): |
| 452 """Generate a random changelist name.""" | 482 """Generate a random changelist name.""" |
| 453 random.seed() | 483 random.seed() |
| 454 current_cl_names = GetCLs() | 484 current_cl_names = GetCLs() |
| 455 while True: | 485 while True: |
| (...skipping 557 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1013 | 1043 |
| 1014 | 1044 |
| 1015 def main(argv=None): | 1045 def main(argv=None): |
| 1016 if argv is None: | 1046 if argv is None: |
| 1017 argv = sys.argv | 1047 argv = sys.argv |
| 1018 | 1048 |
| 1019 if len(argv) == 1: | 1049 if len(argv) == 1: |
| 1020 Help() | 1050 Help() |
| 1021 return 0; | 1051 return 0; |
| 1022 | 1052 |
| 1023 # Create the directory where we store information about changelists if it | 1053 # Create the directories where we store information about changelists if it |
| 1024 # doesn't exist. | 1054 # doesn't exist. |
| 1025 if not os.path.exists(GetInfoDir()): | 1055 if not os.path.exists(GetInfoDir()): |
| 1026 os.mkdir(GetInfoDir()) | 1056 os.mkdir(GetInfoDir()) |
| 1057 if not os.path.exists(GetChangesDir()): |
| 1058 os.mkdir(GetChangesDir()) |
| 1059 # For smooth upgrade support, move the files in GetInfoDir() to |
| 1060 # GetChangesDir(). |
| 1061 # TODO(maruel): Remove this code in August 2009. |
| 1062 for file in os.listdir(unicode(GetInfoDir())): |
| 1063 file_path = os.path.join(unicode(GetInfoDir()), file) |
| 1064 if os.path.isfile(file_path) and file != CODEREVIEW_SETTINGS_FILE: |
| 1065 shutil.move(file_path, GetChangesDir()) |
| 1066 if not os.path.exists(GetCacheDir()): |
| 1067 os.mkdir(GetCacheDir()) |
| 1027 | 1068 |
| 1028 # Commands that don't require an argument. | 1069 # Commands that don't require an argument. |
| 1029 command = argv[1] | 1070 command = argv[1] |
| 1030 if command == "opened": | 1071 if command == "opened": |
| 1031 Opened() | 1072 Opened() |
| 1032 return 0 | 1073 return 0 |
| 1033 if command == "status": | 1074 if command == "status": |
| 1034 Opened() | 1075 Opened() |
| 1035 print "\n--- Not in any changelist:" | 1076 print "\n--- Not in any changelist:" |
| 1036 UnknownFiles([]) | 1077 UnknownFiles([]) |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1097 # the files. This allows commands such as 'gcl diff xxx' to work. | 1138 # the files. This allows commands such as 'gcl diff xxx' to work. |
| 1098 args =["svn", command] | 1139 args =["svn", command] |
| 1099 root = GetRepositoryRoot() | 1140 root = GetRepositoryRoot() |
| 1100 args.extend([os.path.join(root, x) for x in change_info.FileList()]) | 1141 args.extend([os.path.join(root, x) for x in change_info.FileList()]) |
| 1101 RunShell(args, True) | 1142 RunShell(args, True) |
| 1102 return 0 | 1143 return 0 |
| 1103 | 1144 |
| 1104 | 1145 |
| 1105 if __name__ == "__main__": | 1146 if __name__ == "__main__": |
| 1106 sys.exit(main()) | 1147 sys.exit(main()) |
| OLD | NEW |