blob: 456d1d7dd34266f67586de626a8858435ff55eb9 [file] [log] [blame]
[email protected]fb2b8eb2009-04-23 21:03:421#!/usr/bin/python
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
4# found in the LICENSE file.
5#
6# Wrapper script around Rietveld's upload.py that groups files into
7# changelists.
8
9import getpass
10import os
11import random
12import re
[email protected]374d65e2009-05-21 14:00:5213import shutil
[email protected]fb2b8eb2009-04-23 21:03:4214import string
15import subprocess
16import sys
17import tempfile
18import upload
19import urllib2
[email protected]207fdf32009-04-28 19:57:0120import xml.dom.minidom
[email protected]fb2b8eb2009-04-23 21:03:4221
[email protected]46a94102009-05-12 20:32:4322# gcl now depends on gclient.
23import gclient
[email protected]c1675e22009-04-27 20:30:4824
[email protected]374d65e2009-05-21 14:00:5225__version__ = '1.1'
[email protected]c1675e22009-04-27 20:30:4826
27
[email protected]fb2b8eb2009-04-23 21:03:4228CODEREVIEW_SETTINGS = {
29 # Default values.
30 "CODE_REVIEW_SERVER": "codereview.chromium.org",
31 "CC_LIST": "[email protected]",
32 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
33}
34
[email protected]fb2b8eb2009-04-23 21:03:4235# globals that store the root of the current repository and the directory where
36# we store information about changelists.
37repository_root = ""
[email protected]fb2b8eb2009-04-23 21:03:4238
39# Filename where we store repository specific information for gcl.
40CODEREVIEW_SETTINGS_FILE = "codereview.settings"
41
42# Warning message when the change appears to be missing tests.
43MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
44
[email protected]9b613272009-04-24 01:28:2845# Caches whether we read the codereview.settings file yet or not.
46read_gcl_info = False
[email protected]fb2b8eb2009-04-23 21:03:4247
48
[email protected]207fdf32009-04-28 19:57:0149### SVN Functions
50
[email protected]fb2b8eb2009-04-23 21:03:4251def IsSVNMoved(filename):
52 """Determine if a file has been added through svn mv"""
[email protected]46a94102009-05-12 20:32:4353 info = gclient.CaptureSVNInfo(filename)
[email protected]fb2b8eb2009-04-23 21:03:4254 return (info.get('Copied From URL') and
55 info.get('Copied From Rev') and
56 info.get('Schedule') == 'add')
57
58
[email protected]fb2b8eb2009-04-23 21:03:4259def GetSVNFileProperty(file, property_name):
60 """Returns the value of an SVN property for the given file.
61
62 Args:
63 file: The file to check
64 property_name: The name of the SVN property, e.g. "svn:mime-type"
65
66 Returns:
67 The value of the property, which will be the empty string if the property
68 is not set on the file. If the file is not under version control, the
69 empty string is also returned.
70 """
71 output = RunShell(["svn", "propget", property_name, file])
72 if (output.startswith("svn: ") and
73 output.endswith("is not under version control")):
74 return ""
75 else:
76 return output
77
78
[email protected]207fdf32009-04-28 19:57:0179def UnknownFiles(extra_args):
80 """Runs svn status and prints unknown files.
81
82 Any args in |extra_args| are passed to the tool to support giving alternate
83 code locations.
84 """
[email protected]4810a962009-05-12 21:03:3485 return [item[1] for item in gclient.CaptureSVNStatus(extra_args)
86 if item[0][0] == '?']
[email protected]207fdf32009-04-28 19:57:0187
88
[email protected]fb2b8eb2009-04-23 21:03:4289def GetRepositoryRoot():
90 """Returns the top level directory of the current repository.
91
92 The directory is returned as an absolute path.
93 """
94 global repository_root
95 if not repository_root:
[email protected]46a94102009-05-12 20:32:4396 infos = gclient.CaptureSVNInfo(os.getcwd(), print_error=False)
97 cur_dir_repo_root = infos.get("Repository Root")
[email protected]fb2b8eb2009-04-23 21:03:4298 if not cur_dir_repo_root:
99 raise Exception("gcl run outside of repository")
100
101 repository_root = os.getcwd()
102 while True:
103 parent = os.path.dirname(repository_root)
[email protected]8c3ccf32009-05-20 18:28:37104 if (gclient.CaptureSVNInfo(parent, print_error=False).get(
105 "Repository Root") != cur_dir_repo_root):
[email protected]fb2b8eb2009-04-23 21:03:42106 break
107 repository_root = parent
108 return repository_root
109
110
111def GetInfoDir():
112 """Returns the directory where gcl info files are stored."""
[email protected]374d65e2009-05-21 14:00:52113 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
114
115
116def GetChangesDir():
117 """Returns the directory where gcl change files are stored."""
118 return os.path.join(GetInfoDir(), 'changes')
[email protected]fb2b8eb2009-04-23 21:03:42119
120
[email protected]fb2b8eb2009-04-23 21:03:42121def GetCodeReviewSetting(key):
122 """Returns a value for the given key for this repository."""
[email protected]9b613272009-04-24 01:28:28123 global read_gcl_info
124 if not read_gcl_info:
125 read_gcl_info = True
126 # First we check if we have a cached version.
127 cached_settings_file = os.path.join(GetInfoDir(), CODEREVIEW_SETTINGS_FILE)
128 if (not os.path.exists(cached_settings_file) or
129 os.stat(cached_settings_file).st_mtime > 60*60*24*3):
[email protected]46a94102009-05-12 20:32:43130 dir_info = gclient.CaptureSVNInfo(".")
[email protected]9b613272009-04-24 01:28:28131 repo_root = dir_info["Repository Root"]
132 url_path = dir_info["URL"]
133 settings = ""
134 while True:
135 # Look for the codereview.settings file at the current level.
136 svn_path = url_path + "/" + CODEREVIEW_SETTINGS_FILE
137 settings, rc = RunShellWithReturnCode(["svn", "cat", svn_path])
138 if not rc:
139 # Exit the loop if the file was found.
140 break
141 # Make sure to mark settings as empty if not found.
142 settings = ""
143 if url_path == repo_root:
144 # Reached the root. Abandoning search.
145 break;
146 # Go up one level to try again.
147 url_path = os.path.dirname(url_path)
148
149 # Write a cached version even if there isn't a file, so we don't try to
150 # fetch it each time.
151 WriteFile(cached_settings_file, settings)
152
153 output = ReadFile(cached_settings_file)
154 for line in output.splitlines():
[email protected]fb2b8eb2009-04-23 21:03:42155 if not line or line.startswith("#"):
156 continue
157 k, v = line.split(": ", 1)
158 CODEREVIEW_SETTINGS[k] = v
[email protected]fb2b8eb2009-04-23 21:03:42159 return CODEREVIEW_SETTINGS.get(key, "")
160
161
162def IsTreeOpen():
163 """Fetches the tree status and returns either True or False."""
164 url = GetCodeReviewSetting('STATUS')
165 status = ""
166 if url:
167 status = urllib2.urlopen(url).read()
168 return status.find('0') == -1
169
170
171def Warn(msg):
172 ErrorExit(msg, exit=False)
173
174
175def ErrorExit(msg, exit=True):
176 """Print an error message to stderr and optionally exit."""
177 print >>sys.stderr, msg
178 if exit:
179 sys.exit(1)
180
181
182def RunShellWithReturnCode(command, print_output=False):
183 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02184 # Use a shell for subcommands on Windows to get a PATH search, and because svn
185 # may be a batch file.
186 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42187 p = subprocess.Popen(command, stdout=subprocess.PIPE,
188 stderr=subprocess.STDOUT, shell=use_shell,
189 universal_newlines=True)
190 if print_output:
191 output_array = []
192 while True:
193 line = p.stdout.readline()
194 if not line:
195 break
196 if print_output:
197 print line.strip('\n')
198 output_array.append(line)
199 output = "".join(output_array)
200 else:
201 output = p.stdout.read()
202 p.wait()
203 p.stdout.close()
204 return output, p.returncode
205
206
207def RunShell(command, print_output=False):
208 """Executes a command and returns the output."""
209 return RunShellWithReturnCode(command, print_output)[0]
210
211
[email protected]c1675e22009-04-27 20:30:48212def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42213 """Returns the contents of a file."""
[email protected]c1675e22009-04-27 20:30:48214 file = open(filename, flags)
[email protected]fb2b8eb2009-04-23 21:03:42215 result = file.read()
216 file.close()
217 return result
218
219
220def WriteFile(filename, contents):
221 """Overwrites the file with the given contents."""
222 file = open(filename, 'w')
223 file.write(contents)
224 file.close()
225
226
[email protected]be0d1ca2009-05-12 19:23:02227class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42228 """Holds information about a changelist.
229
230 issue: the Rietveld issue number, of "" if it hasn't been uploaded yet.
231 description: the description.
232 files: a list of 2 tuple containing (status, filename) of changed files,
233 with paths being relative to the top repository directory.
234 """
[email protected]be0d1ca2009-05-12 19:23:02235 def __init__(self, name="", issue="", description="", files=None):
[email protected]fb2b8eb2009-04-23 21:03:42236 self.name = name
237 self.issue = issue
238 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02239 if files is None:
240 files = []
[email protected]fb2b8eb2009-04-23 21:03:42241 self.files = files
242 self.patch = None
243
244 def FileList(self):
245 """Returns a list of files."""
246 return [file[1] for file in self.files]
247
248 def _NonDeletedFileList(self):
249 """Returns a list of files in this change, not including deleted files."""
250 return [file[1] for file in self.files if not file[0].startswith("D")]
251
252 def _AddedFileList(self):
253 """Returns a list of files added in this change."""
254 return [file[1] for file in self.files if file[0].startswith("A")]
255
256 def Save(self):
257 """Writes the changelist information to disk."""
258 data = SEPARATOR.join([self.issue,
259 "\n".join([f[0] + f[1] for f in self.files]),
260 self.description])
261 WriteFile(GetChangelistInfoFile(self.name), data)
262
263 def Delete(self):
264 """Removes the changelist information from disk."""
265 os.remove(GetChangelistInfoFile(self.name))
266
267 def CloseIssue(self):
268 """Closes the Rietveld issue for this changelist."""
269 data = [("description", self.description),]
270 ctype, body = upload.EncodeMultipartFormData(data, [])
271 SendToRietveld("/" + self.issue + "/close", body, ctype)
272
273 def UpdateRietveldDescription(self):
274 """Sets the description for an issue on Rietveld."""
275 data = [("description", self.description),]
276 ctype, body = upload.EncodeMultipartFormData(data, [])
277 SendToRietveld("/" + self.issue + "/description", body, ctype)
278
279 def MissingTests(self):
280 """Returns True if the change looks like it needs unit tests but has none.
281
282 A change needs unit tests if it contains any new source files or methods.
283 """
284 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
285 # Ignore third_party entirely.
286 files = [file for file in self._NonDeletedFileList()
287 if file.find("third_party") == -1]
288 added_files = [file for file in self._AddedFileList()
289 if file.find("third_party") == -1]
290
291 # If the change is entirely in third_party, we're done.
292 if len(files) == 0:
293 return False
294
295 # Any new or modified test files?
296 # A test file's name ends with "test.*" or "tests.*".
297 test_files = [test for test in files
298 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
299 if len(test_files) > 0:
300 return False
301
302 # Any new source files?
303 source_files = [file for file in added_files
304 if os.path.splitext(file)[1] in SOURCE_SUFFIXES]
305 if len(source_files) > 0:
306 return True
307
308 # Do the long test, checking the files for new methods.
309 return self._HasNewMethod()
310
311 def _HasNewMethod(self):
312 """Returns True if the changeset contains any new functions, or if a
313 function signature has been changed.
314
315 A function is identified by starting flush left, containing a "(" before
316 the next flush-left line, and either ending with "{" before the next
317 flush-left line or being followed by an unindented "{".
318
319 Currently this returns True for new methods, new static functions, and
320 methods or functions whose signatures have been changed.
321
322 Inline methods added to header files won't be detected by this. That's
323 acceptable for purposes of determining if a unit test is needed, since
324 inline methods should be trivial.
325 """
326 # To check for methods added to source or header files, we need the diffs.
327 # We'll generate them all, since there aren't likely to be many files
328 # apart from source and headers; besides, we'll want them all if we're
329 # uploading anyway.
330 if self.patch is None:
331 self.patch = GenerateDiff(self.FileList())
332
333 definition = ""
334 for line in self.patch.splitlines():
335 if not line.startswith("+"):
336 continue
337 line = line.strip("+").rstrip(" \t")
338 # Skip empty lines, comments, and preprocessor directives.
339 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
340 if line == "" or line.startswith("/") or line.startswith("#"):
341 continue
342
343 # A possible definition ending with "{" is complete, so check it.
344 if definition.endswith("{"):
345 if definition.find("(") != -1:
346 return True
347 definition = ""
348
349 # A { or an indented line, when we're in a definition, continues it.
350 if (definition != "" and
351 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
352 definition += line
353
354 # A flush-left line starts a new possible function definition.
355 elif not line.startswith(" ") and not line.startswith("\t"):
356 definition = line
357
358 return False
359
360
361SEPARATOR = "\n-----\n"
362# The info files have the following format:
363# issue_id\n
364# SEPARATOR\n
365# filepath1\n
366# filepath2\n
367# .
368# .
369# filepathn\n
370# SEPARATOR\n
371# description
372
373
374def GetChangelistInfoFile(changename):
375 """Returns the file that stores information about a changelist."""
376 if not changename or re.search(r'[^\w-]', changename):
377 ErrorExit("Invalid changelist name: " + changename)
[email protected]374d65e2009-05-21 14:00:52378 return os.path.join(GetChangesDir(), changename)
[email protected]fb2b8eb2009-04-23 21:03:42379
380
381def LoadChangelistInfoForMultiple(changenames, fail_on_not_found=True,
382 update_status=False):
383 """Loads many changes and merge their files list into one pseudo change.
384
385 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
386 """
387 changes = changenames.split(',')
388 aggregate_change_info = ChangeInfo(name=changenames)
389 for change in changes:
390 aggregate_change_info.files += LoadChangelistInfo(change,
391 fail_on_not_found,
392 update_status).files
393 return aggregate_change_info
394
395
396def LoadChangelistInfo(changename, fail_on_not_found=True,
397 update_status=False):
398 """Gets information about a changelist.
399
400 Args:
401 fail_on_not_found: if True, this function will quit the program if the
402 changelist doesn't exist.
403 update_status: if True, the svn status will be updated for all the files
404 and unchanged files will be removed.
405
406 Returns: a ChangeInfo object.
407 """
408 info_file = GetChangelistInfoFile(changename)
409 if not os.path.exists(info_file):
410 if fail_on_not_found:
411 ErrorExit("Changelist " + changename + " not found.")
412 return ChangeInfo(changename)
413 data = ReadFile(info_file)
414 split_data = data.split(SEPARATOR, 2)
415 if len(split_data) != 3:
416 os.remove(info_file)
417 ErrorExit("Changelist file %s was corrupt and deleted" % info_file)
418 issue = split_data[0]
419 files = []
420 for line in split_data[1].splitlines():
421 status = line[:7]
422 file = line[7:]
423 files.append((status, file))
424 description = split_data[2]
425 save = False
426 if update_status:
427 for file in files:
428 filename = os.path.join(GetRepositoryRoot(), file[1])
[email protected]4810a962009-05-12 21:03:34429 status_result = gclient.CaptureSVNStatus(filename)
[email protected]207fdf32009-04-28 19:57:01430 if not status_result or not status_result[0][0]:
431 # File has been reverted.
[email protected]fb2b8eb2009-04-23 21:03:42432 save = True
433 files.remove(file)
[email protected]8a62d5b2009-05-11 15:59:01434 continue
435 status = status_result[0][0]
436 if status != file[0]:
[email protected]fb2b8eb2009-04-23 21:03:42437 save = True
438 files[files.index(file)] = (status, file[1])
439 change_info = ChangeInfo(changename, issue, description, files)
440 if save:
441 change_info.Save()
442 return change_info
443
444
445def GetCLs():
446 """Returns a list of all the changelists in this repository."""
[email protected]374d65e2009-05-21 14:00:52447 cls = os.listdir(GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:42448 if CODEREVIEW_SETTINGS_FILE in cls:
449 cls.remove(CODEREVIEW_SETTINGS_FILE)
450 return cls
451
452
453def GenerateChangeName():
454 """Generate a random changelist name."""
455 random.seed()
456 current_cl_names = GetCLs()
457 while True:
458 cl_name = (random.choice(string.ascii_lowercase) +
459 random.choice(string.digits) +
460 random.choice(string.ascii_lowercase) +
461 random.choice(string.digits))
462 if cl_name not in current_cl_names:
463 return cl_name
464
465
466def GetModifiedFiles():
467 """Returns a set that maps from changelist name to (status,filename) tuples.
468
469 Files not in a changelist have an empty changelist name. Filenames are in
470 relation to the top level directory of the current repository. Note that
471 only the current directory and subdirectories are scanned, in order to
472 improve performance while still being flexible.
473 """
474 files = {}
475
476 # Since the files are normalized to the root folder of the repositary, figure
477 # out what we need to add to the paths.
478 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
479
480 # Get a list of all files in changelists.
481 files_in_cl = {}
482 for cl in GetCLs():
483 change_info = LoadChangelistInfo(cl)
484 for status, filename in change_info.files:
485 files_in_cl[filename] = change_info.name
486
487 # Get all the modified files.
[email protected]4810a962009-05-12 21:03:34488 status_result = gclient.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01489 for line in status_result:
490 status = line[0]
491 filename = line[1]
492 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42493 continue
[email protected]fb2b8eb2009-04-23 21:03:42494 if dir_prefix:
495 filename = os.path.join(dir_prefix, filename)
496 change_list_name = ""
497 if filename in files_in_cl:
498 change_list_name = files_in_cl[filename]
499 files.setdefault(change_list_name, []).append((status, filename))
500
501 return files
502
503
504def GetFilesNotInCL():
505 """Returns a list of tuples (status,filename) that aren't in any changelists.
506
507 See docstring of GetModifiedFiles for information about path of files and
508 which directories are scanned.
509 """
510 modified_files = GetModifiedFiles()
511 if "" not in modified_files:
512 return []
513 return modified_files[""]
514
515
516def SendToRietveld(request_path, payload=None,
517 content_type="application/octet-stream", timeout=None):
518 """Send a POST/GET to Rietveld. Returns the response body."""
519 def GetUserCredentials():
520 """Prompts the user for a username and password."""
521 email = upload.GetEmail()
522 password = getpass.getpass("Password for %s: " % email)
523 return email, password
524
525 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
526 rpc_server = upload.HttpRpcServer(server,
527 GetUserCredentials,
528 host_override=server,
529 save_cookies=True)
530 try:
531 return rpc_server.Send(request_path, payload, content_type, timeout)
532 except urllib2.URLError, e:
533 if timeout is None:
534 ErrorExit("Error accessing url %s" % request_path)
535 else:
536 return None
537
538
539def GetIssueDescription(issue):
540 """Returns the issue description from Rietveld."""
541 return SendToRietveld("/" + issue + "/description")
542
543
[email protected]fb2b8eb2009-04-23 21:03:42544def Opened():
545 """Prints a list of modified files in the current directory down."""
546 files = GetModifiedFiles()
547 cl_keys = files.keys()
548 cl_keys.sort()
549 for cl_name in cl_keys:
550 if cl_name:
551 note = ""
552 if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]):
553 note = " (Note: this changelist contains files outside this directory)"
554 print "\n--- Changelist " + cl_name + note + ":"
555 for file in files[cl_name]:
556 print "".join(file)
557
558
559def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53560 if argv:
561 if argv[0] == 'try':
562 TryChange(None, ['--help'], swallow_exception=False)
563 return
564 if argv[0] == 'upload':
565 upload.RealMain(['upload.py', '--help'])
566 return
[email protected]fb2b8eb2009-04-23 21:03:42567
568 print (
569"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48570version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42571
572Basic commands:
573-----------------------------------------
574 gcl change change_name
575 Add/remove files to a changelist. Only scans the current directory and
576 subdirectories.
577
578 gcl upload change_name [-r [email protected],[email protected],...]
579 [--send_mail] [--no_try] [--no_presubmit]
580 Uploads the changelist to the server for review.
581
[email protected]9b613272009-04-24 01:28:28582 gcl commit change_name [--force]
[email protected]fb2b8eb2009-04-23 21:03:42583 Commits the changelist to the repository.
584
585 gcl lint change_name
586 Check all the files in the changelist for possible style violations.
587
588Advanced commands:
589-----------------------------------------
590 gcl delete change_name
591 Deletes a changelist.
592
593 gcl diff change_name
594 Diffs all files in the changelist.
595
596 gcl presubmit change_name
597 Runs presubmit checks without uploading the changelist.
598
599 gcl diff
600 Diffs all files in the current directory and subdirectories that aren't in
601 a changelist.
602
603 gcl changes
604 Lists all the the changelists and the files in them.
605
606 gcl nothave [optional directory]
607 Lists files unknown to Subversion.
608
609 gcl opened
610 Lists modified files in the current directory and subdirectories.
611
612 gcl settings
613 Print the code review settings for this directory.
614
615 gcl status
616 Lists modified and unknown files in the current directory and
617 subdirectories.
618
619 gcl try change_name
620 Sends the change to the tryserver so a trybot can do a test run on your
621 code. To send multiple changes as one path, use a comma-separated list
622 of changenames.
623 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53624
625 gcl help [command]
626 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42627""")
628
629def GetEditor():
630 editor = os.environ.get("SVN_EDITOR")
631 if not editor:
632 editor = os.environ.get("EDITOR")
633
634 if not editor:
635 if sys.platform.startswith("win"):
636 editor = "notepad"
637 else:
638 editor = "vi"
639
640 return editor
641
642
643def GenerateDiff(files, root=None):
644 """Returns a string containing the diff for the given file list.
645
646 The files in the list should either be absolute paths or relative to the
647 given root. If no root directory is provided, the repository root will be
648 used.
649 """
650 previous_cwd = os.getcwd()
651 if root is None:
652 os.chdir(GetRepositoryRoot())
653 else:
654 os.chdir(root)
655
656 diff = []
657 for file in files:
658 # Use svn info output instead of os.path.isdir because the latter fails
659 # when the file is deleted.
[email protected]46a94102009-05-12 20:32:43660 if gclient.CaptureSVNInfo(file).get("Node Kind") in ("dir", "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42661 continue
662 # If the user specified a custom diff command in their svn config file,
663 # then it'll be used when we do svn diff, which we don't want to happen
664 # since we want the unified diff. Using --diff-cmd=diff doesn't always
665 # work, since they can have another diff executable in their path that
666 # gives different line endings. So we use a bogus temp directory as the
667 # config directory, which gets around these problems.
668 if sys.platform.startswith("win"):
669 parent_dir = tempfile.gettempdir()
670 else:
671 parent_dir = sys.path[0] # tempdir is not secure.
672 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
673 if not os.path.exists(bogus_dir):
674 os.mkdir(bogus_dir)
675 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
676 if output:
677 diff.append(output)
[email protected]c3150202009-05-13 14:31:01678 elif IsSVNMoved(file):
679 # svn diff on a mv/cp'd file outputs nothing.
680 # We put in an empty Index entry so upload.py knows about them.
[email protected]fb2b8eb2009-04-23 21:03:42681 diff.append("\nIndex: %s\n" % file)
[email protected]c3150202009-05-13 14:31:01682 else:
683 # The file is not modified anymore. It should be removed from the set.
684 pass
[email protected]fb2b8eb2009-04-23 21:03:42685 os.chdir(previous_cwd)
686 return "".join(diff)
687
688
689def UploadCL(change_info, args):
690 if not change_info.FileList():
691 print "Nothing to upload, changelist is empty."
692 return
693
694 if not "--no_presubmit" in args:
695 if not DoPresubmitChecks(change_info, committing=False):
696 return
697 else:
698 args.remove("--no_presubmit")
699
700 no_try = "--no_try" in args
701 if no_try:
702 args.remove("--no_try")
703 else:
704 # Support --no-try as --no_try
705 no_try = "--no-try" in args
706 if no_try:
707 args.remove("--no-try")
708
709 # Map --send-mail to --send_mail
710 if "--send-mail" in args:
711 args.remove("--send-mail")
712 args.append("--send_mail")
713
714 # Supports --clobber for the try server.
715 clobber = False
716 if "--clobber" in args:
717 args.remove("--clobber")
718 clobber = True
719
[email protected]003c2692009-05-20 13:08:08720 # Disable try when the server is overridden.
721 server_1 = re.compile(r"^-s\b.*")
722 server_2 = re.compile(r"^--server\b.*")
723 for arg in args:
724 if server_1.match(arg) or server_2.match(arg):
725 no_try = True
726 break
[email protected]fb2b8eb2009-04-23 21:03:42727
728 upload_arg = ["upload.py", "-y"]
729 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
730 upload_arg.extend(args)
731
732 desc_file = ""
733 if change_info.issue: # Uploading a new patchset.
734 found_message = False
735 for arg in args:
736 if arg.startswith("--message") or arg.startswith("-m"):
737 found_message = True
738 break
739
740 if not found_message:
741 upload_arg.append("--message=''")
742
743 upload_arg.append("--issue=" + change_info.issue)
744 else: # First time we upload.
745 handle, desc_file = tempfile.mkstemp(text=True)
746 os.write(handle, change_info.description)
747 os.close(handle)
748
749 cc_list = GetCodeReviewSetting("CC_LIST")
750 if cc_list:
751 upload_arg.append("--cc=" + cc_list)
752 upload_arg.append("--description_file=" + desc_file + "")
753 if change_info.description:
754 subject = change_info.description[:77]
755 if subject.find("\r\n") != -1:
756 subject = subject[:subject.find("\r\n")]
757 if subject.find("\n") != -1:
758 subject = subject[:subject.find("\n")]
759 if len(change_info.description) > 77:
760 subject = subject + "..."
761 upload_arg.append("--message=" + subject)
762
763 # Change the current working directory before calling upload.py so that it
764 # shows the correct base.
765 previous_cwd = os.getcwd()
766 os.chdir(GetRepositoryRoot())
767
768 # If we have a lot of files with long paths, then we won't be able to fit
769 # the command to "svn diff". Instead, we generate the diff manually for
770 # each file and concatenate them before passing it to upload.py.
771 if change_info.patch is None:
772 change_info.patch = GenerateDiff(change_info.FileList())
773 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
774 if issue and issue != change_info.issue:
775 change_info.issue = issue
776 change_info.Save()
777
778 if desc_file:
779 os.remove(desc_file)
780
781 # Do background work on Rietveld to lint the file so that the results are
782 # ready when the issue is viewed.
783 SendToRietveld("/lint/issue%s_%s" % (issue, patchset), timeout=0.5)
784
785 # Once uploaded to Rietveld, send it to the try server.
786 if not no_try:
787 try_on_upload = GetCodeReviewSetting('TRY_ON_UPLOAD')
788 if try_on_upload and try_on_upload.lower() == 'true':
789 # Use the local diff.
790 args = [
791 "--issue", change_info.issue,
792 "--patchset", patchset,
793 ]
794 if clobber:
795 args.append('--clobber')
796 TryChange(change_info, args, swallow_exception=True)
797
798 os.chdir(previous_cwd)
799
800
801def PresubmitCL(change_info):
802 """Reports what presubmit checks on the change would report."""
803 if not change_info.FileList():
804 print "Nothing to presubmit check, changelist is empty."
805 return
806
807 print "*** Presubmit checks for UPLOAD would report: ***"
808 DoPresubmitChecks(change_info, committing=False)
809
810 print "\n\n*** Presubmit checks for COMMIT would report: ***"
811 DoPresubmitChecks(change_info, committing=True)
812
813
814def TryChange(change_info, args, swallow_exception):
815 """Create a diff file of change_info and send it to the try server."""
816 try:
817 import trychange
818 except ImportError:
819 if swallow_exception:
820 return
821 ErrorExit("You need to install trychange.py to use the try server.")
822
823 if change_info:
824 trychange_args = ['--name', change_info.name]
825 trychange_args.extend(args)
826 trychange.TryChange(trychange_args,
827 file_list=change_info.FileList(),
828 swallow_exception=swallow_exception,
829 prog='gcl try')
830 else:
831 trychange.TryChange(args,
832 file_list=None,
833 swallow_exception=swallow_exception,
834 prog='gcl try')
835
836
837def Commit(change_info, args):
838 if not change_info.FileList():
839 print "Nothing to commit, changelist is empty."
840 return
841
842 if not "--no_presubmit" in args:
843 if not DoPresubmitChecks(change_info, committing=True):
844 return
845 else:
846 args.remove("--no_presubmit")
847
848 no_tree_status_check = ("--force" in args or "-f" in args)
849 if not no_tree_status_check and not IsTreeOpen():
850 print ("Error: The tree is closed. Try again later or use --force to force"
851 " the commit. May the --force be with you.")
852 return
853
854 commit_cmd = ["svn", "commit"]
855 filename = ''
856 if change_info.issue:
857 # Get the latest description from Rietveld.
858 change_info.description = GetIssueDescription(change_info.issue)
859
860 commit_message = change_info.description.replace('\r\n', '\n')
861 if change_info.issue:
862 commit_message += ('\nReview URL: http://%s/%s' %
863 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
864 change_info.issue))
865
866 handle, commit_filename = tempfile.mkstemp(text=True)
867 os.write(handle, commit_message)
868 os.close(handle)
869
870 handle, targets_filename = tempfile.mkstemp(text=True)
871 os.write(handle, "\n".join(change_info.FileList()))
872 os.close(handle)
873
874 commit_cmd += ['--file=' + commit_filename]
875 commit_cmd += ['--targets=' + targets_filename]
876 # Change the current working directory before calling commit.
877 previous_cwd = os.getcwd()
878 os.chdir(GetRepositoryRoot())
879 output = RunShell(commit_cmd, True)
880 os.remove(commit_filename)
881 os.remove(targets_filename)
882 if output.find("Committed revision") != -1:
883 change_info.Delete()
884
885 if change_info.issue:
886 revision = re.compile(".*?\nCommitted revision (\d+)",
887 re.DOTALL).match(output).group(1)
888 viewvc_url = GetCodeReviewSetting("VIEW_VC")
889 change_info.description = change_info.description + '\n'
890 if viewvc_url:
891 change_info.description += "\nCommitted: " + viewvc_url + revision
892 change_info.CloseIssue()
893 os.chdir(previous_cwd)
894
895
896def Change(change_info):
897 """Creates/edits a changelist."""
898 if change_info.issue:
899 try:
900 description = GetIssueDescription(change_info.issue)
901 except urllib2.HTTPError, err:
902 if err.code == 404:
903 # The user deleted the issue in Rietveld, so forget the old issue id.
904 description = change_info.description
905 change_info.issue = ""
906 change_info.Save()
907 else:
908 ErrorExit("Error getting the description from Rietveld: " + err)
909 else:
910 description = change_info.description
911
912 other_files = GetFilesNotInCL()
913
914 separator1 = ("\n---All lines above this line become the description.\n"
915 "---Repository Root: " + GetRepositoryRoot() + "\n"
916 "---Paths in this changelist (" + change_info.name + "):\n")
917 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
918 text = (description + separator1 + '\n' +
919 '\n'.join([f[0] + f[1] for f in change_info.files]) + separator2 +
920 '\n'.join([f[0] + f[1] for f in other_files]) + '\n')
921
922 handle, filename = tempfile.mkstemp(text=True)
923 os.write(handle, text)
924 os.close(handle)
925
926 os.system(GetEditor() + " " + filename)
927
928 result = ReadFile(filename)
929 os.remove(filename)
930
931 if not result:
932 return
933
934 split_result = result.split(separator1, 1)
935 if len(split_result) != 2:
936 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
937
938 new_description = split_result[0]
939 cl_files_text = split_result[1]
940 if new_description != description:
941 change_info.description = new_description
942 if change_info.issue:
943 # Update the Rietveld issue with the new description.
944 change_info.UpdateRietveldDescription()
945
946 new_cl_files = []
947 for line in cl_files_text.splitlines():
948 if not len(line):
949 continue
950 if line.startswith("---"):
951 break
952 status = line[:7]
953 file = line[7:]
954 new_cl_files.append((status, file))
955 change_info.files = new_cl_files
956
957 change_info.Save()
958 print change_info.name + " changelist saved."
959 if change_info.MissingTests():
960 Warn("WARNING: " + MISSING_TEST_MSG)
961
962# We don't lint files in these path prefixes.
963IGNORE_PATHS = ("webkit",)
964
965# Valid extensions for files we want to lint.
966CPP_EXTENSIONS = ("cpp", "cc", "h")
967
968def Lint(change_info, args):
969 """Runs cpplint.py on all the files in |change_info|"""
970 try:
971 import cpplint
972 except ImportError:
973 ErrorExit("You need to install cpplint.py to lint C++ files.")
974
975 # Change the current working directory before calling lint so that it
976 # shows the correct base.
977 previous_cwd = os.getcwd()
978 os.chdir(GetRepositoryRoot())
979
980 # Process cpplints arguments if any.
981 filenames = cpplint.ParseArguments(args + change_info.FileList())
982
983 for file in filenames:
984 if len([file for suffix in CPP_EXTENSIONS if file.endswith(suffix)]):
985 if len([file for prefix in IGNORE_PATHS if file.startswith(prefix)]):
986 print "Ignoring non-Google styled file %s" % file
987 else:
988 cpplint.ProcessFile(file, cpplint._cpplint_state.verbose_level)
989
990 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
991 os.chdir(previous_cwd)
992
993
994def DoPresubmitChecks(change_info, committing):
995 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
996 # Need to import here to avoid circular dependency.
[email protected]1033acc2009-05-13 14:36:48997 import presubmit_support
998 result = presubmit_support.DoPresubmitChecks(change_info,
999 committing,
1000 verbose=False,
1001 output_stream=sys.stdout,
1002 input_stream=sys.stdin)
[email protected]fb2b8eb2009-04-23 21:03:421003 if not result:
1004 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1005 return result
1006
1007
1008def Changes():
1009 """Print all the changelists and their files."""
1010 for cl in GetCLs():
1011 change_info = LoadChangelistInfo(cl, True, True)
1012 print "\n--- Changelist " + change_info.name + ":"
1013 for file in change_info.files:
1014 print "".join(file)
1015
1016
1017def main(argv=None):
1018 if argv is None:
1019 argv = sys.argv
1020
1021 if len(argv) == 1:
1022 Help()
1023 return 0;
1024
[email protected]374d65e2009-05-21 14:00:521025 # Create the directories where we store information about changelists if it
[email protected]fb2b8eb2009-04-23 21:03:421026 # doesn't exist.
1027 if not os.path.exists(GetInfoDir()):
1028 os.mkdir(GetInfoDir())
[email protected]374d65e2009-05-21 14:00:521029 if not os.path.exists(GetChangesDir()):
1030 os.mkdir(GetChangesDir())
1031 # For smooth upgrade support, move the files in GetInfoDir() to
1032 # GetChangesDir().
1033 # TODO(maruel): Remove this code in August 2009.
1034 for file in os.listdir(unicode(GetInfoDir())):
1035 file_path = os.path.join(unicode(GetInfoDir()), file)
1036 if os.path.isfile(file_path) and file != CODEREVIEW_SETTINGS_FILE:
1037 shutil.move(file_path, GetChangesDir())
[email protected]fb2b8eb2009-04-23 21:03:421038
1039 # Commands that don't require an argument.
1040 command = argv[1]
1041 if command == "opened":
1042 Opened()
1043 return 0
1044 if command == "status":
1045 Opened()
1046 print "\n--- Not in any changelist:"
1047 UnknownFiles([])
1048 return 0
1049 if command == "nothave":
1050 UnknownFiles(argv[2:])
1051 return 0
1052 if command == "changes":
1053 Changes()
1054 return 0
1055 if command == "help":
1056 Help(argv[2:])
1057 return 0
1058 if command == "diff" and len(argv) == 2:
1059 files = GetFilesNotInCL()
1060 print GenerateDiff([x[1] for x in files])
1061 return 0
1062 if command == "settings":
1063 ignore = GetCodeReviewSetting("UNKNOWN");
1064 print CODEREVIEW_SETTINGS
1065 return 0
1066
1067 if len(argv) == 2:
1068 if command == "change":
1069 # Generate a random changelist name.
1070 changename = GenerateChangeName()
1071 else:
1072 ErrorExit("Need a changelist name.")
1073 else:
1074 changename = argv[2]
1075
1076 # When the command is 'try' and --patchset is used, the patch to try
1077 # is on the Rietveld server. 'change' creates a change so it's fine if the
1078 # change didn't exist. All other commands require an existing change.
1079 fail_on_not_found = command != "try" and command != "change"
1080 if command == "try" and changename.find(',') != -1:
1081 change_info = LoadChangelistInfoForMultiple(changename, True, True)
1082 else:
1083 change_info = LoadChangelistInfo(changename, fail_on_not_found, True)
1084
1085 if command == "change":
1086 Change(change_info)
1087 elif command == "lint":
1088 Lint(change_info, argv[3:])
1089 elif command == "upload":
1090 UploadCL(change_info, argv[3:])
1091 elif command == "presubmit":
1092 PresubmitCL(change_info)
1093 elif command in ("commit", "submit"):
1094 Commit(change_info, argv[3:])
1095 elif command == "delete":
1096 change_info.Delete()
1097 elif command == "try":
1098 # When the change contains no file, send the "changename" positional
1099 # argument to trychange.py.
1100 if change_info.files:
1101 args = argv[3:]
1102 else:
1103 change_info = None
1104 args = argv[2:]
1105 TryChange(change_info, args, swallow_exception=False)
1106 else:
1107 # Everything else that is passed into gcl we redirect to svn, after adding
1108 # the files. This allows commands such as 'gcl diff xxx' to work.
1109 args =["svn", command]
1110 root = GetRepositoryRoot()
1111 args.extend([os.path.join(root, x) for x in change_info.FileList()])
1112 RunShell(args, True)
1113 return 0
1114
1115
1116if __name__ == "__main__":
1117 sys.exit(main())