thirdparty/chromium/gcl.py
changeset 145 9626a42a225b
child 146 1849dc2e4638
equal deleted inserted replaced
144:53d8b8064019 145:9626a42a225b
       
     1 #!/usr/bin/python
       
     2 # Wrapper script around Rietveld's upload.py that groups files into
       
     3 # changelists.
       
     4 
       
     5 import getpass
       
     6 import linecache
       
     7 import os
       
     8 import random
       
     9 import re
       
    10 import string
       
    11 import subprocess
       
    12 import sys
       
    13 import tempfile
       
    14 import upload
       
    15 import urllib2
       
    16 
       
    17 CODEREVIEW_SETTINGS = {
       
    18   # Default values.
       
    19   "CODE_REVIEW_SERVER": "codereview.chromium.org",
       
    20   "CC_LIST": "chromium-reviews@googlegroups.com",
       
    21   "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
       
    22 }
       
    23 
       
    24 # Use a shell for subcommands on Windows to get a PATH search, and because svn
       
    25 # may be a batch file.
       
    26 use_shell = sys.platform.startswith("win")
       
    27 
       
    28 
       
    29 # globals that store the root of the current repositary and the directory where
       
    30 # we store information about changelists.
       
    31 repository_root = ""
       
    32 gcl_info_dir = ""
       
    33 
       
    34 
       
    35 def GetSVNFileInfo(file, field):
       
    36   """Returns a field from the svn info output for the given file."""
       
    37   output = RunShell(["svn", "info", file])
       
    38   for line in output.splitlines():
       
    39     search = field + ": "
       
    40     if line.startswith(search):
       
    41       return line[len(search):]
       
    42   return ""
       
    43 
       
    44 
       
    45 def GetRepositoryRoot():
       
    46   """Returns the top level directory of the current repository."""
       
    47   global repository_root
       
    48   if not repository_root:
       
    49     cur_dir_repo_root = GetSVNFileInfo(os.getcwd(), "Repository Root")
       
    50     if not cur_dir_repo_root:
       
    51       ErrorExit("gcl run outside of repository")
       
    52 
       
    53     repository_root = os.getcwd()
       
    54     while True:
       
    55       parent = os.path.dirname(repository_root)
       
    56       if GetSVNFileInfo(parent, "Repository Root") != cur_dir_repo_root:
       
    57         break
       
    58       repository_root = parent
       
    59   # Now read the code review settings for this repository.
       
    60   settings_file = os.path.join(repository_root, "codereview.settings")
       
    61   if os.path.exists(settings_file):
       
    62     output = ReadFile(settings_file)
       
    63     for line in output.splitlines():
       
    64       if not line or line.startswith("#"):
       
    65         continue
       
    66       key, value = line.split(": ", 1)
       
    67       CODEREVIEW_SETTINGS[key] = value
       
    68   return repository_root
       
    69 
       
    70 
       
    71 def GetCodeReviewSetting(key):
       
    72   """Returns a value for the given key for this repository."""
       
    73   return CODEREVIEW_SETTINGS.get(key, "")
       
    74 
       
    75 
       
    76 def GetInfoDir():
       
    77   """Returns the directory where gcl info files are stored."""
       
    78   global gcl_info_dir
       
    79   if not gcl_info_dir:
       
    80     gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
       
    81   return gcl_info_dir
       
    82 
       
    83 
       
    84 def ErrorExit(msg):
       
    85   """Print an error message to stderr and exit."""
       
    86   print >>sys.stderr, msg
       
    87   sys.exit(1)
       
    88 
       
    89 
       
    90 def RunShell(command, print_output=False):
       
    91   """Executes a command and returns the output."""
       
    92   p = subprocess.Popen(command, stdout = subprocess.PIPE,
       
    93                        stderr = subprocess.STDOUT, shell = use_shell,
       
    94                        universal_newlines=True)
       
    95   if print_output:
       
    96     output_array = []
       
    97     while True:
       
    98       line = p.stdout.readline()
       
    99       if not line:
       
   100         break
       
   101       if print_output:
       
   102         print line.strip('\n')
       
   103       output_array.append(line)
       
   104     output = "".join(output_array)
       
   105   else:
       
   106     output = p.stdout.read()
       
   107   p.wait()
       
   108   p.stdout.close()
       
   109   return output
       
   110 
       
   111 
       
   112 def ReadFile(filename):
       
   113   """Returns the contents of a file."""
       
   114   file = open(filename, 'r')
       
   115   result = file.read()
       
   116   file.close()
       
   117   return result
       
   118 
       
   119 
       
   120 def WriteFile(filename, contents):
       
   121   """Overwrites the file with the given contents."""
       
   122   file = open(filename, 'w')
       
   123   file.write(contents)
       
   124   file.close()
       
   125 
       
   126 
       
   127 class ChangeInfo:
       
   128   """Holds information about a changelist.
       
   129   
       
   130     issue: the Rietveld issue number, of "" if it hasn't been uploaded yet.
       
   131     description: the description.
       
   132     files: a list of 2 tuple containing (status, filename) of changed files,
       
   133            with paths being relative to the top repository directory.
       
   134   """
       
   135   def __init__(self, name="", issue="", description="", files=[]):
       
   136     self.name = name
       
   137     self.issue = issue
       
   138     self.description = description
       
   139     self.files = files
       
   140 
       
   141   def FileList(self):
       
   142     """Returns a list of files."""
       
   143     return [file[1] for file in self.files]
       
   144 
       
   145   def Save(self):
       
   146     """Writes the changelist information to disk."""
       
   147     data = SEPARATOR.join([self.issue,
       
   148                           "\n".join([f[0] + f[1] for f in self.files]),
       
   149                           self.description])
       
   150     WriteFile(GetChangelistInfoFile(self.name), data)
       
   151 
       
   152   def Delete(self):
       
   153     """Removes the changelist information from disk."""
       
   154     os.remove(GetChangelistInfoFile(self.name))
       
   155 
       
   156   def CloseIssue(self):
       
   157     """Closes the Rietveld issue for this changelist."""
       
   158     data = [("description", self.description),]
       
   159     ctype, body = upload.EncodeMultipartFormData(data, [])
       
   160     SendToRietveld("/" + self.issue + "/close", body, ctype)
       
   161 
       
   162   def UpdateRietveldDescription(self):
       
   163     """Sets the description for an issue on Rietveld."""
       
   164     data = [("description", self.description),]
       
   165     ctype, body = upload.EncodeMultipartFormData(data, [])
       
   166     SendToRietveld("/" + self.issue + "/description", body, ctype)
       
   167 
       
   168   
       
   169 SEPARATOR = "\n-----\n"
       
   170 # The info files have the following format:
       
   171 # issue_id\n
       
   172 # SEPARATOR\n
       
   173 # filepath1\n
       
   174 # filepath2\n
       
   175 # .
       
   176 # .
       
   177 # filepathn\n
       
   178 # SEPARATOR\n
       
   179 # description
       
   180 
       
   181 
       
   182 def GetChangelistInfoFile(changename):
       
   183   """Returns the file that stores information about a changelist."""
       
   184   if not changename or re.search(r'\W', changename):
       
   185     ErrorExit("Invalid changelist name: " + changename)
       
   186   return os.path.join(GetInfoDir(), changename)
       
   187 
       
   188 
       
   189 def LoadChangelistInfo(changename, fail_on_not_found=True,
       
   190                        update_status=False):
       
   191   """Gets information about a changelist.
       
   192   
       
   193   Args:
       
   194     fail_on_not_found: if True, this function will quit the program if the
       
   195       changelist doesn't exist.
       
   196     update_status: if True, the svn status will be updated for all the files
       
   197       and unchanged files will be removed.
       
   198   
       
   199   Returns: a ChangeInfo object.
       
   200   """
       
   201   info_file = GetChangelistInfoFile(changename)
       
   202   if not os.path.exists(info_file):
       
   203     if fail_on_not_found:
       
   204       ErrorExit("Changelist " + changename + " not found.")
       
   205     return ChangeInfo(changename)
       
   206   data = ReadFile(info_file)
       
   207   split_data = data.split(SEPARATOR, 2)
       
   208   if len(split_data) != 3:
       
   209     os.remove(info_file)
       
   210     ErrorExit("Changelist file %s was corrupt and deleted" % info_file)
       
   211   issue = split_data[0]
       
   212   files = []
       
   213   for line in split_data[1].splitlines():
       
   214     status = line[:7]
       
   215     file = line[7:]
       
   216     files.append((status, file))
       
   217   description = split_data[2]  
       
   218   save = False
       
   219   if update_status:
       
   220     for file in files:
       
   221       filename = os.path.join(GetRepositoryRoot(), file[1])
       
   222       status = RunShell(["svn", "status", filename])[:7]
       
   223       if not status:  # File has been reverted.
       
   224         save = True
       
   225         files.remove(file)
       
   226       elif status != file[0]:
       
   227         save = True
       
   228         files[files.index(file)] = (status, file[1])
       
   229   change_info = ChangeInfo(changename, issue, description, files)
       
   230   if save:
       
   231     change_info.Save()
       
   232   return change_info
       
   233 
       
   234 
       
   235 def GetCLs():
       
   236   """Returns a list of all the changelists in this repository."""
       
   237   return os.listdir(GetInfoDir())
       
   238 
       
   239 
       
   240 def GenerateChangeName():
       
   241   """Generate a random changelist name."""
       
   242   random.seed()
       
   243   current_cl_names = GetCLs()
       
   244   while True:
       
   245     cl_name = (random.choice(string.ascii_lowercase) +
       
   246                random.choice(string.digits) +
       
   247                random.choice(string.ascii_lowercase) +
       
   248                random.choice(string.digits))
       
   249     if cl_name not in current_cl_names:
       
   250       return cl_name
       
   251 
       
   252 
       
   253 def GetModifiedFiles():
       
   254   """Returns a set that maps from changelist name to (status,filename) tuples.
       
   255 
       
   256   Files not in a changelist have an empty changelist name.  Filenames are in
       
   257   relation to the top level directory of the current repositary.  Note that
       
   258   only the current directory and subdirectories are scanned, in order to
       
   259   improve performance while still being flexible.
       
   260   """
       
   261   files = {}
       
   262   
       
   263   # Since the files are normalized to the root folder of the repositary, figure
       
   264   # out what we need to add to the paths.
       
   265   dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
       
   266 
       
   267   # Get a list of all files in changelists.
       
   268   files_in_cl = {}
       
   269   for cl in GetCLs():
       
   270     change_info = LoadChangelistInfo(cl)
       
   271     for status, filename in change_info.files:
       
   272       files_in_cl[filename] = change_info.name
       
   273 
       
   274   # Get all the modified files.
       
   275   status = RunShell(["svn", "status"])
       
   276   for line in status.splitlines():
       
   277     if not len(line) or line[0] == "?":
       
   278       continue
       
   279     status = line[:7]
       
   280     filename = line[7:]
       
   281     if dir_prefix:
       
   282       filename = os.path.join(dir_prefix, filename)
       
   283     change_list_name = ""
       
   284     if filename in files_in_cl:
       
   285       change_list_name = files_in_cl[filename]
       
   286     files.setdefault(change_list_name, []).append((status, filename))
       
   287 
       
   288   return files
       
   289 
       
   290 
       
   291 def GetFilesNotInCL():
       
   292   """Returns a list of tuples (status,filename) that aren't in any changelists.
       
   293   
       
   294   See docstring of GetModifiedFiles for information about path of files and
       
   295   which directories are scanned.
       
   296   """
       
   297   modified_files = GetModifiedFiles()
       
   298   if "" not in modified_files:
       
   299     return []
       
   300   return modified_files[""]
       
   301 
       
   302 
       
   303 def SendToRietveld(request_path, payload=None,
       
   304                    content_type="application/octet-stream"):
       
   305   """Send a POST/GET to Rietveld.  Returns the response body."""
       
   306   def GetUserCredentials():
       
   307     """Prompts the user for a username and password."""
       
   308     email = raw_input("Email: ").strip()
       
   309     password = getpass.getpass("Password for %s: " % email)
       
   310     return email, password
       
   311 
       
   312   server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
       
   313   rpc_server = upload.HttpRpcServer(server,
       
   314                                     GetUserCredentials,
       
   315                                     host_override=server,
       
   316                                     save_cookies=True)
       
   317   return rpc_server.Send(request_path, payload, content_type)
       
   318 
       
   319 
       
   320 def GetIssueDescription(issue):
       
   321   """Returns the issue description from Rietveld."""
       
   322   return SendToRietveld("/" + issue + "/description")
       
   323 
       
   324 
       
   325 def UnknownFiles(extra_args):
       
   326   """Runs svn status and prints unknown files.
       
   327 
       
   328   Any args in |extra_args| are passed to the tool to support giving alternate
       
   329   code locations.
       
   330   """
       
   331   args = ["svn", "status"]
       
   332   args += extra_args
       
   333   p = subprocess.Popen(args, stdout = subprocess.PIPE,
       
   334                        stderr = subprocess.STDOUT, shell = use_shell)
       
   335   while 1:
       
   336     line = p.stdout.readline()
       
   337     if not line:
       
   338       break
       
   339     if line[0] != '?':
       
   340       continue  # Not an unknown file to svn.
       
   341     # The lines look like this:
       
   342     # "?      foo.txt"
       
   343     # and we want just "foo.txt"
       
   344     print line[7:].strip()
       
   345   p.wait()
       
   346   p.stdout.close()
       
   347 
       
   348 
       
   349 def Opened():
       
   350   """Prints a list of modified files in the current directory down."""
       
   351   files = GetModifiedFiles()
       
   352   cl_keys = files.keys()
       
   353   cl_keys.sort()
       
   354   for cl_name in cl_keys:
       
   355     if cl_name:
       
   356       note = ""
       
   357       if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]):
       
   358         note = " (Note: this changelist contains files outside this directory)"
       
   359       print "\n--- Changelist " + cl_name + note + ":"
       
   360     for file in files[cl_name]:
       
   361       print "".join(file)
       
   362 
       
   363 
       
   364 def Help():
       
   365   print ("GCL is a wrapper for Subversion that simplifies working with groups "
       
   366          "of files.\n")
       
   367   print "Basic commands:"
       
   368   print "-----------------------------------------"
       
   369   print "   gcl change change_name"
       
   370   print ("      Add/remove files to a changelist.  Only scans the current "
       
   371          "directory and subdirectories.\n")
       
   372   print ("   gcl upload change_name [-r reviewer1@gmail.com,"
       
   373          "reviewer2@gmail.com,...] [--send_mail]")
       
   374   print "      Uploads the changelist to the server for review.\n"
       
   375   print "   gcl commit change_name"
       
   376   print "      Commits the changelist to the repository.\n"
       
   377   print "Advanced commands:"
       
   378   print "-----------------------------------------"
       
   379   print "   gcl delete change_name"
       
   380   print "      Deletes a changelist.\n"
       
   381   print "   gcl diff change_name"
       
   382   print "      Diffs all files in the changelist.\n"
       
   383   print "   gcl diff"
       
   384   print ("      Diffs all files in the current directory and subdirectories "
       
   385          "that aren't in a changelist.\n")
       
   386   print "   gcl changes"
       
   387   print "      Lists all the the changelists and the files in them.\n"
       
   388   print "   gcl nothave [optional directory]"
       
   389   print "      Lists files unknown to Subversion.\n"
       
   390   print "   gcl opened"
       
   391   print ("      Lists modified files in the current directory and "
       
   392          "subdirectories.\n")
       
   393   print "   gcl try change_name"
       
   394   print ("      Sends the change to the tryserver so a trybot can do a test"
       
   395          " run on your code.\n")
       
   396 
       
   397 
       
   398 def GetEditor():
       
   399   editor = os.environ.get("SVN_EDITOR")
       
   400   if not editor:
       
   401     editor = os.environ.get("EDITOR")
       
   402 
       
   403   if not editor:
       
   404     if sys.platform.startswith("win"):
       
   405       editor = "notepad"
       
   406     else:
       
   407       editor = "vi"
       
   408 
       
   409   return editor
       
   410 
       
   411 
       
   412 def GenerateDiff(files):
       
   413   """Returns a string containing the diff for the given file list."""
       
   414   diff = []
       
   415   for file in files:
       
   416     # Use svn info output instead of os.path.isdir because the latter fails
       
   417     # when the file is deleted.
       
   418     if GetSVNFileInfo(file, "Node Kind") == "directory":
       
   419       continue
       
   420     # If the user specified a custom diff command in their svn config file,
       
   421     # then it'll be used when we do svn diff, which we don't want to happen
       
   422     # since we want the unified diff.  Using --diff-cmd=diff doesn't always
       
   423     # work, since they can have another diff executable in their path that
       
   424     # gives different line endings.  So we use a bogus temp directory as the
       
   425     # config directory, which gets around these problems.
       
   426     if sys.platform.startswith("win"):
       
   427       parent_dir = tempfile.gettempdir()
       
   428     else:
       
   429       parent_dir = sys.path[0]  # tempdir is not secure.
       
   430     bogus_dir = os.path.join(parent_dir, "temp_svn_config")
       
   431     if not os.path.exists(bogus_dir):
       
   432       os.mkdir(bogus_dir)
       
   433     diff.append(RunShell(["svn", "diff", "--config-dir", bogus_dir, file]))
       
   434   return "".join(diff)
       
   435 
       
   436 
       
   437 def UploadCL(change_info, args):
       
   438   if not change_info.FileList():
       
   439     print "Nothing to upload, changelist is empty."
       
   440     return
       
   441 
       
   442   upload_arg = ["upload.py", "-y", "-l"]
       
   443   upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
       
   444   upload_arg.extend(args)
       
   445 
       
   446   desc_file = ""
       
   447   if change_info.issue:  # Uploading a new patchset.
       
   448     upload_arg.append("--message=''")
       
   449     upload_arg.append("--issue=" + change_info.issue)
       
   450   else: # First time we upload.
       
   451     handle, desc_file = tempfile.mkstemp(text=True)
       
   452     os.write(handle, change_info.description)
       
   453     os.close(handle)
       
   454 
       
   455     upload_arg.append("--cc=" + GetCodeReviewSetting("CC_LIST"))
       
   456     upload_arg.append("--description_file=" + desc_file + "")
       
   457     if change_info.description:
       
   458       subject = change_info.description[:77]
       
   459       if subject.find("\r\n") != -1:
       
   460         subject = subject[:subject.find("\r\n")]
       
   461       if subject.find("\n") != -1:
       
   462         subject = subject[:subject.find("\n")]
       
   463       if len(change_info.description) > 77:
       
   464         subject = subject + "..."
       
   465       upload_arg.append("--message=" + subject)
       
   466   
       
   467   # Change the current working directory before calling upload.py so that it
       
   468   # shows the correct base.
       
   469   os.chdir(GetRepositoryRoot())
       
   470 
       
   471   # If we have a lot of files with long paths, then we won't be able to fit
       
   472   # the command to "svn diff".  Instead, we generate the diff manually for
       
   473   # each file and concatenate them before passing it to upload.py.
       
   474   issue = upload.RealMain(upload_arg, GenerateDiff(change_info.FileList()))
       
   475   if issue and issue != change_info.issue:
       
   476     change_info.issue = issue
       
   477     change_info.Save()
       
   478 
       
   479   if desc_file:
       
   480     os.remove(desc_file)
       
   481 
       
   482 
       
   483 def TryChange(change_info, args):
       
   484   """Create a diff file of change_info and send it to the try server."""
       
   485   try:
       
   486     import trychange
       
   487   except ImportError:
       
   488     ErrorExit("You need to install trychange.py to use the try server.")
       
   489 
       
   490   trychange.TryChange(args, change_info.name, change_info.FileList())
       
   491 
       
   492 
       
   493 def Commit(change_info):
       
   494   if not change_info.FileList():
       
   495     print "Nothing to commit, changelist is empty."
       
   496     return
       
   497 
       
   498   commit_cmd = ["svn", "commit"]
       
   499   filename = ''
       
   500   if change_info.issue:
       
   501     # Get the latest description from Rietveld.
       
   502     change_info.description = GetIssueDescription(change_info.issue)
       
   503 
       
   504   commit_message = change_info.description.replace('\r\n', '\n')
       
   505   if change_info.issue:
       
   506     commit_message += ('\nReview URL: http://%s/%s' %
       
   507                        (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
       
   508                         change_info.issue))
       
   509 
       
   510   handle, commit_filename = tempfile.mkstemp(text=True)
       
   511   os.write(handle, commit_message)
       
   512   os.close(handle)
       
   513 
       
   514   handle, targets_filename = tempfile.mkstemp(text=True)
       
   515   os.write(handle, "\n".join(change_info.FileList()))
       
   516   os.close(handle)
       
   517 
       
   518   commit_cmd += ['--file=' + commit_filename]
       
   519   commit_cmd += ['--targets=' + targets_filename]
       
   520   # Change the current working directory before calling commit.
       
   521   os.chdir(GetRepositoryRoot())
       
   522   output = RunShell(commit_cmd, True)
       
   523   os.remove(commit_filename)
       
   524   os.remove(targets_filename)
       
   525   if output.find("Committed revision") != -1:
       
   526     change_info.Delete()
       
   527 
       
   528     if change_info.issue:
       
   529       revision = re.compile(".*?\nCommitted revision (\d+)",
       
   530                             re.DOTALL).match(output).group(1)
       
   531       viewvc_url = GetCodeReviewSetting("VIEW_VC")
       
   532       change_info.description = (change_info.description +
       
   533                                  "\n\nCommitted: " + viewvc_url + revision)
       
   534       change_info.CloseIssue()
       
   535 
       
   536 
       
   537 def Change(change_info):
       
   538   """Creates/edits a changelist."""
       
   539   if change_info.issue:
       
   540     try:
       
   541       description = GetIssueDescription(change_info.issue)
       
   542     except urllib2.HTTPError, err:
       
   543       if err.code == 404:
       
   544         # The user deleted the issue in Rietveld, so forget the old issue id.
       
   545         description = change_info.description
       
   546         change_info.issue = ""
       
   547         change_info.Save()
       
   548       else:
       
   549         ErrorExit("Error getting the description from Rietveld: " + err)
       
   550   else:
       
   551     description = change_info.description
       
   552 
       
   553   other_files = GetFilesNotInCL()
       
   554 
       
   555   separator1 = ("\n---All lines above this line become the description.\n"
       
   556                 "---Repository Root: " + GetRepositoryRoot() + "\n"
       
   557                 "---Paths in this changelist (" + change_info.name + "):\n")
       
   558   separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
       
   559   text = (description + separator1 + '\n' +
       
   560           '\n'.join([f[0] + f[1] for f in change_info.files]) + separator2 +
       
   561           '\n'.join([f[0] + f[1] for f in other_files]) + '\n')
       
   562 
       
   563   handle, filename = tempfile.mkstemp(text=True)
       
   564   os.write(handle, text)
       
   565   os.close(handle)
       
   566   
       
   567   command = GetEditor() + " " + filename
       
   568   os.system(command)
       
   569 
       
   570   result = ReadFile(filename)
       
   571   os.remove(filename)
       
   572 
       
   573   if not result:
       
   574     return
       
   575 
       
   576   split_result = result.split(separator1, 1)
       
   577   if len(split_result) != 2:
       
   578     ErrorExit("Don't modify the text starting with ---!\n\n" + result)
       
   579 
       
   580   new_description = split_result[0]
       
   581   cl_files_text = split_result[1]
       
   582   if new_description != description:
       
   583     change_info.description = new_description
       
   584     if change_info.issue:
       
   585       # Update the Rietveld issue with the new description.
       
   586       change_info.UpdateRietveldDescription()
       
   587 
       
   588   new_cl_files = []
       
   589   for line in cl_files_text.splitlines():
       
   590     if not len(line):
       
   591       continue
       
   592     if line.startswith("---"):
       
   593       break
       
   594     status = line[:7]
       
   595     file = line[7:]
       
   596     new_cl_files.append((status, file))
       
   597   change_info.files = new_cl_files
       
   598 
       
   599   change_info.Save()
       
   600   print change_info.name + " changelist saved."
       
   601 
       
   602 
       
   603 def Changes():
       
   604   """Print all the changlists and their files."""
       
   605   for cl in GetCLs():
       
   606     change_info = LoadChangelistInfo(cl, True, True)
       
   607     print "\n--- Changelist " + change_info.name + ":"
       
   608     for file in change_info.files:
       
   609       print "".join(file)
       
   610 
       
   611 
       
   612 def main(argv=None):
       
   613   if argv is None:
       
   614     argv = sys.argv
       
   615   
       
   616   if len(argv) == 1:
       
   617     Help()
       
   618     return 0;
       
   619 
       
   620   # Create the directory where we store information about changelists if it
       
   621   # doesn't exist.
       
   622   if not os.path.exists(GetInfoDir()):
       
   623     os.mkdir(GetInfoDir())
       
   624 
       
   625   command = argv[1]
       
   626   if command == "opened":
       
   627     Opened()
       
   628     return 0
       
   629   if command == "nothave":
       
   630     UnknownFiles(argv[2:])
       
   631     return 0
       
   632   if command == "changes":
       
   633     Changes()
       
   634     return 0
       
   635   if command == "diff" and len(argv) == 2:
       
   636     files = GetFilesNotInCL()
       
   637     print GenerateDiff([os.path.join(GetRepositoryRoot(), x[1]) for x in files])
       
   638     return 0
       
   639 
       
   640   if len(argv) == 2:
       
   641     if command == "change":
       
   642       # Generate a random changelist name.
       
   643       changename = GenerateChangeName()
       
   644     elif command == "help":
       
   645       Help()
       
   646       return 0
       
   647     else:
       
   648       ErrorExit("Need a changelist name.")
       
   649   else:
       
   650     changename = argv[2]
       
   651 
       
   652   fail_on_not_found = command != "change"
       
   653   change_info = LoadChangelistInfo(changename, fail_on_not_found, True)
       
   654 
       
   655   if command == "change":
       
   656     Change(change_info)
       
   657   elif command == "upload":
       
   658     UploadCL(change_info, argv[3:])
       
   659   elif command == "commit":
       
   660     Commit(change_info)
       
   661   elif command == "delete":
       
   662     change_info.Delete()
       
   663   elif command == "try":
       
   664     TryChange(change_info, argv[3:])
       
   665   else:
       
   666     # Everything else that is passed into gcl we redirect to svn, after adding
       
   667     # the files. This allows commands such as 'gcl diff xxx' to work.
       
   668     args =["svn", command]
       
   669     root = GetRepositoryRoot()
       
   670     args.extend([os.path.join(root, x) for x in change_info.FileList()])
       
   671     RunShell(args, True)
       
   672   return 0
       
   673 
       
   674 
       
   675 if __name__ == "__main__":
       
   676     sys.exit(main())