blob: 7a6f66422e7781cb7e6f05a460b4f9c5206283ea [file] [log] [blame]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00001# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""SCM-specific utility classes."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00006
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00007import glob
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00008import os
9import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000010import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000011import subprocess
12import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000013import tempfile
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000014import xml.dom.minidom
15
16import gclient_utils
17
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000018def ValidateEmail(email):
19 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
20 is not None)
21
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000022
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000023def GetCasedPath(path):
24 """Elcheapos way to get the real path case on Windows."""
25 if sys.platform.startswith('win') and os.path.exists(path):
26 # Reconstruct the path.
27 path = os.path.abspath(path)
28 paths = path.split('\\')
29 for i in range(len(paths)):
30 if i == 0:
31 # Skip drive letter.
32 continue
33 subpath = '\\'.join(paths[:i+1])
34 prev = len('\\'.join(paths[:i]))
35 # glob.glob will return the cased path for the last item only. This is why
36 # we are calling it in a loop. Extract the data we want and put it back
37 # into the list.
38 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
39 path = '\\'.join(paths)
40 return path
41
42
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043class GIT(object):
44 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000045
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000046 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000047 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048 """Runs git, capturing output sent to stdout as a string.
49
50 Args:
51 args: A sequence of command line parameters to be passed to git.
52 in_directory: The directory where git is to be run.
53
54 Returns:
55 The output sent to stdout as a string.
56 """
57 c = [GIT.COMMAND]
58 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000059 try:
60 return gclient_utils.CheckCall(c, in_directory, print_error)
61 except gclient_utils.CheckCallError:
62 if error_ok:
63 return ''
64 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000065
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000066 @staticmethod
67 def CaptureStatus(files, upstream_branch='origin'):
68 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000071
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000072 Returns an array of (status, file) tuples."""
73 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
74 if not files:
75 pass
76 elif isinstance(files, basestring):
77 command.append(files)
78 else:
79 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000080
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000081 status = GIT.Capture(command).rstrip()
82 results = []
83 if status:
84 for statusline in status.split('\n'):
85 m = re.match('^(\w)\t(.+)$', statusline)
86 if not m:
87 raise Exception("status currently unsupported: %s" % statusline)
88 results.append(('%s ' % m.group(1), m.group(2)))
89 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000090
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000091 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000092 def RunAndFilterOutput(args,
93 in_directory,
94 print_messages,
95 print_stdout,
96 filter):
97 """Runs a command, optionally outputting to stdout.
98
99 stdout is passed line-by-line to the given filter function. If
100 print_stdout is true, it is also printed to sys.stdout as in Run.
101
102 Args:
103 args: A sequence of command line parameters to be passed.
104 in_directory: The directory where svn is to be run.
105 print_messages: Whether to print status messages to stdout about
106 which commands are being run.
107 print_stdout: Whether to forward program's output to stdout.
108 filter: A function taking one argument (a string) which will be
109 passed each line (with the ending newline character removed) of
110 program's output for filtering.
111
112 Raises:
113 gclient_utils.Error: An error occurred while running the command.
114 """
115 command = [GIT.COMMAND]
116 command.extend(args)
117 gclient_utils.SubprocessCallAndFilter(command,
118 in_directory,
119 print_messages,
120 print_stdout,
121 filter=filter)
122
123 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000124 def GetEmail(repo_root):
125 """Retrieves the user email address if known."""
126 # We could want to look at the svn cred when it has a svn remote but it
127 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000128 return GIT.Capture(['config', 'user.email'],
129 repo_root, error_ok=True).strip()
130
131 @staticmethod
132 def ShortBranchName(branch):
133 """Converts a name like 'refs/heads/foo' to just 'foo'."""
134 return branch.replace('refs/heads/', '')
135
136 @staticmethod
137 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000138 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000139 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip()
140
141 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000142 def GetBranch(cwd):
143 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000144 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000145
146 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000147 def IsGitSvn(cwd):
148 """Returns true if this repo looks like it's using git-svn."""
149 # If you have any "svn-remote.*" config keys, we think you're using svn.
150 try:
151 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
152 return True
153 except gclient_utils.CheckCallError:
154 return False
155
156 @staticmethod
157 def GetSVNBranch(cwd):
158 """Returns the svn branch name if found."""
159 # Try to figure out which remote branch we're based on.
160 # Strategy:
161 # 1) find all git-svn branches and note their svn URLs.
162 # 2) iterate through our branch history and match up the URLs.
163
164 # regexp matching the git-svn line that contains the URL.
165 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
166
167 # Get the refname and svn url for all refs/remotes/*.
168 remotes = GIT.Capture(
169 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
170 cwd).splitlines()
171 svn_refs = {}
172 for ref in remotes:
173 match = git_svn_re.search(
174 GIT.Capture(['cat-file', '-p', ref], cwd))
175 if match:
176 svn_refs[match.group(1)] = ref
177
178 svn_branch = ''
179 if len(svn_refs) == 1:
180 # Only one svn branch exists -- seems like a good candidate.
181 svn_branch = svn_refs.values()[0]
182 elif len(svn_refs) > 1:
183 # We have more than one remote branch available. We don't
184 # want to go through all of history, so read a line from the
185 # pipe at a time.
186 # The -100 is an arbitrary limit so we don't search forever.
187 cmd = ['git', 'log', '-100', '--pretty=medium']
188 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
189 for line in proc.stdout:
190 match = git_svn_re.match(line)
191 if match:
192 url = match.group(1)
193 if url in svn_refs:
194 svn_branch = svn_refs[url]
195 proc.stdout.close() # Cut pipe.
196 break
197 return svn_branch
198
199 @staticmethod
200 def FetchUpstreamTuple(cwd):
201 """Returns a tuple containg remote and remote ref,
202 e.g. 'origin', 'refs/heads/master'
203 """
204 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000205 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 upstream_branch = None
207 upstream_branch = GIT.Capture(
208 ['config', 'branch.%s.merge' % branch], error_ok=True).strip()
209 if upstream_branch:
210 remote = GIT.Capture(
211 ['config', 'branch.%s.remote' % branch],
212 error_ok=True).strip()
213 else:
214 # Fall back on trying a git-svn upstream branch.
215 if GIT.IsGitSvn(cwd):
216 upstream_branch = GIT.GetSVNBranch(cwd)
217 # Fall back on origin/master if it exits.
218 if not upstream_branch:
219 GIT.Capture(['branch', '-r']).split().count('origin/master')
220 remote = 'origin'
221 upstream_branch = 'refs/heads/master'
222 return remote, upstream_branch
223
224 @staticmethod
225 def GetUpstream(cwd):
226 """Gets the current branch's upstream branch."""
227 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
228 if remote is not '.':
229 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
230 return upstream_branch
231
232 @staticmethod
maruel@chromium.orga9371762009-12-22 18:27:38 +0000233 def GenerateDiff(cwd, branch=None, full_move=False):
234 """Diffs against the upstream branch or optionally another branch.
235
236 full_move means that move or copy operations should completely recreate the
237 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000238 if not branch:
239 branch = GIT.GetUpstream(cwd)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000240 command = ['diff-tree', '-p', '--no-prefix', branch, 'HEAD']
241 if not full_move:
242 command.append('-C')
243 diff = GIT.Capture(command, cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000244 for i in range(len(diff)):
245 # In the case of added files, replace /dev/null with the path to the
246 # file being added.
247 if diff[i].startswith('--- /dev/null'):
248 diff[i] = '--- %s' % diff[i+1][4:]
249 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000250
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000251 @staticmethod
252 def GetPatchName(cwd):
253 """Constructs a name for this patch."""
254 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd).strip()
255 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
256
257 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000258 def GetCheckoutRoot(path):
259 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000260 """
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000261 root = GIT.Capture(['rev-parse', '--show-cdup'], path).strip()
262 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000263
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000264
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000265class SVN(object):
266 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000267
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000268 @staticmethod
269 def Run(args, in_directory):
270 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000271
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000272 Args:
273 args: A sequence of command line parameters to be passed to svn.
274 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000275
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000276 Raises:
277 Error: An error occurred while running the svn command.
278 """
279 c = [SVN.COMMAND]
280 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000281 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000282 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000283
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000284 @staticmethod
285 def Capture(args, in_directory=None, print_error=True):
286 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000287
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000288 Args:
289 args: A sequence of command line parameters to be passed to svn.
290 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000291
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000292 Returns:
293 The output sent to stdout as a string.
294 """
295 c = [SVN.COMMAND]
296 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000297
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000298 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
299 # the svn.exe executable, but shell=True makes subprocess on Linux fail
300 # when it's called with a list because it only tries to execute the
301 # first string ("svn").
302 stderr = None
303 if not print_error:
304 stderr = subprocess.PIPE
305 return subprocess.Popen(c,
306 cwd=in_directory,
307 shell=(sys.platform == 'win32'),
308 stdout=subprocess.PIPE,
309 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 @staticmethod
312 def RunAndGetFileList(options, args, in_directory, file_list):
313 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000314
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000316
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000317 svn's stdout is parsed to collect a list of files checked out or updated.
318 These files are appended to file_list. svn's stdout is also printed to
319 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000320
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000321 Args:
322 options: command line options to gclient
323 args: A sequence of command line parameters to be passed to svn.
324 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000325
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000326 Raises:
327 Error: An error occurred while running the svn command.
328 """
329 command = [SVN.COMMAND]
330 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000331
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000332 # svn update and svn checkout use the same pattern: the first three columns
333 # are for file status, property status, and lock status. This is followed
334 # by two spaces, and then the path to the file.
335 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000336
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000337 # The first three columns of svn status are the same as for svn update and
338 # svn checkout. The next three columns indicate addition-with-history,
339 # switch, and remote lock status. This is followed by one space, and then
340 # the path to the file.
341 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000342
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000343 # args[0] must be a supported command. This will blow up if it's something
344 # else, which is good. Note that the patterns are only effective when
345 # these commands are used in their ordinary forms, the patterns are invalid
346 # for "svn status --show-updates", for example.
347 pattern = {
348 'checkout': update_pattern,
349 'status': status_pattern,
350 'update': update_pattern,
351 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000352 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000353 # Place an upper limit.
354 for i in range(1, 10):
355 previous_list_len = len(file_list)
356 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000357
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000358 def CaptureMatchingLines(line):
359 match = compiled_pattern.search(line)
360 if match:
361 file_list.append(match.group(1))
362 if line.startswith('svn: '):
363 # We can't raise an exception. We can't alias a variable. Use a cheap
364 # way.
365 failure.append(True)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000366
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000367 try:
368 SVN.RunAndFilterOutput(args,
369 in_directory,
370 options.verbose,
371 True,
372 CaptureMatchingLines)
373 except gclient_utils.Error:
374 # We enforce that some progress has been made.
375 if len(failure) and len(file_list) > previous_list_len:
376 if args[0] == 'checkout':
377 args = args[:]
378 # An aborted checkout is now an update.
379 args[0] = 'update'
380 continue
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000381 # No progress was made, bail out.
382 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000383 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000384
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000385 @staticmethod
386 def RunAndFilterOutput(args,
387 in_directory,
388 print_messages,
389 print_stdout,
390 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000391 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000392
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000393 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000394 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000395
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000396 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000397 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000398 in_directory: The directory where svn is to be run.
399 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000400 which commands are being run.
401 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000402 filter: A function taking one argument (a string) which will be
403 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000404 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000405
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000406 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000407 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000408 """
409 command = [SVN.COMMAND]
410 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000411 gclient_utils.SubprocessCallAndFilter(command,
412 in_directory,
413 print_messages,
414 print_stdout,
415 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000416
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 @staticmethod
418 def CaptureInfo(relpath, in_directory=None, print_error=True):
419 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000420
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 Args:
422 relpath: The directory where the working copy resides relative to
423 the directory given by in_directory.
424 in_directory: The directory where svn is to be run.
425 """
426 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
427 dom = gclient_utils.ParseXML(output)
428 result = {}
429 if dom:
430 GetNamedNodeText = gclient_utils.GetNamedNodeText
431 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
432 def C(item, f):
433 if item is not None: return f(item)
434 # /info/entry/
435 # url
436 # reposityory/(root|uuid)
437 # wc-info/(schedule|depth)
438 # commit/(author|date)
439 # str() the results because they may be returned as Unicode, which
440 # interferes with the higher layers matching up things in the deps
441 # dictionary.
442 # TODO(maruel): Fix at higher level instead (!)
443 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
444 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
445 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
446 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
447 'revision'),
448 int)
449 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
450 str)
451 # Differs across versions.
452 if result['Node Kind'] == 'dir':
453 result['Node Kind'] = 'directory'
454 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
455 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
456 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
457 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
458 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000459
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000460 @staticmethod
461 def CaptureHeadRevision(url):
462 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000463
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000464 Returns:
465 Int head revision
466 """
467 info = SVN.Capture(["info", "--xml", url], os.getcwd())
468 dom = xml.dom.minidom.parseString(info)
469 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000470
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000471 @staticmethod
472 def CaptureStatus(files):
473 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000474
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000475 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000476
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000477 Returns an array of (status, file) tuples."""
478 command = ["status", "--xml"]
479 if not files:
480 pass
481 elif isinstance(files, basestring):
482 command.append(files)
483 else:
484 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000485
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000486 status_letter = {
487 None: ' ',
488 '': ' ',
489 'added': 'A',
490 'conflicted': 'C',
491 'deleted': 'D',
492 'external': 'X',
493 'ignored': 'I',
494 'incomplete': '!',
495 'merged': 'G',
496 'missing': '!',
497 'modified': 'M',
498 'none': ' ',
499 'normal': ' ',
500 'obstructed': '~',
501 'replaced': 'R',
502 'unversioned': '?',
503 }
504 dom = gclient_utils.ParseXML(SVN.Capture(command))
505 results = []
506 if dom:
507 # /status/target/entry/(wc-status|commit|author|date)
508 for target in dom.getElementsByTagName('target'):
509 #base_path = target.getAttribute('path')
510 for entry in target.getElementsByTagName('entry'):
511 file_path = entry.getAttribute('path')
512 wc_status = entry.getElementsByTagName('wc-status')
513 assert len(wc_status) == 1
514 # Emulate svn 1.5 status ouput...
515 statuses = [' '] * 7
516 # Col 0
517 xml_item_status = wc_status[0].getAttribute('item')
518 if xml_item_status in status_letter:
519 statuses[0] = status_letter[xml_item_status]
520 else:
521 raise Exception('Unknown item status "%s"; please implement me!' %
522 xml_item_status)
523 # Col 1
524 xml_props_status = wc_status[0].getAttribute('props')
525 if xml_props_status == 'modified':
526 statuses[1] = 'M'
527 elif xml_props_status == 'conflicted':
528 statuses[1] = 'C'
529 elif (not xml_props_status or xml_props_status == 'none' or
530 xml_props_status == 'normal'):
531 pass
532 else:
533 raise Exception('Unknown props status "%s"; please implement me!' %
534 xml_props_status)
535 # Col 2
536 if wc_status[0].getAttribute('wc-locked') == 'true':
537 statuses[2] = 'L'
538 # Col 3
539 if wc_status[0].getAttribute('copied') == 'true':
540 statuses[3] = '+'
541 # Col 4
542 if wc_status[0].getAttribute('switched') == 'true':
543 statuses[4] = 'S'
544 # TODO(maruel): Col 5 and 6
545 item = (''.join(statuses), file_path)
546 results.append(item)
547 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000548
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000549 @staticmethod
550 def IsMoved(filename):
551 """Determine if a file has been added through svn mv"""
552 info = SVN.CaptureInfo(filename)
553 return (info.get('Copied From URL') and
554 info.get('Copied From Rev') and
555 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000556
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000557 @staticmethod
558 def GetFileProperty(file, property_name):
559 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000560
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000561 Args:
562 file: The file to check
563 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000564
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000565 Returns:
566 The value of the property, which will be the empty string if the property
567 is not set on the file. If the file is not under version control, the
568 empty string is also returned.
569 """
570 output = SVN.Capture(["propget", property_name, file])
571 if (output.startswith("svn: ") and
572 output.endswith("is not under version control")):
573 return ""
574 else:
575 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000576
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000577 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000578 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000579 """Diffs a single file.
580
581 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000582 expected relative path.
583 full_move means that move or copy operations should completely recreate the
584 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000585 # Use svn info output instead of os.path.isdir because the latter fails
586 # when the file is deleted.
587 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
588 return None
589 # If the user specified a custom diff command in their svn config file,
590 # then it'll be used when we do svn diff, which we don't want to happen
591 # since we want the unified diff. Using --diff-cmd=diff doesn't always
592 # work, since they can have another diff executable in their path that
593 # gives different line endings. So we use a bogus temp directory as the
594 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000595 bogus_dir = tempfile.mkdtemp()
596 try:
597 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000598 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000599 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000600 command.extend(['--revision', revision])
601 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000602 if data:
603 pass
604 elif SVN.IsMoved(filename):
605 if full_move:
606 file_content = gclient_utils.FileRead(filename, 'rb')
607 # Prepend '+' to every lines.
608 file_content = ['+' + i for i in file_content.splitlines(True)]
609 nb_lines = len(file_content)
610 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000611 data = "Index: %s\n" % filename
612 data += '=' * 67 + '\n'
613 # Note: Should we use /dev/null instead?
614 data += "--- %s\n" % filename
615 data += "+++ %s\n" % filename
616 data += "@@ -0,0 +1,%d @@\n" % nb_lines
617 data += ''.join(file_content)
618 else:
619 # svn diff on a mv/cp'd file outputs nothing.
620 # We put in an empty Index entry so upload.py knows about them.
621 data = "Index: %s\n" % filename
622 else:
623 # The file is not modified anymore. It should be removed from the set.
624 pass
625 finally:
626 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000627 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000628
629 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000630 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000631 """Returns a string containing the diff for the given file list.
632
633 The files in the list should either be absolute paths or relative to the
634 given root. If no root directory is provided, the repository root will be
635 used.
636 The diff will always use relative paths.
637 """
638 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000639 root = root or SVN.GetCheckoutRoot(previous_cwd)
640 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000641 def RelativePath(path, root):
642 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000643 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000644 return path[len(root):]
645 return path
646 try:
647 os.chdir(root)
648 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000649 [SVN.DiffItem(RelativePath(f, root),
650 full_move=full_move,
651 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000652 for f in filenames]))
653 finally:
654 os.chdir(previous_cwd)
655 return diff
656
657
658 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000659 def GetEmail(repo_root):
660 """Retrieves the svn account which we assume is an email address."""
661 infos = SVN.CaptureInfo(repo_root)
662 uuid = infos.get('UUID')
663 root = infos.get('Repository Root')
664 if not root:
665 return None
666
667 # Should check for uuid but it is incorrectly saved for https creds.
668 realm = root.rsplit('/', 1)[0]
669 if root.startswith('https') or not uuid:
670 regexp = re.compile(r'<%s:\d+>.*' % realm)
671 else:
672 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
673 if regexp is None:
674 return None
675 if sys.platform.startswith('win'):
676 if not 'APPDATA' in os.environ:
677 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000678 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
679 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000680 else:
681 if not 'HOME' in os.environ:
682 return None
683 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
684 'svn.simple')
685 for credfile in os.listdir(auth_dir):
686 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
687 if regexp.match(cred_info.get('svn:realmstring')):
688 return cred_info.get('username')
689
690 @staticmethod
691 def ReadSimpleAuth(filename):
692 f = open(filename, 'r')
693 values = {}
694 def ReadOneItem(type):
695 m = re.match(r'%s (\d+)' % type, f.readline())
696 if not m:
697 return None
698 data = f.read(int(m.group(1)))
699 if f.read(1) != '\n':
700 return None
701 return data
702
703 while True:
704 key = ReadOneItem('K')
705 if not key:
706 break
707 value = ReadOneItem('V')
708 if not value:
709 break
710 values[key] = value
711 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000712
713 @staticmethod
714 def GetCheckoutRoot(directory):
715 """Returns the top level directory of the current repository.
716
717 The directory is returned as an absolute path.
718 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000719 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000720 infos = SVN.CaptureInfo(directory, print_error=False)
721 cur_dir_repo_root = infos.get("Repository Root")
722 if not cur_dir_repo_root:
723 return None
724
725 while True:
726 parent = os.path.dirname(directory)
727 if (SVN.CaptureInfo(parent, print_error=False).get(
728 "Repository Root") != cur_dir_repo_root):
729 break
730 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000731 return GetCasedPath(directory)