82 """An unexpected state was encountered by an automated step.""" |
78 """An unexpected state was encountered by an automated step.""" |
83 |
79 |
84 |
80 |
85 class FileAccessError(Error): |
81 class FileAccessError(Error): |
86 """An error occured while accessing a file.""" |
82 """An error occured while accessing a file.""" |
87 |
|
88 |
|
89 def run(argv, cwd=None, capture=False, split_capture=True, stdin=''): |
|
90 """Run the given command and optionally return its output. |
|
91 |
|
92 Note that if you set capture=True, the command's output is |
|
93 buffered in memory. Output capture should only be used with |
|
94 commands that output small amounts of data. O(kB) is fine, O(MB) |
|
95 is starting to push it a little. |
|
96 |
|
97 Args: |
|
98 argv: A list containing the name of the program to run, followed |
|
99 by its argument vector. |
|
100 cwd: Run the program from this directory. |
|
101 capture: If True, capture the program's stdout stream. If False, |
|
102 stdout will output to sys.stdout. |
|
103 split_capture: If True, return the captured output as a list of |
|
104 lines. Else, return as a single unaltered string. |
|
105 stdin: The string to feed to the program's stdin stream. |
|
106 |
|
107 Returns: |
|
108 If capture is True, a string containing the combined |
|
109 stdout/stderr output of the program. If capture is False, |
|
110 nothing is returned. |
|
111 |
|
112 Raises: |
|
113 SubprocessFailed: The subprocess exited with a non-zero exit |
|
114 code. |
|
115 """ |
|
116 print util.colorize('# ' + ' '.join(argv), util.WHITE, bold=True) |
|
117 |
|
118 process = subprocess.Popen(argv, |
|
119 shell=False, |
|
120 cwd=cwd, |
|
121 stdin=subprocess.PIPE, |
|
122 stdout=(subprocess.PIPE if capture else None), |
|
123 stderr=None) |
|
124 output, _ = process.communicate(input=stdin) |
|
125 if process.returncode != 0: |
|
126 raise SubprocessFailed('Process %s failed with output: %s' % |
|
127 (argv[0], output)) |
|
128 if output is not None and split_capture: |
|
129 return output.strip().split('\n') |
|
130 else: |
|
131 return output |
|
132 |
83 |
133 |
84 |
134 def error(msg): |
85 def error(msg): |
135 """Log an error message.""" |
86 """Log an error message.""" |
136 print util.colorize(msg, util.RED, bold=True) |
87 print util.colorize(msg, util.RED, bold=True) |
305 Args: |
256 Args: |
306 url: The Subversion repository URL to check out. |
257 url: The Subversion repository URL to check out. |
307 depth: The depth of the working copy root. |
258 depth: The depth of the working copy root. |
308 """ |
259 """ |
309 assert not self.exists() |
260 assert not self.exists() |
310 run(['svn', 'checkout', '--depth=' + depth, url, self.path()]) |
261 util.run(['svn', 'checkout', '--depth=' + depth, url, self.path()]) |
311 |
262 |
312 def update(self, path='', depth=None): |
263 def update(self, path='', depth=None): |
313 """Update a working copy path, optionally changing depth. |
264 """Update a working copy path, optionally changing depth. |
314 |
265 |
315 Args: |
266 Args: |
316 path: The working copy path to update. |
267 path: The working copy path to update. |
317 depth: If set, change the depth of the path before updating. |
268 depth: If set, change the depth of the path before updating. |
318 """ |
269 """ |
319 assert self.exists() |
270 assert self.exists() |
320 if depth is None: |
271 if depth is None: |
321 run(['svn', 'update', self.path(path)]) |
272 util.run(['svn', 'update', self.path(path)]) |
322 else: |
273 else: |
323 run(['svn', 'update', '--set-depth=' + depth, self.path(path)]) |
274 util.run(['svn', 'update', '--set-depth=' + depth, self.path(path)]) |
324 |
275 |
325 def revert(self, path=''): |
276 def revert(self, path=''): |
326 """Recursively revert a working copy path. |
277 """Recursively revert a working copy path. |
327 |
278 |
328 Note that this command is more zealous than the 'svn revert' |
279 Note that this command is more zealous than the 'svn revert' |
329 command, as it will also delete any files which subversion |
280 command, as it will also delete any files which subversion |
330 does not know about. |
281 does not know about. |
331 """ |
282 """ |
332 run(['svn', 'revert', '-R', self.path(path)]) |
283 util.run(['svn', 'revert', '-R', self.path(path)]) |
333 |
284 |
334 unknown, missing = self._unknownAndMissing(path) |
285 unknown, missing = self._unknownAndMissing(path) |
335 unknown = [os.path.join(self.path(path), p) for p in unknown] |
286 unknown = [os.path.join(self.path(path), p) for p in unknown] |
336 |
287 |
337 if unknown: |
288 if unknown: |
338 # rm -rf makes me uneasy. Verify that all paths to be deleted |
289 # rm -rf makes me uneasy. Verify that all paths to be deleted |
339 # are within the release working copy. |
290 # are within the release working copy. |
340 for p in unknown: |
291 for p in unknown: |
341 assert p.startswith(self.path()) |
292 assert p.startswith(self.path()) |
342 |
293 |
343 run(['rm', '-rf', '--'] + unknown) |
294 util.run(['rm', '-rf', '--'] + unknown) |
344 |
295 |
345 def ls(self, dir=''): |
296 def ls(self, dir=''): |
346 """List the contents of a working copy directory. |
297 """List the contents of a working copy directory. |
347 |
298 |
348 Note that this returns the contents of the directory as seen |
299 Note that this returns the contents of the directory as seen |
349 by the server, not constrained by the depth settings of the |
300 by the server, not constrained by the depth settings of the |
350 local path. |
301 local path. |
351 """ |
302 """ |
352 assert self.exists() |
303 assert self.exists() |
353 return run(['svn', 'ls', self.path(dir)], capture=True) |
304 return util.run(['svn', 'ls', self.path(dir)], capture=True) |
354 |
305 |
355 def copy(self, src, dest): |
306 def copy(self, src, dest): |
356 """Copy a working copy path. |
307 """Copy a working copy path. |
357 |
308 |
358 The copy is only scheduled for commit, not committed. |
309 The copy is only scheduled for commit, not committed. |
360 Args: |
311 Args: |
361 src: The source working copy path. |
312 src: The source working copy path. |
362 dst: The destination working copy path. |
313 dst: The destination working copy path. |
363 """ |
314 """ |
364 assert self.exists() |
315 assert self.exists() |
365 run(['svn', 'cp', self.path(src), self.path(dest)]) |
316 util.run(['svn', 'cp', self.path(src), self.path(dest)]) |
366 |
317 |
367 def propget(self, prop_name, path): |
318 def propget(self, prop_name, path): |
368 """Get the value of a property on a working copy path. |
319 """Get the value of a property on a working copy path. |
369 |
320 |
370 Args: |
321 Args: |
371 prop_name: The property name, eg. 'svn:externals'. |
322 prop_name: The property name, eg. 'svn:externals'. |
372 path: The working copy path on which the property is set. |
323 path: The working copy path on which the property is set. |
373 """ |
324 """ |
374 assert self.exists() |
325 assert self.exists() |
375 return run(['svn', 'propget', prop_name, self.path(path)], capture=True) |
326 return util.run(['svn', 'propget', prop_name, self.path(path)], |
|
327 capture=True) |
376 |
328 |
377 def propset(self, prop_name, prop_value, path): |
329 def propset(self, prop_name, prop_value, path): |
378 """Set the value of a property on a working copy path. |
330 """Set the value of a property on a working copy path. |
379 |
331 |
380 The property change is only scheduled for commit, not committed. |
332 The property change is only scheduled for commit, not committed. |
383 prop_name: The property name, eg. 'svn:externals'. |
335 prop_name: The property name, eg. 'svn:externals'. |
384 prop_value: The value that should be set. |
336 prop_value: The value that should be set. |
385 path: The working copy path on which to set the property. |
337 path: The working copy path on which to set the property. |
386 """ |
338 """ |
387 assert self.exists() |
339 assert self.exists() |
388 run(['svn', 'propset', prop_name, prop_value, self.path(path)]) |
340 util.run(['svn', 'propset', prop_name, prop_value, self.path(path)]) |
389 |
341 |
390 def add(self, paths): |
342 def add(self, paths): |
391 """Schedule working copy paths for addition. |
343 """Schedule working copy paths for addition. |
392 |
344 |
393 The paths are only scheduled for addition, not committed. |
345 The paths are only scheduled for addition, not committed. |
515 A string containing the changes extracted from the remote |
467 A string containing the changes extracted from the remote |
516 repository, in unified diff format suitable for application |
468 repository, in unified diff format suitable for application |
517 using 'patch'. |
469 using 'patch'. |
518 """ |
470 """ |
519 try: |
471 try: |
520 return run(['svn', 'diff', '-c', str(revision), url], |
472 return util.run(['svn', 'diff', '-c', str(revision), url], |
521 capture=True, split_capture=False) |
473 capture=True, split_capture=False) |
522 except SubprocessFailed: |
474 except util.SubprocessFailed: |
523 raise ExpectationFailed('Could not get diff for r%d ' |
475 raise ExpectationFailed('Could not get diff for r%d ' |
524 'from remote repository' % revision) |
476 'from remote repository' % revision) |
525 |
477 |
526 |
478 |
527 # |
479 # |
789 diff = self.wc.diff(self.upstream_repos + '/trunk', rev) |
741 diff = self.wc.diff(self.upstream_repos + '/trunk', rev) |
790 if not diff.strip(): |
742 if not diff.strip(): |
791 raise ExpectationFailed( |
743 raise ExpectationFailed( |
792 'Retrieved diff is empty. ' |
744 'Retrieved diff is empty. ' |
793 'Did you accidentally cherry-pick a branch change?') |
745 'Did you accidentally cherry-pick a branch change?') |
794 run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir), stdin=diff) |
746 util.run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir), |
|
747 stdin=diff) |
795 self.wc.addRemove(self.branch_dir) |
748 self.wc.addRemove(self.branch_dir) |
796 |
749 |
797 yaml_path = self.wc.path(self._branchPath('app/app.yaml')) |
750 yaml_path = self.wc.path(self._branchPath('app/app.yaml')) |
798 out = [] |
751 out = [] |
799 updated_patchlevel = False |
752 updated_patchlevel = False |