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 hsh = '' 103 proc = RunGitCommand(directory, ['rev-parse', 'HEAD']) 104 if proc: 105 output = proc.communicate()[0].strip() 106 if proc.returncode == 0 and output: 107 hsh = output 108 if not hsh: 109 return None 110 pos = '' 111 proc = RunGitCommand(directory, ['show', '-s', '--format=%B', 'HEAD']) 112 if proc: 113 output = proc.communicate()[0] 114 if proc.returncode == 0 and output: 115 for line in reversed(output.splitlines()): 116 if line.startswith('Cr-Commit-Position:'): 117 pos = line.rsplit()[-1].strip() 118 if not pos: 119 return VersionInfo('git', hsh) 120 return VersionInfo('git', '%s-%s' % (hsh, pos)) 121 122 123def FetchGitSVNURLAndRevision(directory, svn_url_regex): 124 """ 125 Fetch the Subversion URL and revision through Git. 126 127 Errors are swallowed. 128 129 Returns: 130 A tuple containing the Subversion URL and revision. 131 """ 132 proc = RunGitCommand(directory, ['log', '-1', '--format=%b']) 133 if proc: 134 output = proc.communicate()[0].strip() 135 if proc.returncode == 0 and output: 136 # Extract the latest SVN revision and the SVN URL. 137 # The target line is the last "git-svn-id: ..." line like this: 138 # git-svn-id: svn://svn.chromium.org/chrome/trunk/src@85528 0039d316.... 139 match = _GIT_SVN_ID_REGEX.search(output) 140 if match: 141 revision = match.group(2) 142 url_match = svn_url_regex.search(match.group(1)) 143 if url_match: 144 url = url_match.group(2) 145 else: 146 url = '' 147 return url, revision 148 return None, None 149 150 151def FetchGitSVNRevision(directory, svn_url_regex): 152 """ 153 Fetch the Git-SVN identifier for the local tree. 154 155 Errors are swallowed. 156 """ 157 url, revision = FetchGitSVNURLAndRevision(directory, svn_url_regex) 158 if url and revision: 159 return VersionInfo(url, revision) 160 return None 161 162 163def FetchVersionInfo(default_lastchange, directory=None, 164 directory_regex_prior_to_src_url='chrome|blink|svn'): 165 """ 166 Returns the last change (in the form of a branch, revision tuple), 167 from some appropriate revision control system. 168 """ 169 svn_url_regex = re.compile( 170 r'.*/(' + directory_regex_prior_to_src_url + r')(/.*)') 171 172 version_info = (FetchSVNRevision(directory, svn_url_regex) or 173 FetchGitSVNRevision(directory, svn_url_regex) or 174 FetchGitRevision(directory)) 175 if not version_info: 176 if default_lastchange and os.path.exists(default_lastchange): 177 revision = open(default_lastchange, 'r').read().strip() 178 version_info = VersionInfo(None, revision) 179 else: 180 version_info = VersionInfo(None, None) 181 return version_info 182 183def GetHeaderGuard(path): 184 """ 185 Returns the header #define guard for the given file path. 186 This treats everything after the last instance of "src/" as being a 187 relevant part of the guard. If there is no "src/", then the entire path 188 is used. 189 """ 190 src_index = path.rfind('src/') 191 if src_index != -1: 192 guard = path[src_index + 4:] 193 else: 194 guard = path 195 guard = guard.upper() 196 return guard.replace('/', '_').replace('.', '_').replace('\\', '_') + '_' 197 198def GetHeaderContents(path, define, version): 199 """ 200 Returns what the contents of the header file should be that indicate the given 201 revision. Note that the #define is specified as a string, even though it's 202 currently always a SVN revision number, in case we need to move to git hashes. 203 """ 204 header_guard = GetHeaderGuard(path) 205 206 header_contents = """/* Generated by lastchange.py, do not edit.*/ 207 208#ifndef %(header_guard)s 209#define %(header_guard)s 210 211#define %(define)s "%(version)s" 212 213#endif // %(header_guard)s 214""" 215 header_contents = header_contents % { 'header_guard': header_guard, 216 'define': define, 217 'version': version } 218 return header_contents 219 220def WriteIfChanged(file_name, contents): 221 """ 222 Writes the specified contents to the specified file_name 223 iff the contents are different than the current contents. 224 """ 225 try: 226 old_contents = open(file_name, 'r').read() 227 except EnvironmentError: 228 pass 229 else: 230 if contents == old_contents: 231 return 232 os.unlink(file_name) 233 open(file_name, 'w').write(contents) 234 235 236def main(argv=None): 237 if argv is None: 238 argv = sys.argv 239 240 parser = optparse.OptionParser(usage="lastchange.py [options]") 241 parser.add_option("-d", "--default-lastchange", metavar="FILE", 242 help="Default last change input FILE.") 243 parser.add_option("-m", "--version-macro", 244 help="Name of C #define when using --header. Defaults to " + 245 "LAST_CHANGE.", 246 default="LAST_CHANGE") 247 parser.add_option("-o", "--output", metavar="FILE", 248 help="Write last change to FILE. " + 249 "Can be combined with --header to write both files.") 250 parser.add_option("", "--header", metavar="FILE", 251 help="Write last change to FILE as a C/C++ header. " + 252 "Can be combined with --output to write both files.") 253 parser.add_option("--revision-only", action='store_true', 254 help="Just print the SVN revision number. Overrides any " + 255 "file-output-related options.") 256 parser.add_option("-s", "--source-dir", metavar="DIR", 257 help="Use repository in the given directory.") 258 opts, args = parser.parse_args(argv[1:]) 259 260 out_file = opts.output 261 header = opts.header 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 if opts.source_dir: 272 src_dir = opts.source_dir 273 else: 274 src_dir = os.path.dirname(os.path.abspath(__file__)) 275 276 version_info = FetchVersionInfo(opts.default_lastchange, src_dir) 277 278 if version_info.revision == None: 279 version_info.revision = '0' 280 281 if opts.revision_only: 282 print version_info.revision 283 else: 284 contents = "LASTCHANGE=%s\n" % version_info.revision 285 if not out_file and not opts.header: 286 sys.stdout.write(contents) 287 else: 288 if out_file: 289 WriteIfChanged(out_file, contents) 290 if header: 291 WriteIfChanged(header, 292 GetHeaderContents(header, opts.version_macro, 293 version_info.revision)) 294 295 return 0 296 297 298if __name__ == '__main__': 299 sys.exit(main()) 300