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

Side by Side Diff: gcl.py

Issue 2269413002: Delete gcl, drover, and trychange (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Comments Created 4 years, 4 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 | « gcl.bat ('k') | gclient_utils.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2012 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 """\
7 Wrapper script around Rietveld's upload.py that simplifies working with groups
8 of files.
9 """
10
11 import json
12 import optparse
13 import os
14 import random
15 import re
16 import ssl
17 import string
18 import sys
19 import tempfile
20 import time
21 import urllib2
22
23
24 import auth
25 import fix_encoding
26 import gclient_utils
27 import git_cl
28 import presubmit_support
29 import rietveld
30 from scm import SVN
31 import subprocess2
32 from third_party import upload
33
34 __version__ = '1.2.1'
35
36
37 CODEREVIEW_SETTINGS = {
38 # To make gcl send reviews to a server, check in a file named
39 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your
40 # project's base directory and add the following line to codereview.settings:
41 # CODE_REVIEW_SERVER: codereview.yourserver.org
42 }
43
44 # globals that store the root of the current repository and the directory where
45 # we store information about changelists.
46 REPOSITORY_ROOT = ""
47
48 # Printed when people upload patches using svn.
49 SWITCH_TO_GIT = """You're using svn to work on depot_tools.
50 Consider switching to git today, so that you're ready when svn stops working
51 and you need a functional checkout for a future fire."""
52
53 # Filename where we store repository specific information for gcl.
54 CODEREVIEW_SETTINGS_FILE = "codereview.settings"
55 CODEREVIEW_SETTINGS_FILE_NOT_FOUND = (
56 'No %s file found. Please add one.' % CODEREVIEW_SETTINGS_FILE)
57
58 # Warning message when the change appears to be missing tests.
59 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!"
60
61 # Global cache of files cached in GetCacheDir().
62 FILES_CACHE = {}
63
64 # Valid extensions for files we want to lint.
65 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
66 DEFAULT_LINT_IGNORE_REGEX = r"$^"
67
68 def CheckHomeForFile(filename):
69 """Checks the users home dir for the existence of the given file. Returns
70 the path to the file if it's there, or None if it is not.
71 """
72 full_path = os.path.expanduser(os.path.join('~', filename))
73 if os.path.exists(full_path):
74 return full_path
75 return None
76
77
78 def UnknownFiles():
79 """Runs svn status and returns unknown files."""
80 return [
81 item[1] for item in SVN.CaptureStatus([], GetRepositoryRoot())
82 if item[0][0] == '?'
83 ]
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 REPOSITORY_ROOT = SVN.GetCheckoutRoot(os.getcwd())
94 if not REPOSITORY_ROOT:
95 raise gclient_utils.Error("gcl run outside of repository")
96 return REPOSITORY_ROOT
97
98
99 def GetInfoDir():
100 """Returns the directory where gcl info files are stored."""
101 return os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
102
103
104 def GetChangesDir():
105 """Returns the directory where gcl change files are stored."""
106 return os.path.join(GetInfoDir(), 'changes')
107
108
109 def GetCacheDir():
110 """Returns the directory where gcl change files are stored."""
111 return os.path.join(GetInfoDir(), 'cache')
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 GetCacheDir() 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 Note: The cache will be inconsistent if the same file is retrieved with both
122 use_root=True and use_root=False. Don't be stupid.
123 """
124 if filename not in FILES_CACHE:
125 # Don't try to look up twice.
126 FILES_CACHE[filename] = None
127 # First we check if we have a cached version.
128 try:
129 cached_file = os.path.join(GetCacheDir(), filename)
130 except (gclient_utils.Error, subprocess2.CalledProcessError):
131 return None
132 if (not os.path.exists(cached_file) or
133 (time.time() - os.stat(cached_file).st_mtime) > max_age):
134 dir_info = SVN.CaptureLocalInfo([], '.')
135 repo_root = dir_info['Repository Root']
136 if use_root:
137 url_path = repo_root
138 else:
139 url_path = dir_info['URL']
140 while True:
141 # Look in the repository at the current level for the file.
142 for _ in range(5):
143 content = None
144 try:
145 # Take advantage of the fact that svn won't output to stderr in case
146 # of success but will do in case of failure so don't mind putting
147 # stderr into content_array.
148 content_array = []
149 svn_path = url_path + '/' + filename
150 args = ['svn', 'cat', svn_path]
151 if sys.platform != 'darwin':
152 # MacOSX 10.5.2 has a bug with svn 1.4.4 that will trigger the
153 # 'Can\'t get username or password' and can be fixed easily.
154 # The fix doesn't work if the user upgraded to svn 1.6.x. Bleh.
155 # I don't have time to fix their broken stuff.
156 args.append('--non-interactive')
157 gclient_utils.CheckCallAndFilter(
158 args, cwd='.', filter_fn=content_array.append)
159 # Exit the loop if the file was found. Override content.
160 content = '\n'.join(content_array)
161 break
162 except (gclient_utils.Error, subprocess2.CalledProcessError):
163 if content_array[0].startswith(
164 'svn: Can\'t get username or password'):
165 ErrorExit('Your svn credentials expired. Please run svn update '
166 'to fix the cached credentials')
167 if content_array[0].startswith('svn: Can\'t get password'):
168 ErrorExit('If are using a Mac and svn --version shows 1.4.x, '
169 'please hack gcl.py to remove --non-interactive usage, it\'s'
170 'a bug on your installed copy')
171 if (content_array[0].startswith('svn: File not found:') or
172 content_array[0].endswith('path not found')):
173 break
174 # Otherwise, fall through to trying again.
175 if content:
176 break
177 if url_path == repo_root:
178 # Reached the root. Abandoning search.
179 break
180 # Go up one level to try again.
181 url_path = os.path.dirname(url_path)
182 if content is not None or filename != CODEREVIEW_SETTINGS_FILE:
183 # Write a cached version even if there isn't a file, so we don't try to
184 # fetch it each time. codereview.settings must always be present so do
185 # not cache negative.
186 gclient_utils.FileWrite(cached_file, content or '')
187 else:
188 content = gclient_utils.FileRead(cached_file, 'r')
189 # Keep the content cached in memory.
190 FILES_CACHE[filename] = content
191 return FILES_CACHE[filename]
192
193
194 def GetCodeReviewSetting(key):
195 """Returns a value for the given key for this repository."""
196 # Use '__just_initialized' as a flag to determine if the settings were
197 # already initialized.
198 if '__just_initialized' not in CODEREVIEW_SETTINGS:
199 settings_file = GetCachedFile(CODEREVIEW_SETTINGS_FILE)
200 if settings_file:
201 CODEREVIEW_SETTINGS.update(
202 gclient_utils.ParseCodereviewSettingsContent(settings_file))
203 CODEREVIEW_SETTINGS.setdefault('__just_initialized', None)
204 return CODEREVIEW_SETTINGS.get(key, "")
205
206
207 def Warn(msg):
208 print >> sys.stderr, msg
209
210
211 def ErrorExit(msg):
212 print >> sys.stderr, msg
213 sys.exit(1)
214
215
216 def RunShellWithReturnCode(command, print_output=False):
217 """Executes a command and returns the output and the return code."""
218 p = subprocess2.Popen(
219 command,
220 cwd=GetRepositoryRoot(),
221 stdout=subprocess2.PIPE,
222 stderr=subprocess2.STDOUT,
223 universal_newlines=True)
224 if print_output:
225 output_array = []
226 while True:
227 line = p.stdout.readline()
228 if not line:
229 break
230 if print_output:
231 print line.strip('\n')
232 output_array.append(line)
233 output = "".join(output_array)
234 else:
235 output = p.stdout.read()
236 p.wait()
237 p.stdout.close()
238 return output, p.returncode
239
240
241 def RunShell(command, print_output=False):
242 """Executes a command and returns the output."""
243 return RunShellWithReturnCode(command, print_output)[0]
244
245
246 def FilterFlag(args, flag):
247 """Returns True if the flag is present in args list.
248
249 The flag is removed from args if present.
250 """
251 if flag in args:
252 args.remove(flag)
253 return True
254 return False
255
256
257 class ChangeInfo(object):
258 """Holds information about a changelist.
259
260 name: change name.
261 issue: the Rietveld issue number or 0 if it hasn't been uploaded yet.
262 patchset: the Rietveld latest patchset number or 0.
263 description: the description.
264 files: a list of 2 tuple containing (status, filename) of changed files,
265 with paths being relative to the top repository directory.
266 local_root: Local root directory
267 rietveld: rietveld server for this change
268 """
269 # Kept for unit test support. This is for the old format, it's deprecated.
270 SEPARATOR = "\n-----\n"
271
272 def __init__(self, name, issue, patchset, description, files, local_root,
273 rietveld_url, needs_upload):
274 # Defer the description processing to git_cl.ChangeDescription.
275 self._desc = git_cl.ChangeDescription(description)
276 self.name = name
277 self.issue = int(issue)
278 self.patchset = int(patchset)
279 self._files = files or []
280 self.patch = None
281 self._local_root = local_root
282 self.needs_upload = needs_upload
283 self.rietveld = gclient_utils.UpgradeToHttps(
284 rietveld_url or GetCodeReviewSetting('CODE_REVIEW_SERVER'))
285 self._rpc_server = None
286
287 @property
288 def description(self):
289 return self._desc.description
290
291 def force_description(self, new_description):
292 self._desc = git_cl.ChangeDescription(new_description)
293 self.needs_upload = True
294
295 def append_footer(self, line):
296 self._desc.append_footer(line)
297
298 def get_reviewers(self):
299 return self._desc.get_reviewers()
300
301 def update_reviewers(self, reviewers):
302 self._desc.update_reviewers(reviewers)
303
304 def NeedsUpload(self):
305 return self.needs_upload
306
307 def GetFileNames(self):
308 """Returns the list of file names included in this change."""
309 return [f[1] for f in self._files]
310
311 def GetFiles(self):
312 """Returns the list of files included in this change with their status."""
313 return self._files
314
315 def GetLocalRoot(self):
316 """Returns the local repository checkout root directory."""
317 return self._local_root
318
319 def Exists(self):
320 """Returns True if this change already exists (i.e., is not new)."""
321 return (self.issue or self.description or self._files)
322
323 def _NonDeletedFileList(self):
324 """Returns a list of files in this change, not including deleted files."""
325 return [f[1] for f in self.GetFiles()
326 if not f[0].startswith("D")]
327
328 def _AddedFileList(self):
329 """Returns a list of files added in this change."""
330 return [f[1] for f in self.GetFiles() if f[0].startswith("A")]
331
332 def Save(self):
333 """Writes the changelist information to disk."""
334 data = json.dumps({
335 'issue': self.issue,
336 'patchset': self.patchset,
337 'needs_upload': self.NeedsUpload(),
338 'files': self.GetFiles(),
339 'description': self.description,
340 'rietveld': self.rietveld,
341 }, sort_keys=True, indent=2)
342 gclient_utils.FileWrite(GetChangelistInfoFile(self.name), data)
343
344 def Delete(self):
345 """Removes the changelist information from disk."""
346 os.remove(GetChangelistInfoFile(self.name))
347
348 def RpcServer(self):
349 if not self._rpc_server:
350 if not self.rietveld:
351 ErrorExit(CODEREVIEW_SETTINGS_FILE_NOT_FOUND)
352 # TODO(vadimsh): glc.py should be deleted soon. Do not bother much about
353 # authentication options and always use defaults.
354 self._rpc_server = rietveld.CachingRietveld(
355 self.rietveld, auth.make_auth_config())
356 return self._rpc_server
357
358 def CloseIssue(self):
359 """Closes the Rietveld issue for this changelist."""
360 # Newer versions of Rietveld require us to pass an XSRF token to POST, so
361 # we fetch it from the server.
362 xsrf_token = self.SendToRietveld(
363 '/xsrf_token',
364 extra_headers={'X-Requesting-XSRF-Token': '1'})
365
366 # You cannot close an issue with a GET.
367 # We pass an empty string for the data so it is a POST rather than a GET.
368 data = [("description", self.description),
369 ("xsrf_token", xsrf_token)]
370 ctype, body = upload.EncodeMultipartFormData(data, [])
371 self.SendToRietveld('/%d/close' % self.issue, payload=body,
372 content_type=ctype)
373
374 def UpdateRietveldDescription(self):
375 """Sets the description for an issue on Rietveld."""
376 data = [("description", self.description),]
377 ctype, body = upload.EncodeMultipartFormData(data, [])
378 self.SendToRietveld('/%d/description' % self.issue, payload=body,
379 content_type=ctype)
380 self.needs_upload = False
381
382 def GetIssueDescription(self):
383 """Returns the issue description from Rietveld."""
384 return self.SendToRietveld('/%d/description' % self.issue).replace('\r\n',
385 '\n')
386
387 def UpdateDescriptionFromIssue(self):
388 """Updates self.description with the issue description from Rietveld."""
389 self._desc = git_cl.ChangeDescription(self.GetIssueDescription())
390
391 def GetApprovingReviewers(self):
392 """Returns the issue reviewers list from Rietveld."""
393 return git_cl.get_approving_reviewers(
394 self.RpcServer().get_issue_properties(self.issue, True))
395
396 def AddComment(self, comment):
397 """Adds a comment for an issue on Rietveld.
398 As a side effect, this will email everyone associated with the issue."""
399 return self.RpcServer().add_comment(self.issue, comment)
400
401 def PrimeLint(self):
402 """Do background work on Rietveld to lint the file so that the results are
403 ready when the issue is viewed."""
404 if self.issue and self.patchset:
405 try:
406 self.SendToRietveld('/lint/issue%s_%s' % (self.issue, self.patchset),
407 timeout=60)
408 except ssl.SSLError as e:
409 # It takes more than 60 seconds to lint some CLs. Silently ignore
410 # the expected timeout.
411 if e.message != 'The read operation timed out':
412 raise
413
414 def SendToRietveld(self, request_path, timeout=None, **kwargs):
415 """Send a POST/GET to Rietveld. Returns the response body."""
416 try:
417 return self.RpcServer().Send(request_path, timeout=timeout, **kwargs)
418 except urllib2.URLError:
419 if timeout is None:
420 ErrorExit('Error accessing url %s' % request_path)
421 else:
422 return None
423
424 def MissingTests(self):
425 """Returns True if the change looks like it needs unit tests but has none.
426
427 A change needs unit tests if it contains any new source files or methods.
428 """
429 SOURCE_SUFFIXES = [".cc", ".cpp", ".c", ".m", ".mm"]
430 # Ignore third_party entirely.
431 files = [f for f in self._NonDeletedFileList()
432 if f.find("third_party") == -1]
433 added_files = [f for f in self._AddedFileList()
434 if f.find("third_party") == -1]
435
436 # If the change is entirely in third_party, we're done.
437 if len(files) == 0:
438 return False
439
440 # Any new or modified test files?
441 # A test file's name ends with "test.*" or "tests.*".
442 test_files = [test for test in files
443 if os.path.splitext(test)[0].rstrip("s").endswith("test")]
444 if len(test_files) > 0:
445 return False
446
447 # Any new source files?
448 source_files = [item for item in added_files
449 if os.path.splitext(item)[1] in SOURCE_SUFFIXES]
450 if len(source_files) > 0:
451 return True
452
453 # Do the long test, checking the files for new methods.
454 return self._HasNewMethod()
455
456 def _HasNewMethod(self):
457 """Returns True if the changeset contains any new functions, or if a
458 function signature has been changed.
459
460 A function is identified by starting flush left, containing a "(" before
461 the next flush-left line, and either ending with "{" before the next
462 flush-left line or being followed by an unindented "{".
463
464 Currently this returns True for new methods, new static functions, and
465 methods or functions whose signatures have been changed.
466
467 Inline methods added to header files won't be detected by this. That's
468 acceptable for purposes of determining if a unit test is needed, since
469 inline methods should be trivial.
470 """
471 # To check for methods added to source or header files, we need the diffs.
472 # We'll generate them all, since there aren't likely to be many files
473 # apart from source and headers; besides, we'll want them all if we're
474 # uploading anyway.
475 if self.patch is None:
476 self.patch = GenerateDiff(self.GetFileNames())
477
478 definition = ""
479 for line in self.patch.splitlines():
480 if not line.startswith("+"):
481 continue
482 line = line.strip("+").rstrip(" \t")
483 # Skip empty lines, comments, and preprocessor directives.
484 # TODO(pamg): Handle multiline comments if it turns out to be a problem.
485 if line == "" or line.startswith("/") or line.startswith("#"):
486 continue
487
488 # A possible definition ending with "{" is complete, so check it.
489 if definition.endswith("{"):
490 if definition.find("(") != -1:
491 return True
492 definition = ""
493
494 # A { or an indented line, when we're in a definition, continues it.
495 if (definition != "" and
496 (line == "{" or line.startswith(" ") or line.startswith("\t"))):
497 definition += line
498
499 # A flush-left line starts a new possible function definition.
500 elif not line.startswith(" ") and not line.startswith("\t"):
501 definition = line
502
503 return False
504
505 @staticmethod
506 def Load(changename, local_root, fail_on_not_found, update_status):
507 """Gets information about a changelist.
508
509 Args:
510 fail_on_not_found: if True, this function will quit the program if the
511 changelist doesn't exist.
512 update_status: if True, the svn status will be updated for all the files
513 and unchanged files will be removed.
514
515 Returns: a ChangeInfo object.
516 """
517 info_file = GetChangelistInfoFile(changename)
518 if not os.path.exists(info_file):
519 if fail_on_not_found:
520 ErrorExit("Changelist " + changename + " not found.")
521 return ChangeInfo(changename, 0, 0, '', None, local_root, None, False)
522 content = gclient_utils.FileRead(info_file)
523 save = False
524 try:
525 values = ChangeInfo._LoadNewFormat(content)
526 except ValueError:
527 try:
528 values = ChangeInfo._LoadOldFormat(content)
529 save = True
530 except ValueError:
531 ErrorExit(
532 ('Changelist file %s is corrupt.\n'
533 'Either run "gcl delete %s" or manually edit the file') % (
534 info_file, changename))
535 files = values['files']
536 if update_status:
537 for item in files[:]:
538 status_result = SVN.CaptureStatus(item[1], local_root)
539 if not status_result or not status_result[0][0]:
540 # File has been reverted.
541 save = True
542 files.remove(item)
543 continue
544 status = status_result[0][0]
545 if status != item[0]:
546 save = True
547 files[files.index(item)] = (status, item[1])
548 change_info = ChangeInfo(
549 changename,
550 values['issue'],
551 values['patchset'],
552 values['description'],
553 files,
554 local_root,
555 values.get('rietveld'),
556 values['needs_upload'])
557 if save:
558 change_info.Save()
559 return change_info
560
561 @staticmethod
562 def _LoadOldFormat(content):
563 # The info files have the following format:
564 # issue_id, patchset\n (, patchset is optional)
565 # SEPARATOR\n
566 # filepath1\n
567 # filepath2\n
568 # .
569 # .
570 # filepathn\n
571 # SEPARATOR\n
572 # description
573 split_data = content.split(ChangeInfo.SEPARATOR, 2)
574 if len(split_data) != 3:
575 raise ValueError('Bad change format')
576 values = {
577 'issue': 0,
578 'patchset': 0,
579 'needs_upload': False,
580 'files': [],
581 }
582 items = split_data[0].split(', ')
583 if items[0]:
584 values['issue'] = int(items[0])
585 if len(items) > 1:
586 values['patchset'] = int(items[1])
587 if len(items) > 2:
588 values['needs_upload'] = (items[2] == "dirty")
589 for line in split_data[1].splitlines():
590 status = line[:7]
591 filename = line[7:]
592 values['files'].append((status, filename))
593 values['description'] = split_data[2]
594 return values
595
596 @staticmethod
597 def _LoadNewFormat(content):
598 return json.loads(content)
599
600 def __str__(self):
601 out = ['%s:' % self.__class__.__name__]
602 for k in dir(self):
603 if k.startswith('__'):
604 continue
605 v = getattr(self, k)
606 if v is self or callable(getattr(self, k)):
607 continue
608 out.append(' %s: %r' % (k, v))
609 return '\n'.join(out)
610
611
612 def GetChangelistInfoFile(changename):
613 """Returns the file that stores information about a changelist."""
614 if not changename or re.search(r'[^\w-]', changename):
615 ErrorExit("Invalid changelist name: " + changename)
616 return os.path.join(GetChangesDir(), changename)
617
618
619 def LoadChangelistInfoForMultiple(changenames, local_root, fail_on_not_found,
620 update_status):
621 """Loads many changes and merge their files list into one pseudo change.
622
623 This is mainly usefull to concatenate many changes into one for a 'gcl try'.
624 """
625 changes = changenames.split(',')
626 aggregate_change_info = ChangeInfo(
627 changenames, 0, 0, '', None, local_root, None, False)
628 for change in changes:
629 aggregate_change_info._files += ChangeInfo.Load(
630 change, local_root, fail_on_not_found, update_status).GetFiles()
631 return aggregate_change_info
632
633
634 def GetCLs():
635 """Returns a list of all the changelists in this repository."""
636 cls = os.listdir(GetChangesDir())
637 if CODEREVIEW_SETTINGS_FILE in cls:
638 cls.remove(CODEREVIEW_SETTINGS_FILE)
639 return cls
640
641
642 def GenerateChangeName():
643 """Generate a random changelist name."""
644 random.seed()
645 current_cl_names = GetCLs()
646 while True:
647 cl_name = (random.choice(string.ascii_lowercase) +
648 random.choice(string.digits) +
649 random.choice(string.ascii_lowercase) +
650 random.choice(string.digits))
651 if cl_name not in current_cl_names:
652 return cl_name
653
654
655 def GetModifiedFiles():
656 """Returns a set that maps from changelist name to (status,filename) tuples.
657
658 Files not in a changelist have an empty changelist name. Filenames are in
659 relation to the top level directory of the current repository. Note that
660 only the current directory and subdirectories are scanned, in order to
661 improve performance while still being flexible.
662 """
663 files = {}
664
665 # Since the files are normalized to the root folder of the repositary, figure
666 # out what we need to add to the paths.
667 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
668
669 # Get a list of all files in changelists.
670 files_in_cl = {}
671 for cl in GetCLs():
672 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(),
673 fail_on_not_found=True, update_status=False)
674 for status, filename in change_info.GetFiles():
675 files_in_cl[filename] = change_info.name
676
677 # Get all the modified files down the current directory.
678 for line in SVN.CaptureStatus(None, os.getcwd()):
679 status = line[0]
680 filename = line[1]
681 if status[0] == "?":
682 continue
683 if dir_prefix:
684 filename = os.path.join(dir_prefix, filename)
685 change_list_name = ""
686 if filename in files_in_cl:
687 change_list_name = files_in_cl[filename]
688 files.setdefault(change_list_name, []).append((status, filename))
689
690 return files
691
692
693 def GetFilesNotInCL():
694 """Returns a list of tuples (status,filename) that aren't in any changelists.
695
696 See docstring of GetModifiedFiles for information about path of files and
697 which directories are scanned.
698 """
699 modified_files = GetModifiedFiles()
700 if "" not in modified_files:
701 return []
702 return modified_files[""]
703
704
705 def ListFiles(show_unknown_files):
706 files = GetModifiedFiles()
707 cl_keys = files.keys()
708 cl_keys.sort()
709 for cl_name in cl_keys:
710 if not cl_name:
711 continue
712 note = ""
713 change_info = ChangeInfo.Load(cl_name, GetRepositoryRoot(),
714 fail_on_not_found=True, update_status=False)
715 if len(change_info.GetFiles()) != len(files[cl_name]):
716 note = " (Note: this changelist contains files outside this directory)"
717 print "\n--- Changelist " + cl_name + note + ":"
718 for filename in files[cl_name]:
719 print "".join(filename)
720 if show_unknown_files:
721 unknown_files = UnknownFiles()
722 if (files.get('') or (show_unknown_files and len(unknown_files))):
723 print "\n--- Not in any changelist:"
724 for item in files.get('', []):
725 print "".join(item)
726 if show_unknown_files:
727 for filename in unknown_files:
728 print "? %s" % filename
729 return 0
730
731
732 def GenerateDiff(files):
733 return SVN.GenerateDiff(
734 files, GetRepositoryRoot(), full_move=False, revision=None)
735
736
737 def GetTreeStatus():
738 tree_status_url = GetCodeReviewSetting('STATUS')
739 return git_cl.GetTreeStatus(tree_status_url) if tree_status_url else "unset"
740
741
742 def OptionallyDoPresubmitChecks(change_info, committing, args):
743 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"):
744 return presubmit_support.PresubmitOutput()
745 return DoPresubmitChecks(change_info, committing, True)
746
747
748 def defer_attributes(a, b):
749 """Copy attributes from an object (like a function) to another."""
750 for x in dir(a):
751 if not getattr(b, x, None):
752 setattr(b, x, getattr(a, x))
753
754
755 def need_change(function):
756 """Converts args -> change_info."""
757 # pylint: disable=W0612,W0621
758 def hook(args):
759 if not len(args) == 1:
760 ErrorExit("You need to pass a change list name")
761 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(), True, True)
762 return function(change_info)
763 defer_attributes(function, hook)
764 hook.need_change = True
765 hook.no_args = True
766 return hook
767
768
769 def need_change_and_args(function):
770 """Converts args -> change_info."""
771 # pylint: disable=W0612,W0621
772 def hook(args):
773 if not args:
774 ErrorExit("You need to pass a change list name")
775 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
776 return function(change_info, args)
777 defer_attributes(function, hook)
778 hook.need_change = True
779 return hook
780
781
782 def no_args(function):
783 """Make sure no args are passed."""
784 # pylint: disable=W0612,W0621
785 def hook(args):
786 if args:
787 ErrorExit("Doesn't support arguments")
788 return function()
789 defer_attributes(function, hook)
790 hook.no_args = True
791 return hook
792
793
794 def attrs(**kwargs):
795 """Decorate a function with new attributes."""
796 def decorate(function):
797 for k in kwargs:
798 setattr(function, k, kwargs[k])
799 return function
800 return decorate
801
802
803 @no_args
804 def CMDopened():
805 """Lists modified files in the current directory down."""
806 return ListFiles(False)
807
808
809 @no_args
810 def CMDstatus():
811 """Lists modified and unknown files in the current directory down."""
812 return ListFiles(True)
813
814
815 @need_change_and_args
816 @attrs(usage='[--no_presubmit] [--no_watchlists]')
817 def CMDupload(change_info, args):
818 """Uploads the changelist to the server for review.
819
820 This does not submit a try job; use gcl try to submit a try job.
821 """
822 if '-s' in args or '--server' in args:
823 ErrorExit('Don\'t use the -s flag, fix codereview.settings instead')
824 if not change_info.GetFiles():
825 print "Nothing to upload, changelist is empty."
826 return 0
827
828 output = OptionallyDoPresubmitChecks(change_info, False, args)
829 if not output.should_continue():
830 return 1
831 no_watchlists = (FilterFlag(args, "--no_watchlists") or
832 FilterFlag(args, "--no-watchlists"))
833
834 # Map --send-mail to --send_mail
835 if FilterFlag(args, "--send-mail"):
836 args.append("--send_mail")
837
838 # Replace -m with -t and --message with --title, but make sure to
839 # preserve anything after the -m/--message.
840 found_deprecated_arg = [False]
841 def replace_message(a):
842 if a.startswith('-m'):
843 found_deprecated_arg[0] = True
844 return '-t' + a[2:]
845 elif a.startswith('--message'):
846 found_deprecated_arg[0] = True
847 return '--title' + a[9:]
848 return a
849 args = map(replace_message, args)
850 if found_deprecated_arg[0]:
851 print >> sys.stderr, (
852 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
853 'In the near future, -m or --message will send a message instead.\n'
854 'See http://goo.gl/JGg0Z for details.\n')
855
856 upload_arg = ["upload.py", "-y"]
857 upload_arg.append("--server=%s" % change_info.rietveld.encode('utf-8'))
858
859 reviewers = change_info.get_reviewers() or output.reviewers
860 if (reviewers and
861 not any(arg.startswith('-r') or arg.startswith('--reviewer') for
862 arg in args)):
863 upload_arg.append('--reviewers=%s' % ','.join(reviewers))
864
865 upload_arg.extend(args)
866
867 desc_file = None
868 try:
869 if change_info.issue:
870 # Uploading a new patchset.
871 upload_arg.append("--issue=%d" % change_info.issue)
872
873 project = GetCodeReviewSetting("PROJECT")
874 if project:
875 print SWITCH_TO_GIT
876 upload_arg.append("--project=%s" % project)
877
878 if not any(i.startswith('--title') or i.startswith('-t') for i in args):
879 upload_arg.append('--title= ')
880 else:
881 # First time we upload.
882 handle, desc_file = tempfile.mkstemp(text=True)
883 os.write(handle, change_info.description)
884 os.close(handle)
885
886 # Watchlist processing -- CC people interested in this changeset
887 # http://dev.chromium.org/developers/contributing-code/watchlists
888 if not no_watchlists:
889 import watchlists
890 watchlist = watchlists.Watchlists(change_info.GetLocalRoot())
891 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames())
892
893 # We check this before applying the "PRIVATE" parameter of codereview
894 # settings assuming that the author of the settings file has put
895 # addresses which we can send private CLs to, and so we should ignore
896 # CC_LIST only when --private is specified explicitly on the command
897 # line.
898 if "--private" in upload_arg:
899 Warn("WARNING: CC_LIST and WATCHLISTS are ignored when --private is "
900 "specified. You need to review and add them manually if "
901 "necessary.")
902 cc_list = ""
903 no_watchlists = True
904 else:
905 cc_list = GetCodeReviewSetting("CC_LIST")
906 if not no_watchlists and watchers:
907 # Filter out all empty elements and join by ','
908 cc_list = ','.join(filter(None, [cc_list] + watchers))
909 if cc_list:
910 upload_arg.append("--cc=" + cc_list)
911 upload_arg.append("--file=%s" % desc_file)
912
913 if GetCodeReviewSetting("PRIVATE") == "True":
914 upload_arg.append("--private")
915
916 project = GetCodeReviewSetting("PROJECT")
917 if project:
918 print SWITCH_TO_GIT
919 upload_arg.append("--project=%s" % project)
920
921 # If we have a lot of files with long paths, then we won't be able to fit
922 # the command to "svn diff". Instead, we generate the diff manually for
923 # each file and concatenate them before passing it to upload.py.
924 if change_info.patch is None:
925 change_info.patch = GenerateDiff(change_info.GetFileNames())
926
927 # Change the current working directory before calling upload.py so that it
928 # shows the correct base.
929 previous_cwd = os.getcwd()
930 os.chdir(change_info.GetLocalRoot())
931 try:
932 try:
933 issue, patchset = upload.RealMain(upload_arg, change_info.patch)
934 except KeyboardInterrupt:
935 sys.exit(1)
936 if issue and patchset:
937 change_info.issue = int(issue)
938 change_info.patchset = int(patchset)
939 change_info.Save()
940 change_info.PrimeLint()
941 finally:
942 os.chdir(previous_cwd)
943 finally:
944 if desc_file:
945 os.remove(desc_file)
946 print "*** Upload does not submit a try; use gcl try to submit a try. ***"
947 return 0
948
949
950 @need_change_and_args
951 @attrs(usage='[--upload]')
952 def CMDpresubmit(change_info, args):
953 """Runs presubmit checks on the change.
954
955 The actual presubmit code is implemented in presubmit_support.py and looks
956 for PRESUBMIT.py files."""
957 if not change_info.GetFiles():
958 print('Nothing to presubmit check, changelist is empty.')
959 return 0
960 parser = optparse.OptionParser()
961 parser.add_option('--upload', action='store_true')
962 options, args = parser.parse_args(args)
963 if args:
964 parser.error('Unrecognized args: %s' % args)
965 if options.upload:
966 print('*** Presubmit checks for UPLOAD would report: ***')
967 return not DoPresubmitChecks(change_info, False, False)
968 else:
969 print('*** Presubmit checks for COMMIT would report: ***')
970 return not DoPresubmitChecks(change_info, True, False)
971
972
973 def TryChange(change_info, args, swallow_exception):
974 """Create a diff file of change_info and send it to the try server."""
975 try:
976 import trychange
977 except ImportError:
978 if swallow_exception:
979 return 1
980 ErrorExit("You need to install trychange.py to use the try server.")
981
982 trychange_args = []
983 if change_info:
984 trychange_args.extend(['--name', change_info.name])
985 if change_info.issue:
986 trychange_args.extend(["--issue", str(change_info.issue)])
987 if change_info.patchset:
988 trychange_args.extend(["--patchset", str(change_info.patchset)])
989 change = presubmit_support.SvnChange(change_info.name,
990 change_info.description,
991 change_info.GetLocalRoot(),
992 change_info.GetFiles(),
993 change_info.issue,
994 change_info.patchset,
995 None)
996 else:
997 change = None
998
999 trychange_args.extend(args)
1000 return trychange.TryChange(
1001 trychange_args,
1002 change=change,
1003 swallow_exception=swallow_exception,
1004 prog='gcl try',
1005 extra_epilog='\n'
1006 'When called from gcl, use the format gcl try <change_name>.\n')
1007
1008
1009 @need_change_and_args
1010 @attrs(usage='[--no_presubmit]')
1011 def CMDcommit(change_info, args):
1012 """Commits the changelist to the repository."""
1013 if not change_info.GetFiles():
1014 print "Nothing to commit, changelist is empty."
1015 return 1
1016
1017 # OptionallyDoPresubmitChecks has a side-effect which eats these flags.
1018 bypassed = '--no_presubmit' in args or '--force' in args
1019 output = OptionallyDoPresubmitChecks(change_info, True, args)
1020 if not output.should_continue():
1021 return 1
1022
1023 # We face a problem with svn here: Let's say change 'bleh' modifies
1024 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies
1025 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*.
1026 # The only fix is to use --non-recursive but that has its issues too:
1027 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise
1028 # you'll get "svn: Cannot non-recursively commit a directory deletion of a
1029 # directory with child nodes". Yay...
1030 commit_cmd = ["svn", "commit"]
1031 if change_info.issue:
1032 # Get the latest description from Rietveld.
1033 change_info.UpdateDescriptionFromIssue()
1034
1035 change_info.update_reviewers(change_info.GetApprovingReviewers())
1036
1037 commit_desc = git_cl.ChangeDescription(change_info.description)
1038 if change_info.issue:
1039 server = change_info.rietveld
1040 if not server.startswith("http://") and not server.startswith("https://"):
1041 server = "http://" + server
1042 commit_desc.append_footer('Review URL: %s/%d' % (server, change_info.issue))
1043
1044 handle, commit_filename = tempfile.mkstemp(text=True)
1045 os.write(handle, commit_desc.description)
1046 os.close(handle)
1047 try:
1048 handle, targets_filename = tempfile.mkstemp(text=True)
1049 os.write(handle, "\n".join(change_info.GetFileNames()))
1050 os.close(handle)
1051 try:
1052 commit_cmd += ['--file=' + commit_filename]
1053 commit_cmd += ['--targets=' + targets_filename]
1054 # Change the current working directory before calling commit.
1055 output = ''
1056 try:
1057 output = RunShell(commit_cmd, True)
1058 except subprocess2.CalledProcessError, e:
1059 ErrorExit('Commit failed.\n%s' % e)
1060 finally:
1061 os.remove(commit_filename)
1062 finally:
1063 os.remove(targets_filename)
1064 if output.find("Committed revision") != -1:
1065 change_info.Delete()
1066
1067 if change_info.issue:
1068 revision = re.compile(".*?\nCommitted revision (\d+)",
1069 re.DOTALL).match(output).group(1)
1070 viewvc_url = GetCodeReviewSetting('VIEW_VC')
1071 if viewvc_url and revision:
1072 change_info.append_footer('Committed: ' + viewvc_url + revision)
1073 elif revision:
1074 change_info.append_footer('Committed: ' + revision)
1075 change_info.CloseIssue()
1076 props = change_info.RpcServer().get_issue_properties(
1077 change_info.issue, False)
1078 patch_num = len(props['patchsets'])
1079 comment = "Committed patchset #%d (id:%d) manually as r%s" % (
1080 patch_num, props['patchsets'][-1], revision)
1081 if bypassed:
1082 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1083 else:
1084 comment += ' (presubmit successful).'
1085 change_info.AddComment(comment)
1086 return 0
1087
1088
1089 def CMDchange(args):
1090 """Creates or edits a changelist.
1091
1092 Only scans the current directory and subdirectories.
1093 """
1094 # Verify the user is running the change command from a read-write checkout.
1095 svn_info = SVN.CaptureLocalInfo([], '.')
1096 if not svn_info:
1097 ErrorExit("Current checkout is unversioned. Please retry with a versioned "
1098 "directory.")
1099
1100 if len(args) == 0:
1101 # Generate a random changelist name.
1102 changename = GenerateChangeName()
1103 elif args[0] == '--force':
1104 changename = GenerateChangeName()
1105 else:
1106 changename = args[0]
1107 change_info = ChangeInfo.Load(changename, GetRepositoryRoot(), False, True)
1108
1109 if len(args) == 2:
1110 if not os.path.isfile(args[1]):
1111 ErrorExit('The change "%s" doesn\'t exist.' % args[1])
1112 f = open(args[1], 'rU')
1113 override_description = f.read()
1114 f.close()
1115 else:
1116 override_description = None
1117
1118 if change_info.issue and not change_info.NeedsUpload():
1119 try:
1120 description = change_info.GetIssueDescription()
1121 except urllib2.HTTPError, err:
1122 if err.code == 404:
1123 # The user deleted the issue in Rietveld, so forget the old issue id.
1124 description = change_info.description
1125 change_info.issue = 0
1126 change_info.Save()
1127 else:
1128 ErrorExit("Error getting the description from Rietveld: " + err)
1129 else:
1130 if override_description:
1131 description = override_description
1132 else:
1133 description = change_info.description
1134
1135 other_files = GetFilesNotInCL()
1136
1137 # Edited files (as opposed to files with only changed properties) will have
1138 # a letter for the first character in the status string.
1139 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE)
1140 affected_files = [x for x in other_files if file_re.match(x[0])]
1141 unaffected_files = [x for x in other_files if not file_re.match(x[0])]
1142
1143 description = description.rstrip() + '\n'
1144
1145 separator1 = ("\n---All lines above this line become the description.\n"
1146 "---Repository Root: " + change_info.GetLocalRoot() + "\n"
1147 "---Paths in this changelist (" + change_info.name + "):\n")
1148 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
1149
1150 text = (description + separator1 + '\n' +
1151 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()]))
1152
1153 if change_info.Exists():
1154 text += (separator2 +
1155 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n')
1156 else:
1157 text += ('\n'.join([f[0] + f[1] for f in affected_files]) + '\n' +
1158 separator2)
1159 text += '\n'.join([f[0] + f[1] for f in unaffected_files]) + '\n'
1160
1161 result = gclient_utils.RunEditor(text, False)
1162 if not result:
1163 ErrorExit('Running editor failed')
1164
1165 split_result = result.split(separator1, 1)
1166 if len(split_result) != 2:
1167 ErrorExit("Don't modify the text starting with ---!\n\n%r" % result)
1168
1169 # Update the CL description if it has changed.
1170 new_description = split_result[0]
1171 cl_files_text = split_result[1]
1172 if new_description != description or override_description:
1173 change_info.force_description(new_description)
1174
1175 new_cl_files = []
1176 for line in cl_files_text.splitlines():
1177 if not len(line):
1178 continue
1179 if line.startswith("---"):
1180 break
1181 status = line[:7]
1182 filename = line[7:]
1183 new_cl_files.append((status, filename))
1184
1185 if (not len(change_info.GetFiles()) and not change_info.issue and
1186 not len(new_description) and not new_cl_files):
1187 ErrorExit("Empty changelist not saved")
1188
1189 change_info._files = new_cl_files
1190 change_info.Save()
1191 if svn_info.get('URL', '').startswith('http:'):
1192 Warn("WARNING: Creating CL in a read-only checkout. You will need to "
1193 "commit using a commit queue!")
1194
1195 print change_info.name + " changelist saved."
1196 if change_info.MissingTests():
1197 Warn("WARNING: " + MISSING_TEST_MSG)
1198
1199 # Update the Rietveld issue.
1200 if change_info.issue and change_info.NeedsUpload():
1201 change_info.UpdateRietveldDescription()
1202 change_info.Save()
1203 return 0
1204
1205
1206 @need_change_and_args
1207 def CMDlint(change_info, args):
1208 """Runs cpplint.py on all the files in the change list.
1209
1210 Checks all the files in the changelist for possible style violations.
1211 """
1212 # Access to a protected member _XX of a client class
1213 # pylint: disable=W0212
1214 try:
1215 import cpplint
1216 import cpplint_chromium
1217 except ImportError:
1218 ErrorExit("You need to install cpplint.py to lint C++ files.")
1219 # Change the current working directory before calling lint so that it
1220 # shows the correct base.
1221 previous_cwd = os.getcwd()
1222 os.chdir(change_info.GetLocalRoot())
1223 try:
1224 # Process cpplints arguments if any.
1225 filenames = cpplint.ParseArguments(args + change_info.GetFileNames())
1226
1227 white_list = GetCodeReviewSetting("LINT_REGEX")
1228 if not white_list:
1229 white_list = DEFAULT_LINT_REGEX
1230 white_regex = re.compile(white_list)
1231 black_list = GetCodeReviewSetting("LINT_IGNORE_REGEX")
1232 if not black_list:
1233 black_list = DEFAULT_LINT_IGNORE_REGEX
1234 black_regex = re.compile(black_list)
1235 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1236 for filename in filenames:
1237 if white_regex.match(filename):
1238 if black_regex.match(filename):
1239 print "Ignoring file %s" % filename
1240 else:
1241 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1242 extra_check_functions)
1243 else:
1244 print "Skipping file %s" % filename
1245 finally:
1246 os.chdir(previous_cwd)
1247
1248 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1249 return 1
1250
1251
1252 def DoPresubmitChecks(change_info, committing, may_prompt):
1253 """Imports presubmit, then calls presubmit.DoPresubmitChecks."""
1254 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True)
1255 change = presubmit_support.SvnChange(change_info.name,
1256 change_info.description,
1257 change_info.GetLocalRoot(),
1258 change_info.GetFiles(),
1259 change_info.issue,
1260 change_info.patchset,
1261 None)
1262 output = presubmit_support.DoPresubmitChecks(
1263 change=change,
1264 committing=committing,
1265 verbose=False,
1266 output_stream=sys.stdout,
1267 input_stream=sys.stdin,
1268 default_presubmit=root_presubmit,
1269 may_prompt=may_prompt,
1270 rietveld_obj=change_info.RpcServer())
1271 if not output.should_continue() and may_prompt:
1272 # TODO(dpranke): move into DoPresubmitChecks(), unify cmd line args.
1273 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
1274
1275 return output
1276
1277
1278 @no_args
1279 def CMDchanges():
1280 """Lists all the changelists and their files."""
1281 for cl in GetCLs():
1282 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1283 print "\n--- Changelist " + change_info.name + ":"
1284 for filename in change_info.GetFiles():
1285 print "".join(filename)
1286 return 0
1287
1288
1289 @no_args
1290 def CMDdeleteempties():
1291 """Delete all changelists that have no files."""
1292 print "\n--- Deleting:"
1293 for cl in GetCLs():
1294 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True)
1295 if not len(change_info.GetFiles()):
1296 print change_info.name
1297 change_info.Delete()
1298 return 0
1299
1300
1301 @no_args
1302 def CMDnothave():
1303 """Lists files unknown to Subversion."""
1304 for filename in UnknownFiles():
1305 print "? " + "".join(filename)
1306 return 0
1307
1308
1309 @attrs(usage='<svn options>')
1310 def CMDdiff(args):
1311 """Diffs all files in the changelist or all files that aren't in a CL."""
1312 files = None
1313 if args:
1314 change_info = ChangeInfo.Load(args.pop(0), GetRepositoryRoot(), True, True)
1315 files = change_info.GetFileNames()
1316 else:
1317 files = [f[1] for f in GetFilesNotInCL()]
1318
1319 root = GetRepositoryRoot()
1320 cmd = ['svn', 'diff']
1321 cmd.extend([os.path.join(root, x) for x in files])
1322 cmd.extend(args)
1323 return RunShellWithReturnCode(cmd, print_output=True)[1]
1324
1325
1326 @no_args
1327 def CMDsettings():
1328 """Prints code review settings for this checkout."""
1329 # Force load settings
1330 GetCodeReviewSetting("UNKNOWN")
1331 del CODEREVIEW_SETTINGS['__just_initialized']
1332 print '\n'.join(("%s: %s" % (str(k), str(v))
1333 for (k,v) in CODEREVIEW_SETTINGS.iteritems()))
1334 return 0
1335
1336
1337 @need_change
1338 def CMDdescription(change_info):
1339 """Prints the description of the specified change to stdout."""
1340 print change_info.description
1341 return 0
1342
1343
1344 def CMDdelete(args):
1345 """Deletes a changelist."""
1346 if not len(args) == 1:
1347 ErrorExit('You need to pass a change list name')
1348 filepath = GetChangelistInfoFile(args[0])
1349 if not os.path.isfile(filepath):
1350 ErrorExit('You need to pass a valid change list name')
1351 os.remove(filepath)
1352 return 0
1353
1354
1355 def CMDtry(args):
1356 """Sends the change to the tryserver to do a test run on your code.
1357
1358 To send multiple changes as one path, use a comma-separated list of
1359 changenames. Use 'gcl help try' for more information!"""
1360 # When the change contains no file, send the "changename" positional
1361 # argument to trychange.py.
1362 # When the command is 'try' and --patchset is used, the patch to try
1363 # is on the Rietveld server.
1364 if not args:
1365 ErrorExit("You need to pass a change list name")
1366 if args[0].find(',') != -1:
1367 change_info = LoadChangelistInfoForMultiple(args[0], GetRepositoryRoot(),
1368 True, True)
1369 else:
1370 change_info = ChangeInfo.Load(args[0], GetRepositoryRoot(),
1371 True, True)
1372
1373 props = change_info.RpcServer().get_issue_properties(
1374 change_info.issue, False)
1375 if props.get('private'):
1376 ErrorExit('Cannot use trybots on a private issue')
1377
1378 if change_info.GetFiles():
1379 args = args[1:]
1380 else:
1381 change_info = None
1382 return TryChange(change_info, args, swallow_exception=False)
1383
1384
1385 @attrs(usage='<old-name> <new-name>')
1386 def CMDrename(args):
1387 """Renames an existing change."""
1388 if len(args) != 2:
1389 ErrorExit("Usage: gcl rename <old-name> <new-name>.")
1390 src, dst = args
1391 src_file = GetChangelistInfoFile(src)
1392 if not os.path.isfile(src_file):
1393 ErrorExit("Change '%s' does not exist." % src)
1394 dst_file = GetChangelistInfoFile(dst)
1395 if os.path.isfile(dst_file):
1396 ErrorExit("Change '%s' already exists; pick a new name." % dst)
1397 os.rename(src_file, dst_file)
1398 print "Change '%s' renamed '%s'." % (src, dst)
1399 return 0
1400
1401
1402 def CMDpassthru(args):
1403 """Everything else that is passed into gcl we redirect to svn.
1404
1405 It assumes a change list name is passed and is converted with the files names.
1406 """
1407 if not args or len(args) < 2:
1408 ErrorExit("You need to pass a change list name for this svn fall-through "
1409 "command")
1410 cl_name = args[1]
1411 args = ["svn", args[0]]
1412 if len(args) > 1:
1413 root = GetRepositoryRoot()
1414 change_info = ChangeInfo.Load(cl_name, root, True, True)
1415 args.extend([os.path.join(root, x) for x in change_info.GetFileNames()])
1416 return RunShellWithReturnCode(args, print_output=True)[1]
1417
1418
1419 def Command(name):
1420 return getattr(sys.modules[__name__], 'CMD' + name, None)
1421
1422
1423 def GenUsage(command):
1424 """Modify an OptParse object with the function's documentation."""
1425 obj = Command(command)
1426 display = command
1427 more = getattr(obj, 'usage', '')
1428 if command == 'help':
1429 display = '<command>'
1430 need_change_val = ''
1431 if getattr(obj, 'need_change', None):
1432 need_change_val = ' <change_list>'
1433 options = ' [options]'
1434 if getattr(obj, 'no_args', None):
1435 options = ''
1436 res = 'Usage: gcl %s%s%s %s\n\n' % (display, need_change_val, options, more)
1437 res += re.sub('\n ', '\n', obj.__doc__)
1438 return res
1439
1440
1441 def CMDhelp(args):
1442 """Prints this help or help for the given command."""
1443 if args and 'CMD' + args[0] in dir(sys.modules[__name__]):
1444 print GenUsage(args[0])
1445
1446 # These commands defer to external tools so give this info too.
1447 if args[0] == 'try':
1448 TryChange(None, ['--help'], swallow_exception=False)
1449 if args[0] == 'upload':
1450 upload.RealMain(['upload.py', '--help'])
1451 return 0
1452
1453 print GenUsage('help')
1454 print sys.modules[__name__].__doc__
1455 print 'version ' + __version__ + '\n'
1456
1457 print('Commands are:\n' + '\n'.join([
1458 ' %-12s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1459 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1460 return 0
1461
1462
1463 def main(argv):
1464 if sys.hexversion < 0x02060000:
1465 print >> sys.stderr, (
1466 '\nYour python version %s is unsupported, please upgrade.\n' %
1467 sys.version.split(' ', 1)[0])
1468 return 2
1469
1470 sys.stderr.write('Warning: gcl is going away soon. Get off subversion!\n')
1471 sys.stderr.write('See http://crbug.com/475321 for more details.\n')
1472
1473 if not argv:
1474 argv = ['help']
1475 command = Command(argv[0])
1476 # Help can be run from anywhere.
1477 if command == CMDhelp:
1478 return command(argv[1:])
1479
1480 try:
1481 GetRepositoryRoot()
1482 except (gclient_utils.Error, subprocess2.CalledProcessError):
1483 print >> sys.stderr, 'To use gcl, you need to be in a subversion checkout.'
1484 return 1
1485
1486 # Create the directories where we store information about changelists if it
1487 # doesn't exist.
1488 try:
1489 if not os.path.exists(GetInfoDir()):
1490 os.mkdir(GetInfoDir())
1491 if not os.path.exists(GetChangesDir()):
1492 os.mkdir(GetChangesDir())
1493 if not os.path.exists(GetCacheDir()):
1494 os.mkdir(GetCacheDir())
1495
1496 if command:
1497 return command(argv[1:])
1498 # Unknown command, try to pass that to svn
1499 return CMDpassthru(argv)
1500 except (gclient_utils.Error, subprocess2.CalledProcessError), e:
1501 print >> sys.stderr, 'Got an exception'
1502 print >> sys.stderr, str(e)
1503 return 1
1504 except upload.ClientLoginError, e:
1505 print >> sys.stderr, 'Got an exception logging in to Rietveld'
1506 print >> sys.stderr, str(e)
1507 return 1
1508 except urllib2.HTTPError, e:
1509 if e.code != 500:
1510 raise
1511 print >> sys.stderr, (
1512 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1513 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))
1514 return 1
1515
1516
1517 if __name__ == "__main__":
1518 fix_encoding.fix_encoding()
1519 try:
1520 sys.exit(main(sys.argv[1:]))
1521 except KeyboardInterrupt:
1522 sys.stderr.write('interrupted\n')
1523 sys.exit(1)
OLDNEW
« no previous file with comments | « gcl.bat ('k') | gclient_utils.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698