blob: c4834116f9818a8a4c4f77596f8b5e5cb6922bbf [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."""
bauerb@chromium.org14ec5042010-03-30 18:19:09 +000073 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000074 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])
sky@chromium.org42d8da52010-04-23 18:25:07 +0000175 # Prefer origin/HEAD over all others.
176 if match and (match.group(1) not in svn_refs or
177 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000178 svn_refs[match.group(1)] = ref
179
180 svn_branch = ''
181 if len(svn_refs) == 1:
182 # Only one svn branch exists -- seems like a good candidate.
183 svn_branch = svn_refs.values()[0]
184 elif len(svn_refs) > 1:
185 # We have more than one remote branch available. We don't
186 # want to go through all of history, so read a line from the
187 # pipe at a time.
188 # The -100 is an arbitrary limit so we don't search forever.
189 cmd = ['git', 'log', '-100', '--pretty=medium']
190 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
191 for line in proc.stdout:
192 match = git_svn_re.match(line)
193 if match:
194 url = match.group(1)
195 if url in svn_refs:
196 svn_branch = svn_refs[url]
197 proc.stdout.close() # Cut pipe.
198 break
199 return svn_branch
200
201 @staticmethod
202 def FetchUpstreamTuple(cwd):
203 """Returns a tuple containg remote and remote ref,
204 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000205 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 """
207 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000208 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000209 upstream_branch = None
210 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000211 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
212 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000213 if upstream_branch:
214 remote = GIT.Capture(
215 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000216 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000217 else:
218 # Fall back on trying a git-svn upstream branch.
219 if GIT.IsGitSvn(cwd):
220 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000221 # Fall back on origin/master if it exits.
222 elif GIT.Capture(['branch', '-r'], in_directory=cwd
223 )[0].split().count('origin/master'):
224 remote = 'origin'
225 upstream_branch = 'refs/heads/master'
226 else:
227 remote = None
228 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000229 return remote, upstream_branch
230
231 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000232 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000233 """Gets the current branch's upstream branch."""
234 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000235 if remote != '.':
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000236 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
237 return upstream_branch
238
239 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000240 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
241 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000242 """Diffs against the upstream branch or optionally another branch.
243
244 full_move means that move or copy operations should completely recreate the
245 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000246 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000247 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000248 command = ['diff', '-p', '--no-prefix', branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000249 if not full_move:
250 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000251 # TODO(maruel): --binary support.
252 if files:
253 command.append('--')
254 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000255 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000256 for i in range(len(diff)):
257 # In the case of added files, replace /dev/null with the path to the
258 # file being added.
259 if diff[i].startswith('--- /dev/null'):
260 diff[i] = '--- %s' % diff[i+1][4:]
261 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000262
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000263 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000264 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
265 """Returns the list of modified files between two branches."""
266 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000267 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000268 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000269 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000270
271 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000272 def GetPatchName(cwd):
273 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000274 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000275 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
276
277 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000278 def GetCheckoutRoot(path):
279 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000280 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000281 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000282 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000283
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000284 @staticmethod
285 def AssertVersion(min_version):
286 """Asserts git's version is at least min_version."""
287 def only_int(val):
288 if val.isdigit():
289 return int(val)
290 else:
291 return 0
292 current_version = GIT.Capture(['--version'])[0].split()[-1]
293 current_version_list = map(only_int, current_version.split('.'))
294 for min_ver in map(int, min_version.split('.')):
295 ver = current_version_list.pop(0)
296 if ver < min_ver:
297 return (False, current_version)
298 elif ver > min_ver:
299 return (True, current_version)
300 return (True, current_version)
301
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000302
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303class SVN(object):
304 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000305 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000306
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000307 @staticmethod
308 def Run(args, in_directory):
309 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 Args:
312 args: A sequence of command line parameters to be passed to svn.
313 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000314
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 Raises:
316 Error: An error occurred while running the svn command.
317 """
318 c = [SVN.COMMAND]
319 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000320 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000321 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000322
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000323 @staticmethod
324 def Capture(args, in_directory=None, print_error=True):
325 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000326
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000327 Args:
328 args: A sequence of command line parameters to be passed to svn.
329 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000330
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000331 Returns:
332 The output sent to stdout as a string.
333 """
334 c = [SVN.COMMAND]
335 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000336
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000337 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
338 # the svn.exe executable, but shell=True makes subprocess on Linux fail
339 # when it's called with a list because it only tries to execute the
340 # first string ("svn").
341 stderr = None
342 if not print_error:
343 stderr = subprocess.PIPE
344 return subprocess.Popen(c,
345 cwd=in_directory,
346 shell=(sys.platform == 'win32'),
347 stdout=subprocess.PIPE,
348 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000349
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000350 @staticmethod
351 def RunAndGetFileList(options, args, in_directory, file_list):
352 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000353
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000354 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 svn's stdout is parsed to collect a list of files checked out or updated.
357 These files are appended to file_list. svn's stdout is also printed to
358 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000359
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000360 Args:
361 options: command line options to gclient
362 args: A sequence of command line parameters to be passed to svn.
363 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000364
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000365 Raises:
366 Error: An error occurred while running the svn command.
367 """
368 command = [SVN.COMMAND]
369 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000370
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000371 # svn update and svn checkout use the same pattern: the first three columns
372 # are for file status, property status, and lock status. This is followed
373 # by two spaces, and then the path to the file.
374 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000375
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000376 # The first three columns of svn status are the same as for svn update and
377 # svn checkout. The next three columns indicate addition-with-history,
378 # switch, and remote lock status. This is followed by one space, and then
379 # the path to the file.
380 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000381
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000382 # args[0] must be a supported command. This will blow up if it's something
383 # else, which is good. Note that the patterns are only effective when
384 # these commands are used in their ordinary forms, the patterns are invalid
385 # for "svn status --show-updates", for example.
386 pattern = {
387 'checkout': update_pattern,
388 'status': status_pattern,
389 'update': update_pattern,
390 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000391 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000392 # Place an upper limit.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000393 for _ in range(1, 10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000394 previous_list_len = len(file_list)
395 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000396
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000397 def CaptureMatchingLines(line):
398 match = compiled_pattern.search(line)
399 if match:
400 file_list.append(match.group(1))
401 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000402 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000403
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000404 try:
405 SVN.RunAndFilterOutput(args,
406 in_directory,
407 options.verbose,
408 True,
409 CaptureMatchingLines)
410 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000411 # We enforce that some progress has been made or HTTP 502.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000412 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
maruel@chromium.org2de10252010-02-08 01:10:39 +0000413 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000414 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000415 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000416 args = ['update'] + args[1:]
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000417 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000418 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000419 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000420 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000421
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000422 @staticmethod
423 def RunAndFilterOutput(args,
424 in_directory,
425 print_messages,
426 print_stdout,
427 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000428 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000429
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000430 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000431 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000432
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000433 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000434 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000435 in_directory: The directory where svn is to be run.
436 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000437 which commands are being run.
438 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000439 filter: A function taking one argument (a string) which will be
440 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000441 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000442
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000443 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000444 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000445 """
446 command = [SVN.COMMAND]
447 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000448 gclient_utils.SubprocessCallAndFilter(command,
449 in_directory,
450 print_messages,
451 print_stdout,
452 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000453
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000454 @staticmethod
455 def CaptureInfo(relpath, in_directory=None, print_error=True):
456 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000457
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000458 Args:
459 relpath: The directory where the working copy resides relative to
460 the directory given by in_directory.
461 in_directory: The directory where svn is to be run.
462 """
463 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
464 dom = gclient_utils.ParseXML(output)
465 result = {}
466 if dom:
467 GetNamedNodeText = gclient_utils.GetNamedNodeText
468 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
469 def C(item, f):
470 if item is not None: return f(item)
471 # /info/entry/
472 # url
473 # reposityory/(root|uuid)
474 # wc-info/(schedule|depth)
475 # commit/(author|date)
476 # str() the results because they may be returned as Unicode, which
477 # interferes with the higher layers matching up things in the deps
478 # dictionary.
479 # TODO(maruel): Fix at higher level instead (!)
480 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
481 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
482 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
483 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
484 'revision'),
485 int)
486 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
487 str)
488 # Differs across versions.
489 if result['Node Kind'] == 'dir':
490 result['Node Kind'] = 'directory'
491 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
492 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
493 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
494 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
495 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000496
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000497 @staticmethod
498 def CaptureHeadRevision(url):
499 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000500
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000501 Returns:
502 Int head revision
503 """
504 info = SVN.Capture(["info", "--xml", url], os.getcwd())
505 dom = xml.dom.minidom.parseString(info)
506 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000507
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000508 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000509 def CaptureBaseRevision(cwd):
510 """Get the base revision of a SVN repository.
511
512 Returns:
513 Int base revision
514 """
515 info = SVN.Capture(["info", "--xml"], cwd)
516 dom = xml.dom.minidom.parseString(info)
517 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
518
519 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000520 def CaptureStatus(files):
521 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000522
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000523 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000524
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000525 Returns an array of (status, file) tuples."""
526 command = ["status", "--xml"]
527 if not files:
528 pass
529 elif isinstance(files, basestring):
530 command.append(files)
531 else:
532 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000533
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000534 status_letter = {
535 None: ' ',
536 '': ' ',
537 'added': 'A',
538 'conflicted': 'C',
539 'deleted': 'D',
540 'external': 'X',
541 'ignored': 'I',
542 'incomplete': '!',
543 'merged': 'G',
544 'missing': '!',
545 'modified': 'M',
546 'none': ' ',
547 'normal': ' ',
548 'obstructed': '~',
549 'replaced': 'R',
550 'unversioned': '?',
551 }
552 dom = gclient_utils.ParseXML(SVN.Capture(command))
553 results = []
554 if dom:
555 # /status/target/entry/(wc-status|commit|author|date)
556 for target in dom.getElementsByTagName('target'):
557 #base_path = target.getAttribute('path')
558 for entry in target.getElementsByTagName('entry'):
559 file_path = entry.getAttribute('path')
560 wc_status = entry.getElementsByTagName('wc-status')
561 assert len(wc_status) == 1
562 # Emulate svn 1.5 status ouput...
563 statuses = [' '] * 7
564 # Col 0
565 xml_item_status = wc_status[0].getAttribute('item')
566 if xml_item_status in status_letter:
567 statuses[0] = status_letter[xml_item_status]
568 else:
569 raise Exception('Unknown item status "%s"; please implement me!' %
570 xml_item_status)
571 # Col 1
572 xml_props_status = wc_status[0].getAttribute('props')
573 if xml_props_status == 'modified':
574 statuses[1] = 'M'
575 elif xml_props_status == 'conflicted':
576 statuses[1] = 'C'
577 elif (not xml_props_status or xml_props_status == 'none' or
578 xml_props_status == 'normal'):
579 pass
580 else:
581 raise Exception('Unknown props status "%s"; please implement me!' %
582 xml_props_status)
583 # Col 2
584 if wc_status[0].getAttribute('wc-locked') == 'true':
585 statuses[2] = 'L'
586 # Col 3
587 if wc_status[0].getAttribute('copied') == 'true':
588 statuses[3] = '+'
589 # Col 4
590 if wc_status[0].getAttribute('switched') == 'true':
591 statuses[4] = 'S'
592 # TODO(maruel): Col 5 and 6
593 item = (''.join(statuses), file_path)
594 results.append(item)
595 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000596
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000597 @staticmethod
598 def IsMoved(filename):
599 """Determine if a file has been added through svn mv"""
600 info = SVN.CaptureInfo(filename)
601 return (info.get('Copied From URL') and
602 info.get('Copied From Rev') and
603 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000604
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000605 @staticmethod
606 def GetFileProperty(file, property_name):
607 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000608
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000609 Args:
610 file: The file to check
611 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000612
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000613 Returns:
614 The value of the property, which will be the empty string if the property
615 is not set on the file. If the file is not under version control, the
616 empty string is also returned.
617 """
618 output = SVN.Capture(["propget", property_name, file])
619 if (output.startswith("svn: ") and
620 output.endswith("is not under version control")):
621 return ""
622 else:
623 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000624
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000625 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000626 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000627 """Diffs a single file.
628
629 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000630 expected relative path.
631 full_move means that move or copy operations should completely recreate the
632 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000633 # Use svn info output instead of os.path.isdir because the latter fails
634 # when the file is deleted.
635 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
636 return None
637 # If the user specified a custom diff command in their svn config file,
638 # then it'll be used when we do svn diff, which we don't want to happen
639 # since we want the unified diff. Using --diff-cmd=diff doesn't always
640 # work, since they can have another diff executable in their path that
641 # gives different line endings. So we use a bogus temp directory as the
642 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000643 bogus_dir = tempfile.mkdtemp()
644 try:
645 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000646 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000647 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000648 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000649 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000650 if full_move:
651 file_content = gclient_utils.FileRead(filename, 'rb')
652 # Prepend '+' to every lines.
653 file_content = ['+' + i for i in file_content.splitlines(True)]
654 nb_lines = len(file_content)
655 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000656 data = "Index: %s\n" % filename
657 data += '=' * 67 + '\n'
658 # Note: Should we use /dev/null instead?
659 data += "--- %s\n" % filename
660 data += "+++ %s\n" % filename
661 data += "@@ -0,0 +1,%d @@\n" % nb_lines
662 data += ''.join(file_content)
663 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000664 # svn diff on a mv/cp'd file outputs nothing if there was no change.
665 data = SVN.Capture(command, None)
666 if not data:
667 # We put in an empty Index entry so upload.py knows about them.
668 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000669 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000670 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000671 finally:
672 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000673 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000674
675 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000676 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000677 """Returns a string containing the diff for the given file list.
678
679 The files in the list should either be absolute paths or relative to the
680 given root. If no root directory is provided, the repository root will be
681 used.
682 The diff will always use relative paths.
683 """
684 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000685 root = root or SVN.GetCheckoutRoot(previous_cwd)
686 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000687 def RelativePath(path, root):
688 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000689 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000690 return path[len(root):]
691 return path
692 try:
693 os.chdir(root)
694 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000695 [SVN.DiffItem(RelativePath(f, root),
696 full_move=full_move,
697 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000698 for f in filenames]))
699 finally:
700 os.chdir(previous_cwd)
701 return diff
702
703
704 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000705 def GetEmail(repo_root):
706 """Retrieves the svn account which we assume is an email address."""
707 infos = SVN.CaptureInfo(repo_root)
708 uuid = infos.get('UUID')
709 root = infos.get('Repository Root')
710 if not root:
711 return None
712
713 # Should check for uuid but it is incorrectly saved for https creds.
714 realm = root.rsplit('/', 1)[0]
715 if root.startswith('https') or not uuid:
716 regexp = re.compile(r'<%s:\d+>.*' % realm)
717 else:
718 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
719 if regexp is None:
720 return None
721 if sys.platform.startswith('win'):
722 if not 'APPDATA' in os.environ:
723 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000724 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
725 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000726 else:
727 if not 'HOME' in os.environ:
728 return None
729 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
730 'svn.simple')
731 for credfile in os.listdir(auth_dir):
732 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
733 if regexp.match(cred_info.get('svn:realmstring')):
734 return cred_info.get('username')
735
736 @staticmethod
737 def ReadSimpleAuth(filename):
738 f = open(filename, 'r')
739 values = {}
740 def ReadOneItem(type):
741 m = re.match(r'%s (\d+)' % type, f.readline())
742 if not m:
743 return None
744 data = f.read(int(m.group(1)))
745 if f.read(1) != '\n':
746 return None
747 return data
748
749 while True:
750 key = ReadOneItem('K')
751 if not key:
752 break
753 value = ReadOneItem('V')
754 if not value:
755 break
756 values[key] = value
757 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000758
759 @staticmethod
760 def GetCheckoutRoot(directory):
761 """Returns the top level directory of the current repository.
762
763 The directory is returned as an absolute path.
764 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000765 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000766 infos = SVN.CaptureInfo(directory, print_error=False)
767 cur_dir_repo_root = infos.get("Repository Root")
768 if not cur_dir_repo_root:
769 return None
770
771 while True:
772 parent = os.path.dirname(directory)
773 if (SVN.CaptureInfo(parent, print_error=False).get(
774 "Repository Root") != cur_dir_repo_root):
775 break
776 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000777 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000778
779 @staticmethod
780 def AssertVersion(min_version):
781 """Asserts svn's version is at least min_version."""
782 def only_int(val):
783 if val.isdigit():
784 return int(val)
785 else:
786 return 0
787 if not SVN.current_version:
788 SVN.current_version = SVN.Capture(['--version']).split()[2]
789 current_version_list = map(only_int, SVN.current_version.split('.'))
790 for min_ver in map(int, min_version.split('.')):
791 ver = current_version_list.pop(0)
792 if ver < min_ver:
793 return (False, SVN.current_version)
794 elif ver > min_ver:
795 return (True, SVN.current_version)
796 return (True, SVN.current_version)