Home | History | Annotate | Download | only in util
      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 lastchange.py -- Chromium revision fetching utility.
      8 """
      9 
     10 import re
     11 import optparse
     12 import os
     13 import subprocess
     14 import sys
     15 
     16 _GIT_SVN_ID_REGEX = re.compile(r'.*git-svn-id:\s*([^@]*)@([0-9]+)', re.DOTALL)
     17 
     18 class VersionInfo(object):
     19   def __init__(self, url, revision):
     20     self.url = url
     21     self.revision = revision
     22 
     23 
     24 def FetchSVNRevision(directory, svn_url_regex):
     25   """
     26   Fetch the Subversion branch and revision for a given directory.
     27 
     28   Errors are swallowed.
     29 
     30   Returns:
     31     A VersionInfo object or None on error.
     32   """
     33   try:
     34     proc = subprocess.Popen(['svn', 'info'],
     35                             stdout=subprocess.PIPE,
     36                             stderr=subprocess.PIPE,
     37                             cwd=directory,
     38                             shell=(sys.platform=='win32'))
     39   except OSError:
     40     # command is apparently either not installed or not executable.
     41     return None
     42   if not proc:
     43     return None
     44 
     45   attrs = {}
     46   for line in proc.stdout:
     47     line = line.strip()
     48     if not line:
     49       continue
     50     key, val = line.split(': ', 1)
     51     attrs[key] = val
     52 
     53   try:
     54     match = svn_url_regex.search(attrs['URL'])
     55     if match:
     56       url = match.group(2)
     57     else:
     58       url = ''
     59     revision = attrs['Revision']
     60   except KeyError:
     61     return None
     62 
     63   return VersionInfo(url, revision)
     64 
     65 
     66 def RunGitCommand(directory, command):
     67   """
     68   Launches git subcommand.
     69 
     70   Errors are swallowed.
     71 
     72   Returns:
     73     A process object or None.
     74   """
     75   command = ['git'] + command
     76   # Force shell usage under cygwin. This is a workaround for
     77   # mysterious loss of cwd while invoking cygwin's git.
     78   # We can't just pass shell=True to Popen, as under win32 this will
     79   # cause CMD to be used, while we explicitly want a cygwin shell.
     80   if sys.platform == 'cygwin':
     81     command = ['sh', '-c', ' '.join(command)]
     82   try:
     83     proc = subprocess.Popen(command,
     84                             stdout=subprocess.PIPE,
     85                             stderr=subprocess.PIPE,
     86                             cwd=directory,
     87                             shell=(sys.platform=='win32'))
     88     return proc
     89   except OSError:
     90     return None
     91 
     92 
     93 def FetchGitRevision(directory):
     94   """
     95   Fetch the Git hash for a given directory.
     96 
     97   Errors are swallowed.
     98 
     99   Returns:
    100     A VersionInfo object or None on error.
    101   """
    102   proc = RunGitCommand(directory, ['rev-parse', 'HEAD'])
    103   if proc:
    104     output = proc.communicate()[0].strip()
    105     if proc.returncode == 0 and output:
    106       return VersionInfo('git', output[:7])
    107   return None
    108 
    109 
    110 def FetchGitSVNURLAndRevision(directory, svn_url_regex):
    111   """
    112   Fetch the Subversion URL and revision through Git.
    113 
    114   Errors are swallowed.
    115 
    116   Returns:
    117     A tuple containing the Subversion URL and revision.
    118   """
    119   proc = RunGitCommand(directory, ['log', '-1',
    120                                    '--grep=git-svn-id', '--format=%b'])
    121   if proc:
    122     output = proc.communicate()[0].strip()
    123     if proc.returncode == 0 and output:
    124       # Extract the latest SVN revision and the SVN URL.
    125       # The target line is the last "git-svn-id: ..." line like this:
    126       # git-svn-id: svn://svn.chromium.org/chrome/trunk/src@85528 0039d316....
    127       match = _GIT_SVN_ID_REGEX.search(output)
    128       if match:
    129         revision = match.group(2)
    130         url_match = svn_url_regex.search(match.group(1))
    131         if url_match:
    132           url = url_match.group(2)
    133         else:
    134           url = ''
    135         return url, revision
    136   return None, None
    137 
    138 
    139 def FetchGitSVNRevision(directory, svn_url_regex):
    140   """
    141   Fetch the Git-SVN identifier for the local tree.
    142 
    143   Errors are swallowed.
    144   """
    145   url, revision = FetchGitSVNURLAndRevision(directory, svn_url_regex)
    146   if url and revision:
    147     return VersionInfo(url, revision)
    148   return None
    149 
    150 
    151 def FetchVersionInfo(default_lastchange, directory=None,
    152                      directory_regex_prior_to_src_url='chrome|blink|svn'):
    153   """
    154   Returns the last change (in the form of a branch, revision tuple),
    155   from some appropriate revision control system.
    156   """
    157   svn_url_regex = re.compile(
    158       r'.*/(' + directory_regex_prior_to_src_url + r')(/.*)')
    159 
    160   version_info = (FetchSVNRevision(directory, svn_url_regex) or
    161                   FetchGitSVNRevision(directory, svn_url_regex) or
    162                   FetchGitRevision(directory))
    163   if not version_info:
    164     if default_lastchange and os.path.exists(default_lastchange):
    165       revision = open(default_lastchange, 'r').read().strip()
    166       version_info = VersionInfo(None, revision)
    167     else:
    168       version_info = VersionInfo(None, None)
    169   return version_info
    170 
    171 
    172 def WriteIfChanged(file_name, contents):
    173   """
    174   Writes the specified contents to the specified file_name
    175   iff the contents are different than the current contents.
    176   """
    177   try:
    178     old_contents = open(file_name, 'r').read()
    179   except EnvironmentError:
    180     pass
    181   else:
    182     if contents == old_contents:
    183       return
    184     os.unlink(file_name)
    185   open(file_name, 'w').write(contents)
    186 
    187 
    188 def main(argv=None):
    189   if argv is None:
    190     argv = sys.argv
    191 
    192   parser = optparse.OptionParser(usage="lastchange.py [options]")
    193   parser.add_option("-d", "--default-lastchange", metavar="FILE",
    194                     help="default last change input FILE")
    195   parser.add_option("-o", "--output", metavar="FILE",
    196                     help="write last change to FILE")
    197   parser.add_option("--revision-only", action='store_true',
    198                     help="just print the SVN revision number")
    199   parser.add_option("-s", "--source-dir", metavar="DIR",
    200                     help="use repository in the given directory")
    201   opts, args = parser.parse_args(argv[1:])
    202 
    203   out_file = opts.output
    204 
    205   while len(args) and out_file is None:
    206     if out_file is None:
    207       out_file = args.pop(0)
    208   if args:
    209     sys.stderr.write('Unexpected arguments: %r\n\n' % args)
    210     parser.print_help()
    211     sys.exit(2)
    212 
    213   if opts.source_dir:
    214     src_dir = opts.source_dir
    215   else:
    216     src_dir = os.path.dirname(os.path.abspath(__file__))
    217 
    218   version_info = FetchVersionInfo(opts.default_lastchange, src_dir)
    219 
    220   if version_info.revision == None:
    221     version_info.revision = '0'
    222 
    223   if opts.revision_only:
    224     print version_info.revision
    225   else:
    226     contents = "LASTCHANGE=%s\n" % version_info.revision
    227     if out_file:
    228       WriteIfChanged(out_file, contents)
    229     else:
    230       sys.stdout.write(contents)
    231 
    232   return 0
    233 
    234 
    235 if __name__ == '__main__':
    236   sys.exit(main())
    237