#!/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 logging import optparse import os import subprocess import sys class VersionInfo(object): def __init__(self, revision_id, full_revision_string): self.revision_id = revision_id self.revision = full_revision_string 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 as e: logging.error('Command %r failed: %s' % (' '.join(command), e)) return None def FetchGitRevision(directory, filter): """ Fetch the Git hash (and Cr-Commit-Position if any) for a given directory. Errors are swallowed. Returns: A VersionInfo object or None on error. """ hsh = '' git_args = ['log', '-1', '--format=%H'] if filter is not None: git_args.append('--grep=' + filter) proc = RunGitCommand(directory, git_args) if proc: output = proc.communicate()[0].strip() if proc.returncode == 0 and output: hsh = output else: logging.error('Git error: rc=%d, output=%r' % (proc.returncode, output)) if not hsh: return None pos = '' proc = RunGitCommand(directory, ['cat-file', 'commit', hsh]) 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 return VersionInfo(hsh, '%s-%s' % (hsh, pos)) def FetchVersionInfo(directory=None, filter=None): """ Returns the last change (as a VersionInfo object) from some appropriate revision control system. """ version_info = FetchGitRevision(directory, filter) if not version_info: version_info = VersionInfo('0', '0') 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. """ 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("-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-id-only", action='store_true', help="Output the revision as a VCS revision ID only (in " + "Git, a 40-character commit hash, excluding the " + "Cr-Commit-Position).") parser.add_option("--print-only", action='store_true', help="Just print the revision string. Overrides any " + "file-output-related options.") parser.add_option("-s", "--source-dir", metavar="DIR", help="Use repository in the given directory.") parser.add_option("", "--filter", metavar="REGEX", help="Only use log entries where the commit message " + "matches the supplied filter regex. Defaults to " + "'^Change-Id:' to suppress local commits.", default='^Change-Id:') opts, args = parser.parse_args(argv[1:]) logging.basicConfig(level=logging.WARNING) out_file = opts.output header = opts.header filter=opts.filter 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(directory=src_dir, filter=filter) revision_string = version_info.revision if opts.revision_id_only: revision_string = version_info.revision_id if opts.print_only: print revision_string else: contents = "LASTCHANGE=%s\n" % revision_string 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, revision_string)) return 0 if __name__ == '__main__': sys.exit(main())