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