Home | History | Annotate | Download | only in util
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 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 class VersionInfo(object):
     17   def __init__(self, url, root, revision):
     18     self.url = url
     19     self.root = root
     20     self.revision = revision
     21 
     22 
     23 def FetchSVNRevision(directory):
     24   """
     25   Fetch the Subversion branch and revision for a given directory.
     26 
     27   Errors are swallowed.
     28 
     29   Returns:
     30     a VersionInfo object or None on error.
     31   """
     32   try:
     33     proc = subprocess.Popen(['svn', 'info'],
     34                             stdout=subprocess.PIPE,
     35                             stderr=subprocess.PIPE,
     36                             cwd=directory,
     37                             shell=(sys.platform=='win32'))
     38   except OSError:
     39     # command is apparently either not installed or not executable.
     40     return None
     41   if not proc:
     42     return None
     43 
     44   attrs = {}
     45   for line in proc.stdout:
     46     line = line.strip()
     47     if not line:
     48       continue
     49     key, val = line.split(': ', 1)
     50     attrs[key] = val
     51 
     52   try:
     53     url = attrs['URL']
     54     root = attrs['Repository Root']
     55     revision = attrs['Revision']
     56   except KeyError:
     57     return None
     58 
     59   return VersionInfo(url, root, revision)
     60 
     61 
     62 def RunGitCommand(directory, command):
     63   """
     64   Launches git subcommand.
     65 
     66   Errors are swallowed.
     67 
     68   Returns:
     69     process object or None.
     70   """
     71   command = ['git'] + command
     72   # Force shell usage under cygwin & win32. This is a workaround for
     73   # mysterious loss of cwd while invoking cygwin's git.
     74   # We can't just pass shell=True to Popen, as under win32 this will
     75   # cause CMD to be used, while we explicitly want a cygwin shell.
     76   if sys.platform in ('cygwin', 'win32'):
     77     command = ['sh', '-c', ' '.join(command)]
     78   try:
     79     proc = subprocess.Popen(command,
     80                             stdout=subprocess.PIPE,
     81                             stderr=subprocess.PIPE,
     82                             cwd=directory)
     83     return proc
     84   except OSError:
     85     return None
     86 
     87 
     88 def FetchGitRevision(directory):
     89   """
     90   Fetch the Git hash for a given directory.
     91 
     92   Errors are swallowed.
     93 
     94   Returns:
     95     a VersionInfo object or None on error.
     96   """
     97   proc = RunGitCommand(directory, ['rev-parse', 'HEAD'])
     98   if proc:
     99     output = proc.communicate()[0].strip()
    100     if proc.returncode == 0 and output:
    101       return VersionInfo('git', 'git', output[:7])
    102   return None
    103 
    104 
    105 def IsGitSVN(directory):
    106   """
    107   Checks whether git-svn has been set up.
    108 
    109   Errors are swallowed.
    110 
    111   Returns:
    112     whether git-svn has been set up.
    113   """
    114   # To test whether git-svn has been set up, query the config for any
    115   # svn-related configuration.  This command exits with an error code
    116   # if there aren't any matches, so ignore its output.
    117   proc = RunGitCommand(directory, ['config', '--get-regexp', '^svn'])
    118   if proc:
    119     return (proc.wait() == 0)
    120   return False
    121 
    122 
    123 def FetchGitSVNURL(directory):
    124   """
    125   Fetch URL of SVN repository bound to git.
    126 
    127   Errors are swallowed.
    128 
    129   Returns:
    130     SVN URL.
    131   """
    132   if IsGitSVN(directory):
    133     proc = RunGitCommand(directory, ['svn', 'info', '--url'])
    134     if proc:
    135       output = proc.communicate()[0].strip()
    136       if proc.returncode == 0:
    137         match = re.search(r'^\w+://.*$', output, re.M)
    138         if match:
    139           return match.group(0)
    140   return ''
    141 
    142 
    143 def FetchGitSVNRoot(directory):
    144   """
    145   Fetch root of SVN repository bound to git.
    146 
    147   Errors are swallowed.
    148 
    149   Returns:
    150     SVN root repository.
    151   """
    152   if IsGitSVN(directory):
    153     git_command = ['config', '--get-regexp', '^svn-remote.svn.url$']
    154     proc = RunGitCommand(directory, git_command)
    155     if proc:
    156       output = proc.communicate()[0].strip()
    157       if proc.returncode == 0:
    158         # Zero return code implies presence of requested configuration variable.
    159         # Its value is second (last) field of output.
    160         match = re.search(r'\S+$', output)
    161         if match:
    162           return match.group(0)
    163   return ''
    164 
    165 
    166 def LookupGitSVNRevision(directory, depth):
    167   """
    168   Fetch the Git-SVN identifier for the local tree.
    169   Parses first |depth| commit messages.
    170 
    171   Errors are swallowed.
    172   """
    173   if not IsGitSVN(directory):
    174     return None
    175   git_re = re.compile(r'^\s*git-svn-id:\s+(\S+)@(\d+)')
    176   proc = RunGitCommand(directory, ['log', '-' + str(depth)])
    177   if proc:
    178     for line in proc.stdout:
    179       match = git_re.match(line)
    180       if match:
    181         id = match.group(2)
    182         if id:
    183           proc.stdout.close()  # Cut pipe for fast exit.
    184           return id
    185   return None
    186 
    187 
    188 def IsGitSVNDirty(directory):
    189   """
    190   Checks whether our git-svn tree contains clean trunk or some branch.
    191 
    192   Errors are swallowed.
    193   """
    194   # For git branches the last commit message is either
    195   # some local commit or a merge.
    196   return LookupGitSVNRevision(directory, 1) is None
    197 
    198 
    199 def FetchGitSVNRevision(directory):
    200   """
    201   Fetch the Git-SVN identifier for the local tree.
    202 
    203   Errors are swallowed.
    204   """
    205   # We assume that at least first 999 commit messages contain svn evidence.
    206   revision = LookupGitSVNRevision(directory, 999)
    207   if not revision:
    208     return None
    209   if IsGitSVNDirty(directory):
    210     revision = revision + '-dirty'
    211   url = FetchGitSVNURL(directory)
    212   root = FetchGitSVNRoot(directory)
    213   return VersionInfo(url, root, revision)
    214 
    215 
    216 def FetchVersionInfo(default_lastchange, directory=None):
    217   """
    218   Returns the last change (in the form of a branch, revision tuple),
    219   from some appropriate revision control system.
    220   """
    221   version_info = (FetchSVNRevision(directory) or
    222       FetchGitSVNRevision(directory) or FetchGitRevision(directory))
    223   if not version_info:
    224     if default_lastchange and os.path.exists(default_lastchange):
    225       revision = open(default_lastchange, 'r').read().strip()
    226       version_info = VersionInfo(None, None, revision)
    227     else:
    228       version_info = VersionInfo('unknown', '', '0')
    229   return version_info
    230 
    231 
    232 def WriteIfChanged(file_name, contents):
    233   """
    234   Writes the specified contents to the specified file_name
    235   iff the contents are different than the current contents.
    236   """
    237   try:
    238     old_contents = open(file_name, 'r').read()
    239   except EnvironmentError:
    240     pass
    241   else:
    242     if contents == old_contents:
    243       return
    244     os.unlink(file_name)
    245   open(file_name, 'w').write(contents)
    246 
    247 
    248 def main(argv=None):
    249   if argv is None:
    250     argv = sys.argv
    251 
    252   parser = optparse.OptionParser(usage="lastchange.py [options]")
    253   parser.add_option("-d", "--default-lastchange", metavar="FILE",
    254                     help="default last change input FILE")
    255   parser.add_option("-o", "--output", metavar="FILE",
    256                     help="write last change to FILE")
    257   parser.add_option("--revision-only", action='store_true',
    258                     help="just print the SVN revision number")
    259   opts, args = parser.parse_args(argv[1:])
    260 
    261   out_file = opts.output
    262 
    263   while len(args) and out_file is None:
    264     if out_file is None:
    265       out_file = args.pop(0)
    266   if args:
    267     sys.stderr.write('Unexpected arguments: %r\n\n' % args)
    268     parser.print_help()
    269     sys.exit(2)
    270 
    271   version_info = FetchVersionInfo(opts.default_lastchange)
    272 
    273   if opts.revision_only:
    274     print version_info.revision
    275   else:
    276     contents = "LASTCHANGE=%s\n" % version_info.revision
    277     if out_file:
    278       WriteIfChanged(out_file, contents)
    279     else:
    280       sys.stdout.write(contents)
    281 
    282   return 0
    283 
    284 
    285 if __name__ == '__main__':
    286   sys.exit(main())
    287