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""" 7lastchange.py -- Chromium revision fetching utility. 8""" 9 10import re 11import optparse 12import os 13import subprocess 14import sys 15 16_GIT_SVN_ID_REGEX = re.compile(r'.*git-svn-id:\s*([^@]*)@([0-9]+)', re.DOTALL) 17 18class VersionInfo(object): 19 def __init__(self, url, revision): 20 self.url = url 21 self.revision = revision 22 23 24def 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 66def 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 93def 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 110def 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 139def 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 151def 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 171def GetHeaderGuard(path): 172 """ 173 Returns the header #define guard for the given file path. 174 This treats everything after the last instance of "src/" as being a 175 relevant part of the guard. If there is no "src/", then the entire path 176 is used. 177 """ 178 src_index = path.rfind('src/') 179 if src_index != -1: 180 guard = path[src_index + 4:] 181 else: 182 guard = path 183 guard = guard.upper() 184 return guard.replace('/', '_').replace('.', '_').replace('\\', '_') + '_' 185 186def GetHeaderContents(path, define, version): 187 """ 188 Returns what the contents of the header file should be that indicate the given 189 revision. Note that the #define is specified as a string, even though it's 190 currently always a SVN revision number, in case we need to move to git hashes. 191 """ 192 header_guard = GetHeaderGuard(path) 193 194 header_contents = """/* Generated by lastchange.py, do not edit.*/ 195 196#ifndef %(header_guard)s 197#define %(header_guard)s 198 199#define %(define)s "%(version)s" 200 201#endif // %(header_guard)s 202""" 203 header_contents = header_contents % { 'header_guard': header_guard, 204 'define': define, 205 'version': version } 206 return header_contents 207 208def WriteIfChanged(file_name, contents): 209 """ 210 Writes the specified contents to the specified file_name 211 iff the contents are different than the current contents. 212 """ 213 try: 214 old_contents = open(file_name, 'r').read() 215 except EnvironmentError: 216 pass 217 else: 218 if contents == old_contents: 219 return 220 os.unlink(file_name) 221 open(file_name, 'w').write(contents) 222 223 224def main(argv=None): 225 if argv is None: 226 argv = sys.argv 227 228 parser = optparse.OptionParser(usage="lastchange.py [options]") 229 parser.add_option("-d", "--default-lastchange", metavar="FILE", 230 help="Default last change input FILE.") 231 parser.add_option("-m", "--version-macro", 232 help="Name of C #define when using --header. Defaults to " + 233 "LAST_CHANGE.", 234 default="LAST_CHANGE") 235 parser.add_option("-o", "--output", metavar="FILE", 236 help="Write last change to FILE. " + 237 "Can be combined with --header to write both files.") 238 parser.add_option("", "--header", metavar="FILE", 239 help="Write last change to FILE as a C/C++ header. " + 240 "Can be combined with --output to write both files.") 241 parser.add_option("--revision-only", action='store_true', 242 help="Just print the SVN revision number. Overrides any " + 243 "file-output-related options.") 244 parser.add_option("-s", "--source-dir", metavar="DIR", 245 help="Use repository in the given directory.") 246 opts, args = parser.parse_args(argv[1:]) 247 248 out_file = opts.output 249 header = opts.header 250 251 while len(args) and out_file is None: 252 if out_file is None: 253 out_file = args.pop(0) 254 if args: 255 sys.stderr.write('Unexpected arguments: %r\n\n' % args) 256 parser.print_help() 257 sys.exit(2) 258 259 if opts.source_dir: 260 src_dir = opts.source_dir 261 else: 262 src_dir = os.path.dirname(os.path.abspath(__file__)) 263 264 version_info = FetchVersionInfo(opts.default_lastchange, src_dir) 265 266 if version_info.revision == None: 267 version_info.revision = '0' 268 269 if opts.revision_only: 270 print version_info.revision 271 else: 272 contents = "LASTCHANGE=%s\n" % version_info.revision 273 if not out_file and not opts.header: 274 sys.stdout.write(contents) 275 else: 276 if out_file: 277 WriteIfChanged(out_file, contents) 278 if header: 279 WriteIfChanged(header, 280 GetHeaderContents(header, opts.version_macro, 281 version_info.revision)) 282 283 return 0 284 285 286if __name__ == '__main__': 287 sys.exit(main()) 288