Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(104)

Side by Side Diff: depot_tools/gcl.py

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

Powered by Google App Engine
This is Rietveld 408576698