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 |