|
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 """ |
|
7 lastchange.py -- Chromium revision fetching utility. |
|
8 """ |
|
9 |
|
10 import re |
|
11 import optparse |
|
12 import os |
|
13 import subprocess |
|
14 import sys |
|
15 |
|
16 _GIT_SVN_ID_REGEX = re.compile(r'.*git-svn-id:\s*([^@]*)@([0-9]+)', re.DOTALL) |
|
17 |
|
18 class VersionInfo(object): |
|
19 def __init__(self, url, revision): |
|
20 self.url = url |
|
21 self.revision = revision |
|
22 |
|
23 |
|
24 def 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 |
|
66 def 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 |
|
93 def 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 |
|
110 def 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 |
|
139 def 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 |
|
151 def FetchVersionInfo(default_lastchange, directory=None, |
|
152 directory_regex_prior_to_src_url='chrome|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 |
|
172 def 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 |
|
188 def 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 opts, args = parser.parse_args(argv[1:]) |
|
200 |
|
201 out_file = opts.output |
|
202 |
|
203 while len(args) and out_file is None: |
|
204 if out_file is None: |
|
205 out_file = args.pop(0) |
|
206 if args: |
|
207 sys.stderr.write('Unexpected arguments: %r\n\n' % args) |
|
208 parser.print_help() |
|
209 sys.exit(2) |
|
210 |
|
211 version_info = FetchVersionInfo(opts.default_lastchange, |
|
212 os.path.dirname(sys.argv[0])) |
|
213 |
|
214 if version_info.revision == None: |
|
215 version_info.revision = '0' |
|
216 |
|
217 if opts.revision_only: |
|
218 print version_info.revision |
|
219 else: |
|
220 contents = "LASTCHANGE=%s\n" % version_info.revision |
|
221 if out_file: |
|
222 WriteIfChanged(out_file, contents) |
|
223 else: |
|
224 sys.stdout.write(contents) |
|
225 |
|
226 return 0 |
|
227 |
|
228 |
|
229 if __name__ == '__main__': |
|
230 sys.exit(main()) |