blob: df22271a22f995165c2e634af1a2bd551143d7c5 [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:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000063 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000064 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.org7be5ef22010-01-30 22:31:50 +000081 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000082 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.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000104 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000105 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'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000129 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000130
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.org7be5ef22010-01-30 22:31:50 +0000139 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000140
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'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000170 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000171 svn_refs = {}
172 for ref in remotes:
173 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000174 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 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(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000208 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
209 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000210 if upstream_branch:
211 remote = GIT.Capture(
212 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000213 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000214 else:
215 # Fall back on trying a git-svn upstream branch.
216 if GIT.IsGitSvn(cwd):
217 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000218 return remote, upstream_branch
219
220 @staticmethod
221 def GetUpstream(cwd):
222 """Gets the current branch's upstream branch."""
223 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
224 if remote is not '.':
225 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
226 return upstream_branch
227
228 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000229 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
230 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000231 """Diffs against the upstream branch or optionally another branch.
232
233 full_move means that move or copy operations should completely recreate the
234 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000235 if not branch:
236 branch = GIT.GetUpstream(cwd)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000237 command = ['diff-tree', '-p', '--no-prefix', branch, branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000238 if not full_move:
239 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000240 # TODO(maruel): --binary support.
241 if files:
242 command.append('--')
243 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000244 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000245 for i in range(len(diff)):
246 # In the case of added files, replace /dev/null with the path to the
247 # file being added.
248 if diff[i].startswith('--- /dev/null'):
249 diff[i] = '--- %s' % diff[i+1][4:]
250 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000251
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000252 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000253 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
254 """Returns the list of modified files between two branches."""
255 if not branch:
256 branch = GIT.GetUpstream(cwd)
257 command = ['diff', '--name-only', branch, branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000258 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000259
260 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000261 def GetPatchName(cwd):
262 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000263 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000264 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
265
266 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000267 def GetCheckoutRoot(path):
268 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000269 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000270 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000271 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000272
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000273
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000274class SVN(object):
275 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000276
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000277 @staticmethod
278 def Run(args, in_directory):
279 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 Args:
282 args: A sequence of command line parameters to be passed to svn.
283 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000284
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000285 Raises:
286 Error: An error occurred while running the svn command.
287 """
288 c = [SVN.COMMAND]
289 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000290 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000291 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000292
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000293 @staticmethod
294 def Capture(args, in_directory=None, print_error=True):
295 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000296
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000297 Args:
298 args: A sequence of command line parameters to be passed to svn.
299 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000300
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 Returns:
302 The output sent to stdout as a string.
303 """
304 c = [SVN.COMMAND]
305 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000306
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000307 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
308 # the svn.exe executable, but shell=True makes subprocess on Linux fail
309 # when it's called with a list because it only tries to execute the
310 # first string ("svn").
311 stderr = None
312 if not print_error:
313 stderr = subprocess.PIPE
314 return subprocess.Popen(c,
315 cwd=in_directory,
316 shell=(sys.platform == 'win32'),
317 stdout=subprocess.PIPE,
318 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000319
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000320 @staticmethod
321 def RunAndGetFileList(options, args, in_directory, file_list):
322 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000323
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000324 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000325
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000326 svn's stdout is parsed to collect a list of files checked out or updated.
327 These files are appended to file_list. svn's stdout is also printed to
328 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000329
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000330 Args:
331 options: command line options to gclient
332 args: A sequence of command line parameters to be passed to svn.
333 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000334
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000335 Raises:
336 Error: An error occurred while running the svn command.
337 """
338 command = [SVN.COMMAND]
339 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000340
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000341 # svn update and svn checkout use the same pattern: the first three columns
342 # are for file status, property status, and lock status. This is followed
343 # by two spaces, and then the path to the file.
344 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000345
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000346 # The first three columns of svn status are the same as for svn update and
347 # svn checkout. The next three columns indicate addition-with-history,
348 # switch, and remote lock status. This is followed by one space, and then
349 # the path to the file.
350 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000351
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000352 # args[0] must be a supported command. This will blow up if it's something
353 # else, which is good. Note that the patterns are only effective when
354 # these commands are used in their ordinary forms, the patterns are invalid
355 # for "svn status --show-updates", for example.
356 pattern = {
357 'checkout': update_pattern,
358 'status': status_pattern,
359 'update': update_pattern,
360 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000361 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000362 # Place an upper limit.
363 for i in range(1, 10):
364 previous_list_len = len(file_list)
365 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000366
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000367 def CaptureMatchingLines(line):
368 match = compiled_pattern.search(line)
369 if match:
370 file_list.append(match.group(1))
371 if line.startswith('svn: '):
372 # We can't raise an exception. We can't alias a variable. Use a cheap
373 # way.
374 failure.append(True)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000375
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000376 try:
377 SVN.RunAndFilterOutput(args,
378 in_directory,
379 options.verbose,
380 True,
381 CaptureMatchingLines)
382 except gclient_utils.Error:
383 # We enforce that some progress has been made.
384 if len(failure) and len(file_list) > previous_list_len:
385 if args[0] == 'checkout':
386 args = args[:]
387 # An aborted checkout is now an update.
388 args[0] = 'update'
389 continue
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000390 # No progress was made, bail out.
391 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000392 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000393
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000394 @staticmethod
395 def RunAndFilterOutput(args,
396 in_directory,
397 print_messages,
398 print_stdout,
399 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000400 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000401
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000402 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000403 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000404
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000405 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000406 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000407 in_directory: The directory where svn is to be run.
408 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000409 which commands are being run.
410 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000411 filter: A function taking one argument (a string) which will be
412 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000413 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000414
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000416 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 """
418 command = [SVN.COMMAND]
419 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000420 gclient_utils.SubprocessCallAndFilter(command,
421 in_directory,
422 print_messages,
423 print_stdout,
424 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000425
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000426 @staticmethod
427 def CaptureInfo(relpath, in_directory=None, print_error=True):
428 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000429
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000430 Args:
431 relpath: The directory where the working copy resides relative to
432 the directory given by in_directory.
433 in_directory: The directory where svn is to be run.
434 """
435 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
436 dom = gclient_utils.ParseXML(output)
437 result = {}
438 if dom:
439 GetNamedNodeText = gclient_utils.GetNamedNodeText
440 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
441 def C(item, f):
442 if item is not None: return f(item)
443 # /info/entry/
444 # url
445 # reposityory/(root|uuid)
446 # wc-info/(schedule|depth)
447 # commit/(author|date)
448 # str() the results because they may be returned as Unicode, which
449 # interferes with the higher layers matching up things in the deps
450 # dictionary.
451 # TODO(maruel): Fix at higher level instead (!)
452 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
453 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
454 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
455 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
456 'revision'),
457 int)
458 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
459 str)
460 # Differs across versions.
461 if result['Node Kind'] == 'dir':
462 result['Node Kind'] = 'directory'
463 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
464 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
465 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
466 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
467 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000468
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000469 @staticmethod
470 def CaptureHeadRevision(url):
471 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000472
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000473 Returns:
474 Int head revision
475 """
476 info = SVN.Capture(["info", "--xml", url], os.getcwd())
477 dom = xml.dom.minidom.parseString(info)
478 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000479
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000480 @staticmethod
481 def CaptureStatus(files):
482 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000483
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000484 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000485
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000486 Returns an array of (status, file) tuples."""
487 command = ["status", "--xml"]
488 if not files:
489 pass
490 elif isinstance(files, basestring):
491 command.append(files)
492 else:
493 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000494
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000495 status_letter = {
496 None: ' ',
497 '': ' ',
498 'added': 'A',
499 'conflicted': 'C',
500 'deleted': 'D',
501 'external': 'X',
502 'ignored': 'I',
503 'incomplete': '!',
504 'merged': 'G',
505 'missing': '!',
506 'modified': 'M',
507 'none': ' ',
508 'normal': ' ',
509 'obstructed': '~',
510 'replaced': 'R',
511 'unversioned': '?',
512 }
513 dom = gclient_utils.ParseXML(SVN.Capture(command))
514 results = []
515 if dom:
516 # /status/target/entry/(wc-status|commit|author|date)
517 for target in dom.getElementsByTagName('target'):
518 #base_path = target.getAttribute('path')
519 for entry in target.getElementsByTagName('entry'):
520 file_path = entry.getAttribute('path')
521 wc_status = entry.getElementsByTagName('wc-status')
522 assert len(wc_status) == 1
523 # Emulate svn 1.5 status ouput...
524 statuses = [' '] * 7
525 # Col 0
526 xml_item_status = wc_status[0].getAttribute('item')
527 if xml_item_status in status_letter:
528 statuses[0] = status_letter[xml_item_status]
529 else:
530 raise Exception('Unknown item status "%s"; please implement me!' %
531 xml_item_status)
532 # Col 1
533 xml_props_status = wc_status[0].getAttribute('props')
534 if xml_props_status == 'modified':
535 statuses[1] = 'M'
536 elif xml_props_status == 'conflicted':
537 statuses[1] = 'C'
538 elif (not xml_props_status or xml_props_status == 'none' or
539 xml_props_status == 'normal'):
540 pass
541 else:
542 raise Exception('Unknown props status "%s"; please implement me!' %
543 xml_props_status)
544 # Col 2
545 if wc_status[0].getAttribute('wc-locked') == 'true':
546 statuses[2] = 'L'
547 # Col 3
548 if wc_status[0].getAttribute('copied') == 'true':
549 statuses[3] = '+'
550 # Col 4
551 if wc_status[0].getAttribute('switched') == 'true':
552 statuses[4] = 'S'
553 # TODO(maruel): Col 5 and 6
554 item = (''.join(statuses), file_path)
555 results.append(item)
556 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000557
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000558 @staticmethod
559 def IsMoved(filename):
560 """Determine if a file has been added through svn mv"""
561 info = SVN.CaptureInfo(filename)
562 return (info.get('Copied From URL') and
563 info.get('Copied From Rev') and
564 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000565
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 @staticmethod
567 def GetFileProperty(file, property_name):
568 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000569
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000570 Args:
571 file: The file to check
572 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000573
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 Returns:
575 The value of the property, which will be the empty string if the property
576 is not set on the file. If the file is not under version control, the
577 empty string is also returned.
578 """
579 output = SVN.Capture(["propget", property_name, file])
580 if (output.startswith("svn: ") and
581 output.endswith("is not under version control")):
582 return ""
583 else:
584 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000585
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000587 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000588 """Diffs a single file.
589
590 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000591 expected relative path.
592 full_move means that move or copy operations should completely recreate the
593 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000594 # Use svn info output instead of os.path.isdir because the latter fails
595 # when the file is deleted.
596 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
597 return None
598 # If the user specified a custom diff command in their svn config file,
599 # then it'll be used when we do svn diff, which we don't want to happen
600 # since we want the unified diff. Using --diff-cmd=diff doesn't always
601 # work, since they can have another diff executable in their path that
602 # gives different line endings. So we use a bogus temp directory as the
603 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000604 bogus_dir = tempfile.mkdtemp()
605 try:
606 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000607 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000608 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000609 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000610 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000611 if full_move:
612 file_content = gclient_utils.FileRead(filename, 'rb')
613 # Prepend '+' to every lines.
614 file_content = ['+' + i for i in file_content.splitlines(True)]
615 nb_lines = len(file_content)
616 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000617 data = "Index: %s\n" % filename
618 data += '=' * 67 + '\n'
619 # Note: Should we use /dev/null instead?
620 data += "--- %s\n" % filename
621 data += "+++ %s\n" % filename
622 data += "@@ -0,0 +1,%d @@\n" % nb_lines
623 data += ''.join(file_content)
624 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000625 # svn diff on a mv/cp'd file outputs nothing if there was no change.
626 data = SVN.Capture(command, None)
627 if not data:
628 # We put in an empty Index entry so upload.py knows about them.
629 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000630 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000631 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000632 finally:
633 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000634 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000635
636 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000637 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000638 """Returns a string containing the diff for the given file list.
639
640 The files in the list should either be absolute paths or relative to the
641 given root. If no root directory is provided, the repository root will be
642 used.
643 The diff will always use relative paths.
644 """
645 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000646 root = root or SVN.GetCheckoutRoot(previous_cwd)
647 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000648 def RelativePath(path, root):
649 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000650 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000651 return path[len(root):]
652 return path
653 try:
654 os.chdir(root)
655 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000656 [SVN.DiffItem(RelativePath(f, root),
657 full_move=full_move,
658 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000659 for f in filenames]))
660 finally:
661 os.chdir(previous_cwd)
662 return diff
663
664
665 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000666 def GetEmail(repo_root):
667 """Retrieves the svn account which we assume is an email address."""
668 infos = SVN.CaptureInfo(repo_root)
669 uuid = infos.get('UUID')
670 root = infos.get('Repository Root')
671 if not root:
672 return None
673
674 # Should check for uuid but it is incorrectly saved for https creds.
675 realm = root.rsplit('/', 1)[0]
676 if root.startswith('https') or not uuid:
677 regexp = re.compile(r'<%s:\d+>.*' % realm)
678 else:
679 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
680 if regexp is None:
681 return None
682 if sys.platform.startswith('win'):
683 if not 'APPDATA' in os.environ:
684 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000685 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
686 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000687 else:
688 if not 'HOME' in os.environ:
689 return None
690 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
691 'svn.simple')
692 for credfile in os.listdir(auth_dir):
693 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
694 if regexp.match(cred_info.get('svn:realmstring')):
695 return cred_info.get('username')
696
697 @staticmethod
698 def ReadSimpleAuth(filename):
699 f = open(filename, 'r')
700 values = {}
701 def ReadOneItem(type):
702 m = re.match(r'%s (\d+)' % type, f.readline())
703 if not m:
704 return None
705 data = f.read(int(m.group(1)))
706 if f.read(1) != '\n':
707 return None
708 return data
709
710 while True:
711 key = ReadOneItem('K')
712 if not key:
713 break
714 value = ReadOneItem('V')
715 if not value:
716 break
717 values[key] = value
718 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000719
720 @staticmethod
721 def GetCheckoutRoot(directory):
722 """Returns the top level directory of the current repository.
723
724 The directory is returned as an absolute path.
725 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000726 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000727 infos = SVN.CaptureInfo(directory, print_error=False)
728 cur_dir_repo_root = infos.get("Repository Root")
729 if not cur_dir_repo_root:
730 return None
731
732 while True:
733 parent = os.path.dirname(directory)
734 if (SVN.CaptureInfo(parent, print_error=False).get(
735 "Repository Root") != cur_dir_repo_root):
736 break
737 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000738 return GetCasedPath(directory)