blob: d8c09afa91d4f272b217dcf84ed9730a8e63f9ac [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
[email protected]207fdf32009-04-28 19:57:0119import xml.dom.minidom
[email protected]fb2b8eb2009-04-23 21:03:4220
[email protected]46a94102009-05-12 20:32:4321# gcl now depends on gclient.
22import gclient
[email protected]c1675e22009-04-27 20:30:4823
24__version__ = '1.0'
25
26
[email protected]fb2b8eb2009-04-23 21:03:4227CODEREVIEW_SETTINGS = {
28 # Default values.
29 "CODE_REVIEW_SERVER": "codereview.chromium.org",
30 "CC_LIST": "[email protected]",
31 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
32}
33
[email protected]fb2b8eb2009-04-23 21:03:4234# globals that store the root of the current repository and the directory where
35# we store information about changelists.
36repository_root = ""
37gcl_info_dir = ""
38
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]46a94102009-05-12 20:32:43104 if (gclient.CaptureSVNInfo(parent).get("Repository Root") !=
105 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."""
113 global gcl_info_dir
114 if not gcl_info_dir:
115 gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
116 return gcl_info_dir
117
118
[email protected]fb2b8eb2009-04-23 21:03:42119def GetCodeReviewSetting(key):
120 """Returns a value for the given key for this repository."""
[email protected]9b613272009-04-24 01:28:28121 global read_gcl_info
122 if not read_gcl_info:
123 read_gcl_info = True
124 # First we check if we have a cached version.
125 cached_settings_file = os.path.join(GetInfoDir(), CODEREVIEW_SETTINGS_FILE)
126 if (not os.path.exists(cached_settings_file) or
127 os.stat(cached_settings_file).st_mtime > 60*60*24*3):
[email protected]46a94102009-05-12 20:32:43128 dir_info = gclient.CaptureSVNInfo(".")
[email protected]9b613272009-04-24 01:28:28129 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():
[email protected]fb2b8eb2009-04-23 21:03:42153 if not line or line.startswith("#"):
154 continue
155 k, v = line.split(": ", 1)
156 CODEREVIEW_SETTINGS[k] = v
[email protected]fb2b8eb2009-04-23 21:03:42157 return CODEREVIEW_SETTINGS.get(key, "")
158
159
160def IsTreeOpen():
161 """Fetches the tree status and returns either True or False."""
162 url = GetCodeReviewSetting('STATUS')
163 status = ""
164 if url:
165 status = urllib2.urlopen(url).read()
166 return status.find('0') == -1
167
168
169def Warn(msg):
170 ErrorExit(msg, exit=False)
171
172
173def ErrorExit(msg, exit=True):
174 """Print an error message to stderr and optionally exit."""
175 print >>sys.stderr, msg
176 if exit:
177 sys.exit(1)
178
179
180def RunShellWithReturnCode(command, print_output=False):
181 """Executes a command and returns the output and the return code."""
[email protected]be0d1ca2009-05-12 19:23:02182 # Use a shell for subcommands on Windows to get a PATH search, and because svn
183 # may be a batch file.
184 use_shell = sys.platform.startswith("win")
[email protected]fb2b8eb2009-04-23 21:03:42185 p = subprocess.Popen(command, stdout=subprocess.PIPE,
186 stderr=subprocess.STDOUT, shell=use_shell,
187 universal_newlines=True)
188 if print_output:
189 output_array = []
190 while True:
191 line = p.stdout.readline()
192 if not line:
193 break
194 if print_output:
195 print line.strip('\n')
196 output_array.append(line)
197 output = "".join(output_array)
198 else:
199 output = p.stdout.read()
200 p.wait()
201 p.stdout.close()
202 return output, p.returncode
203
204
205def RunShell(command, print_output=False):
206 """Executes a command and returns the output."""
207 return RunShellWithReturnCode(command, print_output)[0]
208
209
[email protected]c1675e22009-04-27 20:30:48210def ReadFile(filename, flags='r'):
[email protected]fb2b8eb2009-04-23 21:03:42211 """Returns the contents of a file."""
[email protected]c1675e22009-04-27 20:30:48212 file = open(filename, flags)
[email protected]fb2b8eb2009-04-23 21:03:42213 result = file.read()
214 file.close()
215 return result
216
217
218def WriteFile(filename, contents):
219 """Overwrites the file with the given contents."""
220 file = open(filename, 'w')
221 file.write(contents)
222 file.close()
223
224
[email protected]be0d1ca2009-05-12 19:23:02225class ChangeInfo(object):
[email protected]fb2b8eb2009-04-23 21:03:42226 """Holds information about a changelist.
227
228 issue: the Rietveld issue number, of "" if it hasn't been uploaded yet.
229 description: the description.
230 files: a list of 2 tuple containing (status, filename) of changed files,
231 with paths being relative to the top repository directory.
232 """
[email protected]be0d1ca2009-05-12 19:23:02233 def __init__(self, name="", issue="", description="", files=None):
[email protected]fb2b8eb2009-04-23 21:03:42234 self.name = name
235 self.issue = issue
236 self.description = description
[email protected]be0d1ca2009-05-12 19:23:02237 if files is None:
238 files = []
[email protected]fb2b8eb2009-04-23 21:03:42239 self.files = files
240 self.patch = None
241
242 def FileList(self):
243 """Returns a list of files."""
244 return [file[1] for file in self.files]
245
246 def _NonDeletedFileList(self):
247 """Returns a list of files in this change, not including deleted files."""
248 return [file[1] for file in self.files if not file[0].startswith("D")]
249
250 def _AddedFileList(self):
251 """Returns a list of files added in this change."""
252 return [file[1] for file in self.files if file[0].startswith("A")]
253
254 def Save(self):
255 """Writes the changelist information to disk."""
256 data = SEPARATOR.join([self.issue,
257 "\n".join([f[0] + f[1] for f in self.files]),
258 self.description])
259 WriteFile(GetChangelistInfoFile(self.name), data)
260
261 def Delete(self):
262 """Removes the changelist information from disk."""
263 os.remove(GetChangelistInfoFile(self.name))
264
265 def CloseIssue(self):
266 """Closes the Rietveld issue for this changelist."""
267 data = [("description", self.description),]
268 ctype, body = upload.EncodeMultipartFormData(data, [])
269 SendToRietveld("/" + self.issue + "/close", body, ctype)
270
271 def UpdateRietveldDescription(self):
272 """Sets the description for an issue on Rietveld."""
273 data = [("description", self.description),]
274 ctype, body = upload.EncodeMultipartFormData(data, [])
275 SendToRietveld("/" + self.issue + "/description", body, ctype)
276
277 def MissingTests(self):
278 """Returns True if the change looks like it needs unit tests but has none.
279
280 A change needs unit tests if it contains any new source files or methods.
281 """
282 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
283 # Ignore third_party entirely.
284 files = [file for file in self._NonDeletedFileList()
285 if file.find("third_party") == -1]
286 added_files = [file for file in self._AddedFileList()
287 if file.find("third_party") == -1]
288
289 # If the change is entirely in third_party, we're done.
290 if len(files) == 0:
291 return False
292
293 # Any new or modified test files?
294 # A test file's name ends with "test.*" or "tests.*".
295 test_files = [test for test in files
296 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
297 if len(test_files) > 0:
298 return False
299
300 # Any new source files?
301 source_files = [file for file in added_files
302 if os.path.splitext(file)[1] in SOURCE_SUFFIXES]
303 if len(source_files) > 0:
304 return True
305
306 # Do the long test, checking the files for new methods.
307 return self._HasNewMethod()
308
309 def _HasNewMethod(self):
310 """Returns True if the changeset contains any new functions, or if a
311 function signature has been changed.
312
313 A function is identified by starting flush left, containing a "(" before
314 the next flush-left line, and either ending with "{" before the next
315 flush-left line or being followed by an unindented "{".
316
317 Currently this returns True for new methods, new static functions, and
318 methods or functions whose signatures have been changed.
319
320 Inline methods added to header files won't be detected by this. That's
321 acceptable for purposes of determining if a unit test is needed, since
322 inline methods should be trivial.
323 """
324 # To check for methods added to source or header files, we need the diffs.
325 # We'll generate them all, since there aren't likely to be many files
326 # apart from source and headers; besides, we'll want them all if we're
327 # uploading anyway.
328 if self.patch is None:
329 self.patch = GenerateDiff(self.FileList())
330
331 definition = ""
332 for line in self.patch.splitlines():
333 if not line.startswith("+"):
334 continue
335 line = line.strip("+").rstrip(" \t")
336 # Skip empty lines, comments, and preprocessor directives.
337 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
338 if line == "" or line.startswith("/") or line.startswith("#"):
339 continue
340
341 # A possible definition ending with "{" is complete, so check it.
342 if definition.endswith("{"):
343 if definition.find("(") != -1:
344 return True
345 definition = ""
346
347 # A { or an indented line, when we're in a definition, continues it.
348 if (definition != "" and
349 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
350 definition += line
351
352 # A flush-left line starts a new possible function definition.
353 elif not line.startswith(" ") and not line.startswith("\t"):
354 definition = line
355
356 return False
357
358
359SEPARATOR = "\n-----\n"
360# The info files have the following format:
361# issue_id\n
362# SEPARATOR\n
363# filepath1\n
364# filepath2\n
365# .
366# .
367# filepathn\n
368# SEPARATOR\n
369# description
370
371
372def GetChangelistInfoFile(changename):
373 """Returns the file that stores information about a changelist."""
374 if not changename or re.search(r'[^\w-]', changename):
375 ErrorExit("Invalid changelist name: " + changename)
376 return os.path.join(GetInfoDir(), changename)
377
378
379def LoadChangelistInfoForMultiple(changenames, fail_on_not_found=True,
380 update_status=False):
381 """Loads many changes and merge their files list into one pseudo change.
382
383 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
384 """
385 changes = changenames.split(',')
386 aggregate_change_info = ChangeInfo(name=changenames)
387 for change in changes:
388 aggregate_change_info.files += LoadChangelistInfo(change,
389 fail_on_not_found,
390 update_status).files
391 return aggregate_change_info
392
393
394def LoadChangelistInfo(changename, fail_on_not_found=True,
395 update_status=False):
396 """Gets information about a changelist.
397
398 Args:
399 fail_on_not_found: if True, this function will quit the program if the
400 changelist doesn't exist.
401 update_status: if True, the svn status will be updated for all the files
402 and unchanged files will be removed.
403
404 Returns: a ChangeInfo object.
405 """
406 info_file = GetChangelistInfoFile(changename)
407 if not os.path.exists(info_file):
408 if fail_on_not_found:
409 ErrorExit("Changelist " + changename + " not found.")
410 return ChangeInfo(changename)
411 data = ReadFile(info_file)
412 split_data = data.split(SEPARATOR, 2)
413 if len(split_data) != 3:
414 os.remove(info_file)
415 ErrorExit("Changelist file %s was corrupt and deleted" % info_file)
416 issue = split_data[0]
417 files = []
418 for line in split_data[1].splitlines():
419 status = line[:7]
420 file = line[7:]
421 files.append((status, file))
422 description = split_data[2]
423 save = False
424 if update_status:
425 for file in files:
426 filename = os.path.join(GetRepositoryRoot(), file[1])
[email protected]4810a962009-05-12 21:03:34427 status_result = gclient.CaptureSVNStatus(filename)
[email protected]207fdf32009-04-28 19:57:01428 if not status_result or not status_result[0][0]:
429 # File has been reverted.
[email protected]fb2b8eb2009-04-23 21:03:42430 save = True
431 files.remove(file)
[email protected]8a62d5b2009-05-11 15:59:01432 continue
433 status = status_result[0][0]
434 if status != file[0]:
[email protected]fb2b8eb2009-04-23 21:03:42435 save = True
436 files[files.index(file)] = (status, file[1])
437 change_info = ChangeInfo(changename, issue, description, files)
438 if save:
439 change_info.Save()
440 return change_info
441
442
443def GetCLs():
444 """Returns a list of all the changelists in this repository."""
445 cls = os.listdir(GetInfoDir())
446 if CODEREVIEW_SETTINGS_FILE in cls:
447 cls.remove(CODEREVIEW_SETTINGS_FILE)
448 return cls
449
450
451def GenerateChangeName():
452 """Generate a random changelist name."""
453 random.seed()
454 current_cl_names = GetCLs()
455 while True:
456 cl_name = (random.choice(string.ascii_lowercase) +
457 random.choice(string.digits) +
458 random.choice(string.ascii_lowercase) +
459 random.choice(string.digits))
460 if cl_name not in current_cl_names:
461 return cl_name
462
463
464def GetModifiedFiles():
465 """Returns a set that maps from changelist name to (status,filename) tuples.
466
467 Files not in a changelist have an empty changelist name. Filenames are in
468 relation to the top level directory of the current repository. Note that
469 only the current directory and subdirectories are scanned, in order to
470 improve performance while still being flexible.
471 """
472 files = {}
473
474 # Since the files are normalized to the root folder of the repositary, figure
475 # out what we need to add to the paths.
476 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
477
478 # Get a list of all files in changelists.
479 files_in_cl = {}
480 for cl in GetCLs():
481 change_info = LoadChangelistInfo(cl)
482 for status, filename in change_info.files:
483 files_in_cl[filename] = change_info.name
484
485 # Get all the modified files.
[email protected]4810a962009-05-12 21:03:34486 status_result = gclient.CaptureSVNStatus(None)
[email protected]207fdf32009-04-28 19:57:01487 for line in status_result:
488 status = line[0]
489 filename = line[1]
490 if status[0] == "?":
[email protected]fb2b8eb2009-04-23 21:03:42491 continue
[email protected]fb2b8eb2009-04-23 21:03:42492 if dir_prefix:
493 filename = os.path.join(dir_prefix, filename)
494 change_list_name = ""
495 if filename in files_in_cl:
496 change_list_name = files_in_cl[filename]
497 files.setdefault(change_list_name, []).append((status, filename))
498
499 return files
500
501
502def GetFilesNotInCL():
503 """Returns a list of tuples (status,filename) that aren't in any changelists.
504
505 See docstring of GetModifiedFiles for information about path of files and
506 which directories are scanned.
507 """
508 modified_files = GetModifiedFiles()
509 if "" not in modified_files:
510 return []
511 return modified_files[""]
512
513
514def SendToRietveld(request_path, payload=None,
515 content_type="application/octet-stream", timeout=None):
516 """Send a POST/GET to Rietveld. Returns the response body."""
517 def GetUserCredentials():
518 """Prompts the user for a username and password."""
519 email = upload.GetEmail()
520 password = getpass.getpass("Password for %s: " % email)
521 return email, password
522
523 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
524 rpc_server = upload.HttpRpcServer(server,
525 GetUserCredentials,
526 host_override=server,
527 save_cookies=True)
528 try:
529 return rpc_server.Send(request_path, payload, content_type, timeout)
530 except urllib2.URLError, e:
531 if timeout is None:
532 ErrorExit("Error accessing url %s" % request_path)
533 else:
534 return None
535
536
537def GetIssueDescription(issue):
538 """Returns the issue description from Rietveld."""
539 return SendToRietveld("/" + issue + "/description")
540
541
[email protected]fb2b8eb2009-04-23 21:03:42542def Opened():
543 """Prints a list of modified files in the current directory down."""
544 files = GetModifiedFiles()
545 cl_keys = files.keys()
546 cl_keys.sort()
547 for cl_name in cl_keys:
548 if cl_name:
549 note = ""
550 if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]):
551 note = " (Note: this changelist contains files outside this directory)"
552 print "\n--- Changelist " + cl_name + note + ":"
553 for file in files[cl_name]:
554 print "".join(file)
555
556
557def Help(argv=None):
[email protected]3bcc6ce2009-05-12 22:53:53558 if argv:
559 if argv[0] == 'try':
560 TryChange(None, ['--help'], swallow_exception=False)
561 return
562 if argv[0] == 'upload':
563 upload.RealMain(['upload.py', '--help'])
564 return
[email protected]fb2b8eb2009-04-23 21:03:42565
566 print (
567"""GCL is a wrapper for Subversion that simplifies working with groups of files.
[email protected]c1675e22009-04-27 20:30:48568version """ + __version__ + """
[email protected]fb2b8eb2009-04-23 21:03:42569
570Basic commands:
571-----------------------------------------
572 gcl change change_name
573 Add/remove files to a changelist. Only scans the current directory and
574 subdirectories.
575
576 gcl upload change_name [-r [email protected],[email protected],...]
577 [--send_mail] [--no_try] [--no_presubmit]
578 Uploads the changelist to the server for review.
579
[email protected]9b613272009-04-24 01:28:28580 gcl commit change_name [--force]
[email protected]fb2b8eb2009-04-23 21:03:42581 Commits the changelist to the repository.
582
583 gcl lint change_name
584 Check all the files in the changelist for possible style violations.
585
586Advanced commands:
587-----------------------------------------
588 gcl delete change_name
589 Deletes a changelist.
590
591 gcl diff change_name
592 Diffs all files in the changelist.
593
594 gcl presubmit change_name
595 Runs presubmit checks without uploading the changelist.
596
597 gcl diff
598 Diffs all files in the current directory and subdirectories that aren't in
599 a changelist.
600
601 gcl changes
602 Lists all the the changelists and the files in them.
603
604 gcl nothave [optional directory]
605 Lists files unknown to Subversion.
606
607 gcl opened
608 Lists modified files in the current directory and subdirectories.
609
610 gcl settings
611 Print the code review settings for this directory.
612
613 gcl status
614 Lists modified and unknown files in the current directory and
615 subdirectories.
616
617 gcl try change_name
618 Sends the change to the tryserver so a trybot can do a test run on your
619 code. To send multiple changes as one path, use a comma-separated list
620 of changenames.
621 --> Use 'gcl help try' for more information!
[email protected]3bcc6ce2009-05-12 22:53:53622
623 gcl help [command]
624 Print this help menu, or help for the given command if it exists.
[email protected]fb2b8eb2009-04-23 21:03:42625""")
626
627def GetEditor():
628 editor = os.environ.get("SVN_EDITOR")
629 if not editor:
630 editor = os.environ.get("EDITOR")
631
632 if not editor:
633 if sys.platform.startswith("win"):
634 editor = "notepad"
635 else:
636 editor = "vi"
637
638 return editor
639
640
641def GenerateDiff(files, root=None):
642 """Returns a string containing the diff for the given file list.
643
644 The files in the list should either be absolute paths or relative to the
645 given root. If no root directory is provided, the repository root will be
646 used.
647 """
648 previous_cwd = os.getcwd()
649 if root is None:
650 os.chdir(GetRepositoryRoot())
651 else:
652 os.chdir(root)
653
654 diff = []
655 for file in files:
656 # Use svn info output instead of os.path.isdir because the latter fails
657 # when the file is deleted.
[email protected]46a94102009-05-12 20:32:43658 if gclient.CaptureSVNInfo(file).get("Node Kind") in ("dir", "directory"):
[email protected]fb2b8eb2009-04-23 21:03:42659 continue
660 # If the user specified a custom diff command in their svn config file,
661 # then it'll be used when we do svn diff, which we don't want to happen
662 # since we want the unified diff. Using --diff-cmd=diff doesn't always
663 # work, since they can have another diff executable in their path that
664 # gives different line endings. So we use a bogus temp directory as the
665 # config directory, which gets around these problems.
666 if sys.platform.startswith("win"):
667 parent_dir = tempfile.gettempdir()
668 else:
669 parent_dir = sys.path[0] # tempdir is not secure.
670 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
671 if not os.path.exists(bogus_dir):
672 os.mkdir(bogus_dir)
673 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
674 if output:
675 diff.append(output)
[email protected]c3150202009-05-13 14:31:01676 elif IsSVNMoved(file):
677 # svn diff on a mv/cp'd file outputs nothing.
678 # We put in an empty Index entry so upload.py knows about them.
[email protected]fb2b8eb2009-04-23 21:03:42679 diff.append("\nIndex: %s\n" % file)
[email protected]c3150202009-05-13 14:31:01680 else:
681 # The file is not modified anymore. It should be removed from the set.
682 pass
[email protected]fb2b8eb2009-04-23 21:03:42683 os.chdir(previous_cwd)
684 return "".join(diff)
685
686
687def UploadCL(change_info, args):
688 if not change_info.FileList():
689 print "Nothing to upload, changelist is empty."
690 return
691
692 if not "--no_presubmit" in args:
693 if not DoPresubmitChecks(change_info, committing=False):
694 return
695 else:
696 args.remove("--no_presubmit")
697
698 no_try = "--no_try" in args
699 if no_try:
700 args.remove("--no_try")
701 else:
702 # Support --no-try as --no_try
703 no_try = "--no-try" in args
704 if no_try:
705 args.remove("--no-try")
706
707 # Map --send-mail to --send_mail
708 if "--send-mail" in args:
709 args.remove("--send-mail")
710 args.append("--send_mail")
711
712 # Supports --clobber for the try server.
713 clobber = False
714 if "--clobber" in args:
715 args.remove("--clobber")
716 clobber = True
717
[email protected]003c2692009-05-20 13:08:08718 # Disable try when the server is overridden.
719 server_1 = re.compile(r"^-s\b.*")
720 server_2 = re.compile(r"^--server\b.*")
721 for arg in args:
722 if server_1.match(arg) or server_2.match(arg):
723 no_try = True
724 break
[email protected]fb2b8eb2009-04-23 21:03:42725
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.
[email protected]1033acc2009-05-13 14:36:48995 import presubmit_support
996 result = presubmit_support.DoPresubmitChecks(change_info,
997 committing,
998 verbose=False,
999 output_stream=sys.stdout,
1000 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())