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