Pristine initial commit of gcl.py script from chromium.org.
authorTodd Larsen <tlarsen@google.com>
Tue, 16 Sep 2008 01:14:18 +0000
changeset 145 9626a42a225b
parent 144 53d8b8064019
child 146 1849dc2e4638
Pristine initial commit of gcl.py script from chromium.org.
thirdparty/chromium/LICENSE
thirdparty/chromium/gcl.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/chromium/LICENSE	Tue Sep 16 01:14:18 2008 +0000
@@ -0,0 +1,27 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/chromium/gcl.py	Tue Sep 16 01:14:18 2008 +0000
@@ -0,0 +1,676 @@
+#!/usr/bin/python
+# Wrapper script around Rietveld's upload.py that groups files into
+# changelists.
+
+import getpass
+import linecache
+import os
+import random
+import re
+import string
+import subprocess
+import sys
+import tempfile
+import upload
+import urllib2
+
+CODEREVIEW_SETTINGS = {
+  # Default values.
+  "CODE_REVIEW_SERVER": "codereview.chromium.org",
+  "CC_LIST": "chromium-reviews@googlegroups.com",
+  "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=",
+}
+
+# Use a shell for subcommands on Windows to get a PATH search, and because svn
+# may be a batch file.
+use_shell = sys.platform.startswith("win")
+
+
+# globals that store the root of the current repositary and the directory where
+# we store information about changelists.
+repository_root = ""
+gcl_info_dir = ""
+
+
+def GetSVNFileInfo(file, field):
+  """Returns a field from the svn info output for the given file."""
+  output = RunShell(["svn", "info", file])
+  for line in output.splitlines():
+    search = field + ": "
+    if line.startswith(search):
+      return line[len(search):]
+  return ""
+
+
+def GetRepositoryRoot():
+  """Returns the top level directory of the current repository."""
+  global repository_root
+  if not repository_root:
+    cur_dir_repo_root = GetSVNFileInfo(os.getcwd(), "Repository Root")
+    if not cur_dir_repo_root:
+      ErrorExit("gcl run outside of repository")
+
+    repository_root = os.getcwd()
+    while True:
+      parent = os.path.dirname(repository_root)
+      if GetSVNFileInfo(parent, "Repository Root") != cur_dir_repo_root:
+        break
+      repository_root = parent
+  # Now read the code review settings for this repository.
+  settings_file = os.path.join(repository_root, "codereview.settings")
+  if os.path.exists(settings_file):
+    output = ReadFile(settings_file)
+    for line in output.splitlines():
+      if not line or line.startswith("#"):
+        continue
+      key, value = line.split(": ", 1)
+      CODEREVIEW_SETTINGS[key] = value
+  return repository_root
+
+
+def GetCodeReviewSetting(key):
+  """Returns a value for the given key for this repository."""
+  return CODEREVIEW_SETTINGS.get(key, "")
+
+
+def GetInfoDir():
+  """Returns the directory where gcl info files are stored."""
+  global gcl_info_dir
+  if not gcl_info_dir:
+    gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
+  return gcl_info_dir
+
+
+def ErrorExit(msg):
+  """Print an error message to stderr and exit."""
+  print >>sys.stderr, msg
+  sys.exit(1)
+
+
+def RunShell(command, print_output=False):
+  """Executes a command and returns the output."""
+  p = subprocess.Popen(command, stdout = subprocess.PIPE,
+                       stderr = subprocess.STDOUT, shell = use_shell,
+                       universal_newlines=True)
+  if print_output:
+    output_array = []
+    while True:
+      line = p.stdout.readline()
+      if not line:
+        break
+      if print_output:
+        print line.strip('\n')
+      output_array.append(line)
+    output = "".join(output_array)
+  else:
+    output = p.stdout.read()
+  p.wait()
+  p.stdout.close()
+  return output
+
+
+def ReadFile(filename):
+  """Returns the contents of a file."""
+  file = open(filename, 'r')
+  result = file.read()
+  file.close()
+  return result
+
+
+def WriteFile(filename, contents):
+  """Overwrites the file with the given contents."""
+  file = open(filename, 'w')
+  file.write(contents)
+  file.close()
+
+
+class ChangeInfo:
+  """Holds information about a changelist.
+  
+    issue: the Rietveld issue number, of "" if it hasn't been uploaded yet.
+    description: the description.
+    files: a list of 2 tuple containing (status, filename) of changed files,
+           with paths being relative to the top repository directory.
+  """
+  def __init__(self, name="", issue="", description="", files=[]):
+    self.name = name
+    self.issue = issue
+    self.description = description
+    self.files = files
+
+  def FileList(self):
+    """Returns a list of files."""
+    return [file[1] for file in self.files]
+
+  def Save(self):
+    """Writes the changelist information to disk."""
+    data = SEPARATOR.join([self.issue,
+                          "\n".join([f[0] + f[1] for f in self.files]),
+                          self.description])
+    WriteFile(GetChangelistInfoFile(self.name), data)
+
+  def Delete(self):
+    """Removes the changelist information from disk."""
+    os.remove(GetChangelistInfoFile(self.name))
+
+  def CloseIssue(self):
+    """Closes the Rietveld issue for this changelist."""
+    data = [("description", self.description),]
+    ctype, body = upload.EncodeMultipartFormData(data, [])
+    SendToRietveld("/" + self.issue + "/close", body, ctype)
+
+  def UpdateRietveldDescription(self):
+    """Sets the description for an issue on Rietveld."""
+    data = [("description", self.description),]
+    ctype, body = upload.EncodeMultipartFormData(data, [])
+    SendToRietveld("/" + self.issue + "/description", body, ctype)
+
+  
+SEPARATOR = "\n-----\n"
+# The info files have the following format:
+# issue_id\n
+# SEPARATOR\n
+# filepath1\n
+# filepath2\n
+# .
+# .
+# filepathn\n
+# SEPARATOR\n
+# description
+
+
+def GetChangelistInfoFile(changename):
+  """Returns the file that stores information about a changelist."""
+  if not changename or re.search(r'\W', changename):
+    ErrorExit("Invalid changelist name: " + changename)
+  return os.path.join(GetInfoDir(), changename)
+
+
+def LoadChangelistInfo(changename, fail_on_not_found=True,
+                       update_status=False):
+  """Gets information about a changelist.
+  
+  Args:
+    fail_on_not_found: if True, this function will quit the program if the
+      changelist doesn't exist.
+    update_status: if True, the svn status will be updated for all the files
+      and unchanged files will be removed.
+  
+  Returns: a ChangeInfo object.
+  """
+  info_file = GetChangelistInfoFile(changename)
+  if not os.path.exists(info_file):
+    if fail_on_not_found:
+      ErrorExit("Changelist " + changename + " not found.")
+    return ChangeInfo(changename)
+  data = ReadFile(info_file)
+  split_data = data.split(SEPARATOR, 2)
+  if len(split_data) != 3:
+    os.remove(info_file)
+    ErrorExit("Changelist file %s was corrupt and deleted" % info_file)
+  issue = split_data[0]
+  files = []
+  for line in split_data[1].splitlines():
+    status = line[:7]
+    file = line[7:]
+    files.append((status, file))
+  description = split_data[2]  
+  save = False
+  if update_status:
+    for file in files:
+      filename = os.path.join(GetRepositoryRoot(), file[1])
+      status = RunShell(["svn", "status", filename])[:7]
+      if not status:  # File has been reverted.
+        save = True
+        files.remove(file)
+      elif status != file[0]:
+        save = True
+        files[files.index(file)] = (status, file[1])
+  change_info = ChangeInfo(changename, issue, description, files)
+  if save:
+    change_info.Save()
+  return change_info
+
+
+def GetCLs():
+  """Returns a list of all the changelists in this repository."""
+  return os.listdir(GetInfoDir())
+
+
+def GenerateChangeName():
+  """Generate a random changelist name."""
+  random.seed()
+  current_cl_names = GetCLs()
+  while True:
+    cl_name = (random.choice(string.ascii_lowercase) +
+               random.choice(string.digits) +
+               random.choice(string.ascii_lowercase) +
+               random.choice(string.digits))
+    if cl_name not in current_cl_names:
+      return cl_name
+
+
+def GetModifiedFiles():
+  """Returns a set that maps from changelist name to (status,filename) tuples.
+
+  Files not in a changelist have an empty changelist name.  Filenames are in
+  relation to the top level directory of the current repositary.  Note that
+  only the current directory and subdirectories are scanned, in order to
+  improve performance while still being flexible.
+  """
+  files = {}
+  
+  # Since the files are normalized to the root folder of the repositary, figure
+  # out what we need to add to the paths.
+  dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
+
+  # Get a list of all files in changelists.
+  files_in_cl = {}
+  for cl in GetCLs():
+    change_info = LoadChangelistInfo(cl)
+    for status, filename in change_info.files:
+      files_in_cl[filename] = change_info.name
+
+  # Get all the modified files.
+  status = RunShell(["svn", "status"])
+  for line in status.splitlines():
+    if not len(line) or line[0] == "?":
+      continue
+    status = line[:7]
+    filename = line[7:]
+    if dir_prefix:
+      filename = os.path.join(dir_prefix, filename)
+    change_list_name = ""
+    if filename in files_in_cl:
+      change_list_name = files_in_cl[filename]
+    files.setdefault(change_list_name, []).append((status, filename))
+
+  return files
+
+
+def GetFilesNotInCL():
+  """Returns a list of tuples (status,filename) that aren't in any changelists.
+  
+  See docstring of GetModifiedFiles for information about path of files and
+  which directories are scanned.
+  """
+  modified_files = GetModifiedFiles()
+  if "" not in modified_files:
+    return []
+  return modified_files[""]
+
+
+def SendToRietveld(request_path, payload=None,
+                   content_type="application/octet-stream"):
+  """Send a POST/GET to Rietveld.  Returns the response body."""
+  def GetUserCredentials():
+    """Prompts the user for a username and password."""
+    email = raw_input("Email: ").strip()
+    password = getpass.getpass("Password for %s: " % email)
+    return email, password
+
+  server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
+  rpc_server = upload.HttpRpcServer(server,
+                                    GetUserCredentials,
+                                    host_override=server,
+                                    save_cookies=True)
+  return rpc_server.Send(request_path, payload, content_type)
+
+
+def GetIssueDescription(issue):
+  """Returns the issue description from Rietveld."""
+  return SendToRietveld("/" + issue + "/description")
+
+
+def UnknownFiles(extra_args):
+  """Runs svn status and prints unknown files.
+
+  Any args in |extra_args| are passed to the tool to support giving alternate
+  code locations.
+  """
+  args = ["svn", "status"]
+  args += extra_args
+  p = subprocess.Popen(args, stdout = subprocess.PIPE,
+                       stderr = subprocess.STDOUT, shell = use_shell)
+  while 1:
+    line = p.stdout.readline()
+    if not line:
+      break
+    if line[0] != '?':
+      continue  # Not an unknown file to svn.
+    # The lines look like this:
+    # "?      foo.txt"
+    # and we want just "foo.txt"
+    print line[7:].strip()
+  p.wait()
+  p.stdout.close()
+
+
+def Opened():
+  """Prints a list of modified files in the current directory down."""
+  files = GetModifiedFiles()
+  cl_keys = files.keys()
+  cl_keys.sort()
+  for cl_name in cl_keys:
+    if cl_name:
+      note = ""
+      if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]):
+        note = " (Note: this changelist contains files outside this directory)"
+      print "\n--- Changelist " + cl_name + note + ":"
+    for file in files[cl_name]:
+      print "".join(file)
+
+
+def Help():
+  print ("GCL is a wrapper for Subversion that simplifies working with groups "
+         "of files.\n")
+  print "Basic commands:"
+  print "-----------------------------------------"
+  print "   gcl change change_name"
+  print ("      Add/remove files to a changelist.  Only scans the current "
+         "directory and subdirectories.\n")
+  print ("   gcl upload change_name [-r reviewer1@gmail.com,"
+         "reviewer2@gmail.com,...] [--send_mail]")
+  print "      Uploads the changelist to the server for review.\n"
+  print "   gcl commit change_name"
+  print "      Commits the changelist to the repository.\n"
+  print "Advanced commands:"
+  print "-----------------------------------------"
+  print "   gcl delete change_name"
+  print "      Deletes a changelist.\n"
+  print "   gcl diff change_name"
+  print "      Diffs all files in the changelist.\n"
+  print "   gcl diff"
+  print ("      Diffs all files in the current directory and subdirectories "
+         "that aren't in a changelist.\n")
+  print "   gcl changes"
+  print "      Lists all the the changelists and the files in them.\n"
+  print "   gcl nothave [optional directory]"
+  print "      Lists files unknown to Subversion.\n"
+  print "   gcl opened"
+  print ("      Lists modified files in the current directory and "
+         "subdirectories.\n")
+  print "   gcl try change_name"
+  print ("      Sends the change to the tryserver so a trybot can do a test"
+         " run on your code.\n")
+
+
+def GetEditor():
+  editor = os.environ.get("SVN_EDITOR")
+  if not editor:
+    editor = os.environ.get("EDITOR")
+
+  if not editor:
+    if sys.platform.startswith("win"):
+      editor = "notepad"
+    else:
+      editor = "vi"
+
+  return editor
+
+
+def GenerateDiff(files):
+  """Returns a string containing the diff for the given file list."""
+  diff = []
+  for file in files:
+    # Use svn info output instead of os.path.isdir because the latter fails
+    # when the file is deleted.
+    if GetSVNFileInfo(file, "Node Kind") == "directory":
+      continue
+    # If the user specified a custom diff command in their svn config file,
+    # then it'll be used when we do svn diff, which we don't want to happen
+    # since we want the unified diff.  Using --diff-cmd=diff doesn't always
+    # work, since they can have another diff executable in their path that
+    # gives different line endings.  So we use a bogus temp directory as the
+    # config directory, which gets around these problems.
+    if sys.platform.startswith("win"):
+      parent_dir = tempfile.gettempdir()
+    else:
+      parent_dir = sys.path[0]  # tempdir is not secure.
+    bogus_dir = os.path.join(parent_dir, "temp_svn_config")
+    if not os.path.exists(bogus_dir):
+      os.mkdir(bogus_dir)
+    diff.append(RunShell(["svn", "diff", "--config-dir", bogus_dir, file]))
+  return "".join(diff)
+
+
+def UploadCL(change_info, args):
+  if not change_info.FileList():
+    print "Nothing to upload, changelist is empty."
+    return
+
+  upload_arg = ["upload.py", "-y", "-l"]
+  upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
+  upload_arg.extend(args)
+
+  desc_file = ""
+  if change_info.issue:  # Uploading a new patchset.
+    upload_arg.append("--message=''")
+    upload_arg.append("--issue=" + change_info.issue)
+  else: # First time we upload.
+    handle, desc_file = tempfile.mkstemp(text=True)
+    os.write(handle, change_info.description)
+    os.close(handle)
+
+    upload_arg.append("--cc=" + GetCodeReviewSetting("CC_LIST"))
+    upload_arg.append("--description_file=" + desc_file + "")
+    if change_info.description:
+      subject = change_info.description[:77]
+      if subject.find("\r\n") != -1:
+        subject = subject[:subject.find("\r\n")]
+      if subject.find("\n") != -1:
+        subject = subject[:subject.find("\n")]
+      if len(change_info.description) > 77:
+        subject = subject + "..."
+      upload_arg.append("--message=" + subject)
+  
+  # Change the current working directory before calling upload.py so that it
+  # shows the correct base.
+  os.chdir(GetRepositoryRoot())
+
+  # If we have a lot of files with long paths, then we won't be able to fit
+  # the command to "svn diff".  Instead, we generate the diff manually for
+  # each file and concatenate them before passing it to upload.py.
+  issue = upload.RealMain(upload_arg, GenerateDiff(change_info.FileList()))
+  if issue and issue != change_info.issue:
+    change_info.issue = issue
+    change_info.Save()
+
+  if desc_file:
+    os.remove(desc_file)
+
+
+def TryChange(change_info, args):
+  """Create a diff file of change_info and send it to the try server."""
+  try:
+    import trychange
+  except ImportError:
+    ErrorExit("You need to install trychange.py to use the try server.")
+
+  trychange.TryChange(args, change_info.name, change_info.FileList())
+
+
+def Commit(change_info):
+  if not change_info.FileList():
+    print "Nothing to commit, changelist is empty."
+    return
+
+  commit_cmd = ["svn", "commit"]
+  filename = ''
+  if change_info.issue:
+    # Get the latest description from Rietveld.
+    change_info.description = GetIssueDescription(change_info.issue)
+
+  commit_message = change_info.description.replace('\r\n', '\n')
+  if change_info.issue:
+    commit_message += ('\nReview URL: http://%s/%s' %
+                       (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
+                        change_info.issue))
+
+  handle, commit_filename = tempfile.mkstemp(text=True)
+  os.write(handle, commit_message)
+  os.close(handle)
+
+  handle, targets_filename = tempfile.mkstemp(text=True)
+  os.write(handle, "\n".join(change_info.FileList()))
+  os.close(handle)
+
+  commit_cmd += ['--file=' + commit_filename]
+  commit_cmd += ['--targets=' + targets_filename]
+  # Change the current working directory before calling commit.
+  os.chdir(GetRepositoryRoot())
+  output = RunShell(commit_cmd, True)
+  os.remove(commit_filename)
+  os.remove(targets_filename)
+  if output.find("Committed revision") != -1:
+    change_info.Delete()
+
+    if change_info.issue:
+      revision = re.compile(".*?\nCommitted revision (\d+)",
+                            re.DOTALL).match(output).group(1)
+      viewvc_url = GetCodeReviewSetting("VIEW_VC")
+      change_info.description = (change_info.description +
+                                 "\n\nCommitted: " + viewvc_url + revision)
+      change_info.CloseIssue()
+
+
+def Change(change_info):
+  """Creates/edits a changelist."""
+  if change_info.issue:
+    try:
+      description = GetIssueDescription(change_info.issue)
+    except urllib2.HTTPError, err:
+      if err.code == 404:
+        # The user deleted the issue in Rietveld, so forget the old issue id.
+        description = change_info.description
+        change_info.issue = ""
+        change_info.Save()
+      else:
+        ErrorExit("Error getting the description from Rietveld: " + err)
+  else:
+    description = change_info.description
+
+  other_files = GetFilesNotInCL()
+
+  separator1 = ("\n---All lines above this line become the description.\n"
+                "---Repository Root: " + GetRepositoryRoot() + "\n"
+                "---Paths in this changelist (" + change_info.name + "):\n")
+  separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
+  text = (description + separator1 + '\n' +
+          '\n'.join([f[0] + f[1] for f in change_info.files]) + separator2 +
+          '\n'.join([f[0] + f[1] for f in other_files]) + '\n')
+
+  handle, filename = tempfile.mkstemp(text=True)
+  os.write(handle, text)
+  os.close(handle)
+  
+  command = GetEditor() + " " + filename
+  os.system(command)
+
+  result = ReadFile(filename)
+  os.remove(filename)
+
+  if not result:
+    return
+
+  split_result = result.split(separator1, 1)
+  if len(split_result) != 2:
+    ErrorExit("Don't modify the text starting with ---!\n\n" + result)
+
+  new_description = split_result[0]
+  cl_files_text = split_result[1]
+  if new_description != description:
+    change_info.description = new_description
+    if change_info.issue:
+      # Update the Rietveld issue with the new description.
+      change_info.UpdateRietveldDescription()
+
+  new_cl_files = []
+  for line in cl_files_text.splitlines():
+    if not len(line):
+      continue
+    if line.startswith("---"):
+      break
+    status = line[:7]
+    file = line[7:]
+    new_cl_files.append((status, file))
+  change_info.files = new_cl_files
+
+  change_info.Save()
+  print change_info.name + " changelist saved."
+
+
+def Changes():
+  """Print all the changlists and their files."""
+  for cl in GetCLs():
+    change_info = LoadChangelistInfo(cl, True, True)
+    print "\n--- Changelist " + change_info.name + ":"
+    for file in change_info.files:
+      print "".join(file)
+
+
+def main(argv=None):
+  if argv is None:
+    argv = sys.argv
+  
+  if len(argv) == 1:
+    Help()
+    return 0;
+
+  # Create the directory where we store information about changelists if it
+  # doesn't exist.
+  if not os.path.exists(GetInfoDir()):
+    os.mkdir(GetInfoDir())
+
+  command = argv[1]
+  if command == "opened":
+    Opened()
+    return 0
+  if command == "nothave":
+    UnknownFiles(argv[2:])
+    return 0
+  if command == "changes":
+    Changes()
+    return 0
+  if command == "diff" and len(argv) == 2:
+    files = GetFilesNotInCL()
+    print GenerateDiff([os.path.join(GetRepositoryRoot(), x[1]) for x in files])
+    return 0
+
+  if len(argv) == 2:
+    if command == "change":
+      # Generate a random changelist name.
+      changename = GenerateChangeName()
+    elif command == "help":
+      Help()
+      return 0
+    else:
+      ErrorExit("Need a changelist name.")
+  else:
+    changename = argv[2]
+
+  fail_on_not_found = command != "change"
+  change_info = LoadChangelistInfo(changename, fail_on_not_found, True)
+
+  if command == "change":
+    Change(change_info)
+  elif command == "upload":
+    UploadCL(change_info, argv[3:])
+  elif command == "commit":
+    Commit(change_info)
+  elif command == "delete":
+    change_info.Delete()
+  elif command == "try":
+    TryChange(change_info, argv[3:])
+  else:
+    # Everything else that is passed into gcl we redirect to svn, after adding
+    # the files. This allows commands such as 'gcl diff xxx' to work.
+    args =["svn", command]
+    root = GetRepositoryRoot()
+    args.extend([os.path.join(root, x) for x in change_info.FileList()])
+    RunShell(args, True)
+  return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())