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