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 171 172def WriteIfChanged(file_name, contents): 173 """ 174 Writes the specified contents to the specified file_name 175 iff the contents are different than the current contents. 176 """ 177 try: 178 old_contents = open(file_name, 'r').read() 179 except EnvironmentError: 180 pass 181 else: 182 if contents == old_contents: 183 return 184 os.unlink(file_name) 185 open(file_name, 'w').write(contents) 186 187 188def main(argv=None): 189 if argv is None: 190 argv = sys.argv 191 192 parser = optparse.OptionParser(usage="lastchange.py [options]") 193 parser.add_option("-d", "--default-lastchange", metavar="FILE", 194 help="default last change input FILE") 195 parser.add_option("-o", "--output", metavar="FILE", 196 help="write last change to FILE") 197 parser.add_option("--revision-only", action='store_true', 198 help="just print the SVN revision number") 199 parser.add_option("-s", "--source-dir", metavar="DIR", 200 help="use repository in the given directory") 201 opts, args = parser.parse_args(argv[1:]) 202 203 out_file = opts.output 204 205 while len(args) and out_file is None: 206 if out_file is None: 207 out_file = args.pop(0) 208 if args: 209 sys.stderr.write('Unexpected arguments: %r\n\n' % args) 210 parser.print_help() 211 sys.exit(2) 212 213 if opts.source_dir: 214 src_dir = opts.source_dir 215 else: 216 src_dir = os.path.dirname(os.path.abspath(__file__)) 217 218 version_info = FetchVersionInfo(opts.default_lastchange, src_dir) 219 220 if version_info.revision == None: 221 version_info.revision = '0' 222 223 if opts.revision_only: 224 print version_info.revision 225 else: 226 contents = "LASTCHANGE=%s\n" % version_info.revision 227 if out_file: 228 WriteIfChanged(out_file, contents) 229 else: 230 sys.stdout.write(contents) 231 232 return 0 233 234 235if __name__ == '__main__': 236 sys.exit(main()) 237