Index: build/util/lastchange.py |
diff --git a/build/util/lastchange.py b/build/util/lastchange.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..3f3ee4af47117c5d00143713217a27964eb226c6 |
--- /dev/null |
+++ b/build/util/lastchange.py |
@@ -0,0 +1,309 @@ |
+#!/usr/bin/env python |
+# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+""" |
+lastchange.py -- Chromium revision fetching utility. |
+""" |
+ |
+import re |
+import optparse |
+import os |
+import subprocess |
+import sys |
+ |
+_GIT_SVN_ID_REGEX = re.compile(r'.*git-svn-id:\s*([^@]*)@([0-9]+)', re.DOTALL) |
+ |
+class VersionInfo(object): |
+ def __init__(self, url, revision): |
+ self.url = url |
+ self.revision = revision |
+ |
+ |
+def FetchSVNRevision(directory, svn_url_regex): |
+ """ |
+ Fetch the Subversion branch and revision for a given directory. |
+ |
+ Errors are swallowed. |
+ |
+ Returns: |
+ A VersionInfo object or None on error. |
+ """ |
+ try: |
+ proc = subprocess.Popen(['svn', 'info'], |
+ stdout=subprocess.PIPE, |
+ stderr=subprocess.PIPE, |
+ cwd=directory, |
+ shell=(sys.platform=='win32')) |
+ except OSError: |
+ # command is apparently either not installed or not executable. |
+ return None |
+ if not proc: |
+ return None |
+ |
+ attrs = {} |
+ for line in proc.stdout: |
+ line = line.strip() |
+ if not line: |
+ continue |
+ key, val = line.split(': ', 1) |
+ attrs[key] = val |
+ |
+ try: |
+ match = svn_url_regex.search(attrs['URL']) |
+ if match: |
+ url = match.group(2) |
+ else: |
+ url = '' |
+ revision = attrs['Revision'] |
+ except KeyError: |
+ return None |
+ |
+ return VersionInfo(url, revision) |
+ |
+ |
+def RunGitCommand(directory, command): |
+ """ |
+ Launches git subcommand. |
+ |
+ Errors are swallowed. |
+ |
+ Returns: |
+ A process object or None. |
+ """ |
+ command = ['git'] + command |
+ # Force shell usage under cygwin. This is a workaround for |
+ # mysterious loss of cwd while invoking cygwin's git. |
+ # We can't just pass shell=True to Popen, as under win32 this will |
+ # cause CMD to be used, while we explicitly want a cygwin shell. |
+ if sys.platform == 'cygwin': |
+ command = ['sh', '-c', ' '.join(command)] |
+ try: |
+ proc = subprocess.Popen(command, |
+ stdout=subprocess.PIPE, |
+ stderr=subprocess.PIPE, |
+ cwd=directory, |
+ shell=(sys.platform=='win32')) |
+ return proc |
+ except OSError: |
+ return None |
+ |
+ |
+def FetchGitRevision(directory): |
+ """ |
+ Fetch the Git hash for a given directory. |
+ |
+ Errors are swallowed. |
+ |
+ Returns: |
+ A VersionInfo object or None on error. |
+ """ |
+ hsh = '' |
+ proc = RunGitCommand(directory, ['rev-parse', 'HEAD']) |
+ if proc: |
+ output = proc.communicate()[0].strip() |
+ if proc.returncode == 0 and output: |
+ hsh = output |
+ if not hsh: |
+ return None |
+ pos = '' |
+ proc = RunGitCommand(directory, ['cat-file', 'commit', 'HEAD']) |
+ if proc: |
+ output = proc.communicate()[0] |
+ if proc.returncode == 0 and output: |
+ for line in reversed(output.splitlines()): |
+ if line.startswith('Cr-Commit-Position:'): |
+ pos = line.rsplit()[-1].strip() |
+ break |
+ if not pos: |
+ return VersionInfo('git', hsh) |
+ return VersionInfo('git', '%s-%s' % (hsh, pos)) |
+ |
+ |
+def FetchGitSVNURLAndRevision(directory, svn_url_regex, go_deeper): |
+ """ |
+ Fetch the Subversion URL and revision through Git. |
+ |
+ Errors are swallowed. |
+ |
+ Returns: |
+ A tuple containing the Subversion URL and revision. |
+ """ |
+ git_args = ['log', '-1', '--format=%b'] |
+ if go_deeper: |
+ git_args.append('--grep=git-svn-id') |
+ proc = RunGitCommand(directory, git_args) |
+ if proc: |
+ output = proc.communicate()[0].strip() |
+ if proc.returncode == 0 and output: |
+ # Extract the latest SVN revision and the SVN URL. |
+ # The target line is the last "git-svn-id: ..." line like this: |
+ # git-svn-id: svn://svn.chromium.org/chrome/trunk/src@85528 0039d316.... |
+ match = _GIT_SVN_ID_REGEX.search(output) |
+ if match: |
+ revision = match.group(2) |
+ url_match = svn_url_regex.search(match.group(1)) |
+ if url_match: |
+ url = url_match.group(2) |
+ else: |
+ url = '' |
+ return url, revision |
+ return None, None |
+ |
+ |
+def FetchGitSVNRevision(directory, svn_url_regex, go_deeper): |
+ """ |
+ Fetch the Git-SVN identifier for the local tree. |
+ |
+ Errors are swallowed. |
+ """ |
+ url, revision = FetchGitSVNURLAndRevision(directory, svn_url_regex, go_deeper) |
+ if url and revision: |
+ return VersionInfo(url, revision) |
+ return None |
+ |
+ |
+def FetchVersionInfo(default_lastchange, directory=None, |
+ directory_regex_prior_to_src_url='chrome|blink|svn', |
+ go_deeper=False): |
+ """ |
+ Returns the last change (in the form of a branch, revision tuple), |
+ from some appropriate revision control system. |
+ """ |
+ svn_url_regex = re.compile( |
+ r'.*/(' + directory_regex_prior_to_src_url + r')(/.*)') |
+ |
+ version_info = (FetchSVNRevision(directory, svn_url_regex) or |
+ FetchGitSVNRevision(directory, svn_url_regex, go_deeper) or |
+ FetchGitRevision(directory)) |
+ if not version_info: |
+ if default_lastchange and os.path.exists(default_lastchange): |
+ revision = open(default_lastchange, 'r').read().strip() |
+ version_info = VersionInfo(None, revision) |
+ else: |
+ version_info = VersionInfo(None, None) |
+ return version_info |
+ |
+def GetHeaderGuard(path): |
+ """ |
+ Returns the header #define guard for the given file path. |
+ This treats everything after the last instance of "src/" as being a |
+ relevant part of the guard. If there is no "src/", then the entire path |
+ is used. |
+ """ |
+ src_index = path.rfind('src/') |
+ if src_index != -1: |
+ guard = path[src_index + 4:] |
+ else: |
+ guard = path |
+ guard = guard.upper() |
+ return guard.replace('/', '_').replace('.', '_').replace('\\', '_') + '_' |
+ |
+def GetHeaderContents(path, define, version): |
+ """ |
+ Returns what the contents of the header file should be that indicate the given |
+ revision. Note that the #define is specified as a string, even though it's |
+ currently always a SVN revision number, in case we need to move to git hashes. |
+ """ |
+ header_guard = GetHeaderGuard(path) |
+ |
+ header_contents = """/* Generated by lastchange.py, do not edit.*/ |
+ |
+#ifndef %(header_guard)s |
+#define %(header_guard)s |
+ |
+#define %(define)s "%(version)s" |
+ |
+#endif // %(header_guard)s |
+""" |
+ header_contents = header_contents % { 'header_guard': header_guard, |
+ 'define': define, |
+ 'version': version } |
+ return header_contents |
+ |
+def WriteIfChanged(file_name, contents): |
+ """ |
+ Writes the specified contents to the specified file_name |
+ iff the contents are different than the current contents. |
+ """ |
+ try: |
+ old_contents = open(file_name, 'r').read() |
+ except EnvironmentError: |
+ pass |
+ else: |
+ if contents == old_contents: |
+ return |
+ os.unlink(file_name) |
+ open(file_name, 'w').write(contents) |
+ |
+ |
+def main(argv=None): |
+ if argv is None: |
+ argv = sys.argv |
+ |
+ parser = optparse.OptionParser(usage="lastchange.py [options]") |
+ parser.add_option("-d", "--default-lastchange", metavar="FILE", |
+ help="Default last change input FILE.") |
+ parser.add_option("-m", "--version-macro", |
+ help="Name of C #define when using --header. Defaults to " + |
+ "LAST_CHANGE.", |
+ default="LAST_CHANGE") |
+ parser.add_option("-o", "--output", metavar="FILE", |
+ help="Write last change to FILE. " + |
+ "Can be combined with --header to write both files.") |
+ parser.add_option("", "--header", metavar="FILE", |
+ help="Write last change to FILE as a C/C++ header. " + |
+ "Can be combined with --output to write both files.") |
+ parser.add_option("--revision-only", action='store_true', |
+ help="Just print the SVN revision number. Overrides any " + |
+ "file-output-related options.") |
+ parser.add_option("-s", "--source-dir", metavar="DIR", |
+ help="Use repository in the given directory.") |
+ parser.add_option("--git-svn-go-deeper", action='store_true', |
+ help="In a Git-SVN repo, dig down to the last committed " + |
+ "SVN change (historic behaviour).") |
+ opts, args = parser.parse_args(argv[1:]) |
+ |
+ out_file = opts.output |
+ header = opts.header |
+ |
+ while len(args) and out_file is None: |
+ if out_file is None: |
+ out_file = args.pop(0) |
+ if args: |
+ sys.stderr.write('Unexpected arguments: %r\n\n' % args) |
+ parser.print_help() |
+ sys.exit(2) |
+ |
+ if opts.source_dir: |
+ src_dir = opts.source_dir |
+ else: |
+ src_dir = os.path.dirname(os.path.abspath(__file__)) |
+ |
+ version_info = FetchVersionInfo(opts.default_lastchange, |
+ directory=src_dir, |
+ go_deeper=opts.git_svn_go_deeper) |
+ |
+ if version_info.revision == None: |
+ version_info.revision = '0' |
+ |
+ if opts.revision_only: |
+ print version_info.revision |
+ else: |
+ contents = "LASTCHANGE=%s\n" % version_info.revision |
+ if not out_file and not opts.header: |
+ sys.stdout.write(contents) |
+ else: |
+ if out_file: |
+ WriteIfChanged(out_file, contents) |
+ if header: |
+ WriteIfChanged(header, |
+ GetHeaderContents(header, opts.version_macro, |
+ version_info.revision)) |
+ |
+ return 0 |
+ |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |