michael@0: #!/usr/bin/env python michael@0: # Copyright (c) 2012 The Chromium Authors. All rights reserved. michael@0: # Use of this source code is governed by a BSD-style license that can be michael@0: # found in the LICENSE file. michael@0: michael@0: """ michael@0: lastchange.py -- Chromium revision fetching utility. michael@0: """ michael@0: michael@0: import re michael@0: import optparse michael@0: import os michael@0: import subprocess michael@0: import sys michael@0: michael@0: _GIT_SVN_ID_REGEX = re.compile(r'.*git-svn-id:\s*([^@]*)@([0-9]+)', re.DOTALL) michael@0: michael@0: class VersionInfo(object): michael@0: def __init__(self, url, revision): michael@0: self.url = url michael@0: self.revision = revision michael@0: michael@0: michael@0: def FetchSVNRevision(directory, svn_url_regex): michael@0: """ michael@0: Fetch the Subversion branch and revision for a given directory. michael@0: michael@0: Errors are swallowed. michael@0: michael@0: Returns: michael@0: A VersionInfo object or None on error. michael@0: """ michael@0: try: michael@0: proc = subprocess.Popen(['svn', 'info'], michael@0: stdout=subprocess.PIPE, michael@0: stderr=subprocess.PIPE, michael@0: cwd=directory, michael@0: shell=(sys.platform=='win32')) michael@0: except OSError: michael@0: # command is apparently either not installed or not executable. michael@0: return None michael@0: if not proc: michael@0: return None michael@0: michael@0: attrs = {} michael@0: for line in proc.stdout: michael@0: line = line.strip() michael@0: if not line: michael@0: continue michael@0: key, val = line.split(': ', 1) michael@0: attrs[key] = val michael@0: michael@0: try: michael@0: match = svn_url_regex.search(attrs['URL']) michael@0: if match: michael@0: url = match.group(2) michael@0: else: michael@0: url = '' michael@0: revision = attrs['Revision'] michael@0: except KeyError: michael@0: return None michael@0: michael@0: return VersionInfo(url, revision) michael@0: michael@0: michael@0: def RunGitCommand(directory, command): michael@0: """ michael@0: Launches git subcommand. michael@0: michael@0: Errors are swallowed. michael@0: michael@0: Returns: michael@0: A process object or None. michael@0: """ michael@0: command = ['git'] + command michael@0: # Force shell usage under cygwin. This is a workaround for michael@0: # mysterious loss of cwd while invoking cygwin's git. michael@0: # We can't just pass shell=True to Popen, as under win32 this will michael@0: # cause CMD to be used, while we explicitly want a cygwin shell. michael@0: if sys.platform == 'cygwin': michael@0: command = ['sh', '-c', ' '.join(command)] michael@0: try: michael@0: proc = subprocess.Popen(command, michael@0: stdout=subprocess.PIPE, michael@0: stderr=subprocess.PIPE, michael@0: cwd=directory, michael@0: shell=(sys.platform=='win32')) michael@0: return proc michael@0: except OSError: michael@0: return None michael@0: michael@0: michael@0: def FetchGitRevision(directory): michael@0: """ michael@0: Fetch the Git hash for a given directory. michael@0: michael@0: Errors are swallowed. michael@0: michael@0: Returns: michael@0: A VersionInfo object or None on error. michael@0: """ michael@0: proc = RunGitCommand(directory, ['rev-parse', 'HEAD']) michael@0: if proc: michael@0: output = proc.communicate()[0].strip() michael@0: if proc.returncode == 0 and output: michael@0: return VersionInfo('git', output[:7]) michael@0: return None michael@0: michael@0: michael@0: def FetchGitSVNURLAndRevision(directory, svn_url_regex): michael@0: """ michael@0: Fetch the Subversion URL and revision through Git. michael@0: michael@0: Errors are swallowed. michael@0: michael@0: Returns: michael@0: A tuple containing the Subversion URL and revision. michael@0: """ michael@0: proc = RunGitCommand(directory, ['log', '-1', michael@0: '--grep=git-svn-id', '--format=%b']) michael@0: if proc: michael@0: output = proc.communicate()[0].strip() michael@0: if proc.returncode == 0 and output: michael@0: # Extract the latest SVN revision and the SVN URL. michael@0: # The target line is the last "git-svn-id: ..." line like this: michael@0: # git-svn-id: svn://svn.chromium.org/chrome/trunk/src@85528 0039d316.... michael@0: match = _GIT_SVN_ID_REGEX.search(output) michael@0: if match: michael@0: revision = match.group(2) michael@0: url_match = svn_url_regex.search(match.group(1)) michael@0: if url_match: michael@0: url = url_match.group(2) michael@0: else: michael@0: url = '' michael@0: return url, revision michael@0: return None, None michael@0: michael@0: michael@0: def FetchGitSVNRevision(directory, svn_url_regex): michael@0: """ michael@0: Fetch the Git-SVN identifier for the local tree. michael@0: michael@0: Errors are swallowed. michael@0: """ michael@0: url, revision = FetchGitSVNURLAndRevision(directory, svn_url_regex) michael@0: if url and revision: michael@0: return VersionInfo(url, revision) michael@0: return None michael@0: michael@0: michael@0: def FetchVersionInfo(default_lastchange, directory=None, michael@0: directory_regex_prior_to_src_url='chrome|svn'): michael@0: """ michael@0: Returns the last change (in the form of a branch, revision tuple), michael@0: from some appropriate revision control system. michael@0: """ michael@0: svn_url_regex = re.compile( michael@0: r'.*/(' + directory_regex_prior_to_src_url + r')(/.*)') michael@0: michael@0: version_info = (FetchSVNRevision(directory, svn_url_regex) or michael@0: FetchGitSVNRevision(directory, svn_url_regex) or michael@0: FetchGitRevision(directory)) michael@0: if not version_info: michael@0: if default_lastchange and os.path.exists(default_lastchange): michael@0: revision = open(default_lastchange, 'r').read().strip() michael@0: version_info = VersionInfo(None, revision) michael@0: else: michael@0: version_info = VersionInfo(None, None) michael@0: return version_info michael@0: michael@0: michael@0: def WriteIfChanged(file_name, contents): michael@0: """ michael@0: Writes the specified contents to the specified file_name michael@0: iff the contents are different than the current contents. michael@0: """ michael@0: try: michael@0: old_contents = open(file_name, 'r').read() michael@0: except EnvironmentError: michael@0: pass michael@0: else: michael@0: if contents == old_contents: michael@0: return michael@0: os.unlink(file_name) michael@0: open(file_name, 'w').write(contents) michael@0: michael@0: michael@0: def main(argv=None): michael@0: if argv is None: michael@0: argv = sys.argv michael@0: michael@0: parser = optparse.OptionParser(usage="lastchange.py [options]") michael@0: parser.add_option("-d", "--default-lastchange", metavar="FILE", michael@0: help="default last change input FILE") michael@0: parser.add_option("-o", "--output", metavar="FILE", michael@0: help="write last change to FILE") michael@0: parser.add_option("--revision-only", action='store_true', michael@0: help="just print the SVN revision number") michael@0: opts, args = parser.parse_args(argv[1:]) michael@0: michael@0: out_file = opts.output michael@0: michael@0: while len(args) and out_file is None: michael@0: if out_file is None: michael@0: out_file = args.pop(0) michael@0: if args: michael@0: sys.stderr.write('Unexpected arguments: %r\n\n' % args) michael@0: parser.print_help() michael@0: sys.exit(2) michael@0: michael@0: version_info = FetchVersionInfo(opts.default_lastchange, michael@0: os.path.dirname(sys.argv[0])) michael@0: michael@0: if version_info.revision == None: michael@0: version_info.revision = '0' michael@0: michael@0: if opts.revision_only: michael@0: print version_info.revision michael@0: else: michael@0: contents = "LASTCHANGE=%s\n" % version_info.revision michael@0: if out_file: michael@0: WriteIfChanged(out_file, contents) michael@0: else: michael@0: sys.stdout.write(contents) michael@0: michael@0: return 0 michael@0: michael@0: michael@0: if __name__ == '__main__': michael@0: sys.exit(main())