1
# Copyright (C) 2008, 2009, 2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""bzr-upload command implementations."""
25
lazy_import.lazy_import(globals(), """
44
def _get_branch_option(branch, option):
45
return branch.get_config().get_user_option(option)
47
# FIXME: Get rid of that as soon as we depend on a bzr API that includes
48
# get_user_option_as_bool
49
def _get_branch_bool_option(branch, option):
50
conf = branch.get_config()
51
if hasattr(conf, 'get_user_option_as_bool'):
52
value = conf.get_user_option_as_bool(option)
54
value = conf.get_user_option(option)
56
if value.lower().strip() == 'true':
62
def _set_branch_option(branch, option, value):
63
branch.get_config().set_user_option(option, value)
66
def get_upload_location(branch):
67
return _get_branch_option(branch, 'upload_location')
70
def set_upload_location(branch, location):
71
_set_branch_option(branch, 'upload_location', location)
74
# FIXME: Add more tests around invalid paths used here or relative paths that
75
# doesn't exist on remote (if only to get proper error messages)
76
def get_upload_revid_location(branch):
77
loc = _get_branch_option(branch, 'upload_revid_location')
79
loc = '.bzr-upload.revid'
83
def set_upload_revid_location(branch, location):
84
_set_branch_option(branch, 'upload_revid_location', location)
87
def get_upload_auto(branch):
88
auto = _get_branch_bool_option(branch, 'upload_auto')
90
auto = False # Default to False if not specified
94
def set_upload_auto(branch, auto):
95
# FIXME: What's the point in allowing a boolean here instead of requiring
96
# the callers to use strings instead ?
101
_set_branch_option(branch, 'upload_auto', auto_str)
104
def get_upload_auto_quiet(branch):
105
quiet = _get_branch_bool_option(branch, 'upload_auto_quiet')
107
quiet = False # Default to False if not specified
111
def set_upload_auto_quiet(branch, quiet):
112
_set_branch_option(branch, 'upload_auto_quiet', quiet)
115
class BzrUploader(object):
117
def __init__(self, branch, to_transport, outf, tree, rev_id,
120
self.to_transport = to_transport
125
self._pending_deletions = []
126
self._pending_renames = []
127
self._uploaded_revid = None
130
def _up_stat(self, relpath):
131
return self.to_transport.stat(urlutils.escape(relpath))
133
def _up_rename(self, old_path, new_path):
134
return self.to_transport.rename(urlutils.escape(old_path),
135
urlutils.escape(new_path))
137
def _up_delete(self, relpath):
138
return self.to_transport.delete(urlutils.escape(relpath))
140
def _up_delete_tree(self, relpath):
141
return self.to_transport.delete_tree(urlutils.escape(relpath))
143
def _up_mkdir(self, relpath, mode):
144
return self.to_transport.mkdir(urlutils.escape(relpath), mode)
146
def _up_rmdir(self, relpath):
147
return self.to_transport.rmdir(urlutils.escape(relpath))
149
def _up_put_bytes(self, relpath, bytes, mode):
150
self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
152
def _up_get_bytes(self, relpath):
153
return self.to_transport.get_bytes(urlutils.escape(relpath))
155
def set_uploaded_revid(self, rev_id):
156
# XXX: Add tests for concurrent updates, etc.
157
revid_path = get_upload_revid_location(self.branch)
158
self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
159
self._uploaded_revid = rev_id
161
def get_uploaded_revid(self):
162
if self._uploaded_revid is None:
163
revid_path = get_upload_revid_location(self.branch)
165
self._uploaded_revid = self._up_get_bytes(revid_path)
166
except errors.NoSuchFile:
167
# We have not upload to here.
168
self._uploaded_revid = revision.NULL_REVISION
169
return self._uploaded_revid
171
def _get_ignored(self):
172
if self._ignored is None:
174
ignore_file = self.tree.get_file_by_path('.bzrignore-upload')
175
ignored_patterns = ignores.parse_ignore_file(ignore_file)
176
except errors.NoSuchId:
177
ignored_patterns = []
178
self._ignored = globbing.Globster(ignored_patterns)
181
def is_ignored(self, relpath):
182
glob = self._get_ignored()
183
ignored = glob.match(relpath)
186
# We still need to check that all parents are not ignored
187
dir = os.path.dirname(relpath)
188
while dir and not ignored:
189
ignored = glob.match(dir)
191
dir = os.path.dirname(dir)
194
def upload_file(self, relpath, id, mode=None):
196
if self.tree.is_executable(id):
201
self.outf.write('Uploading %s\n' % relpath)
202
self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
204
def upload_file_robustly(self, relpath, id, mode=None):
205
"""Upload a file, clearing the way on the remote side.
207
When doing a full upload, it may happen that a directory exists where
208
we want to put our file.
211
st = self._up_stat(relpath)
212
if stat.S_ISDIR(st.st_mode):
213
# A simple rmdir may not be enough
215
self.outf.write('Clearing %s/%s\n' % (
216
self.to_transport.external_url(), relpath))
217
self._up_delete_tree(relpath)
218
except errors.PathError:
220
self.upload_file(relpath, id, mode)
222
def make_remote_dir(self, relpath, mode=None):
225
self._up_mkdir(relpath, mode)
227
def make_remote_dir_robustly(self, relpath, mode=None):
228
"""Create a remote directory, clearing the way on the remote side.
230
When doing a full upload, it may happen that a file exists where we
231
want to create our directory.
234
st = self._up_stat(relpath)
235
if not stat.S_ISDIR(st.st_mode):
237
self.outf.write('Deleting %s/%s\n' % (
238
self.to_transport.external_url(), relpath))
239
self._up_delete(relpath)
241
# Ok the remote dir already exists, nothing to do
243
except errors.PathError:
245
self.make_remote_dir(relpath, mode)
247
def delete_remote_file(self, relpath):
249
self.outf.write('Deleting %s\n' % relpath)
250
self._up_delete(relpath)
252
def delete_remote_dir(self, relpath):
254
self.outf.write('Deleting %s\n' % relpath)
255
self._up_rmdir(relpath)
256
# XXX: Add a test where a subdir is ignored but we still want to
257
# delete the dir -- vila 100106
259
def delete_remote_dir_maybe(self, relpath):
260
"""Try to delete relpath, keeping failures to retry later."""
262
self._up_rmdir(relpath)
263
# any kind of PathError would be OK, though we normally expect
265
except errors.PathError:
266
self._pending_deletions.append(relpath)
268
def finish_deletions(self):
269
if self._pending_deletions:
270
# Process the previously failed deletions in reverse order to
271
# delete children before parents
272
for relpath in reversed(self._pending_deletions):
273
self._up_rmdir(relpath)
274
# The following shouldn't be needed since we use it once per
275
# upload, but better safe than sorry ;-)
276
self._pending_deletions = []
278
def rename_remote(self, old_relpath, new_relpath):
279
"""Rename a remote file or directory taking care of collisions.
281
To avoid collisions during bulk renames, each renamed target is
282
temporarily assigned a unique name. When all renames have been done,
283
each target get its proper name.
285
# We generate a sufficiently random name to *assume* that
286
# no collisions will occur and don't worry about it (nor
292
stamp = '.tmp.%.9f.%d.%d' % (time.time(),
294
random.randint(0,0x7FFFFFFF))
296
self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
297
self._up_rename(old_relpath, stamp)
298
self._pending_renames.append((stamp, new_relpath))
300
def finish_renames(self):
301
for (stamp, new_path) in self._pending_renames:
302
self._up_rename(stamp, new_path)
303
# The following shouldn't be needed since we use it once per upload,
304
# but better safe than sorry ;-)
305
self._pending_renames = []
307
def upload_full_tree(self):
308
self.to_transport.ensure_base() # XXX: Handle errors (add
309
# --create-prefix option ?)
310
self.tree.lock_read()
312
for relpath, ie in self.tree.inventory.iter_entries():
313
if relpath in ('', '.bzrignore', '.bzrignore-upload'):
315
# .bzrignore and .bzrignore-upload have no meaning outside
316
# a working tree so do not upload them
318
if self.is_ignored(relpath):
320
self.outf.write('Ignoring %s\n' % relpath)
322
if ie.kind == 'file':
323
self.upload_file_robustly(relpath, ie.file_id)
324
elif ie.kind == 'directory':
325
self.make_remote_dir_robustly(relpath)
326
elif ie.kind == 'symlink':
328
target = self.tree.path_content_summary(path)[3]
329
self.outf.write('Not uploading symlink %s -> %s\n'
332
raise NotImplementedError
333
self.set_uploaded_revid(self.rev_id)
337
def upload_tree(self):
338
# If we can't find the revid file on the remote location, upload the
340
rev_id = self.get_uploaded_revid()
342
if rev_id == revision.NULL_REVISION:
344
self.outf.write('No uploaded revision id found,'
345
' switching to full upload\n')
346
self.upload_full_tree()
350
# Check if the revision hasn't already been uploaded
351
if rev_id == self.rev_id:
353
self.outf.write('Remote location already up to date\n')
355
from_tree = self.branch.repository.revision_tree(rev_id)
356
self.to_transport.ensure_base() # XXX: Handle errors (add
357
# --create-prefix option ?)
358
changes = self.tree.changes_from(from_tree)
359
self.tree.lock_read()
361
for (path, id, kind) in changes.removed:
362
if self.is_ignored(path):
364
self.outf.write('Ignoring %s\n' % path)
367
self.delete_remote_file(path)
368
elif kind is 'directory':
369
self.delete_remote_dir_maybe(path)
370
elif kind == 'symlink':
372
target = self.tree.path_content_summary(path)[3]
373
self.outf.write('Not deleting remote symlink %s -> %s\n'
376
raise NotImplementedError
378
for (old_path, new_path, id, kind,
379
content_change, exec_change) in changes.renamed:
380
if self.is_ignored(old_path) and self.is_ignored(new_path):
382
self.outf.write('Ignoring %s\n' % old_path)
383
self.outf.write('Ignoring %s\n' % new_path)
386
# We update the old_path content because renames and
387
# deletions are differed.
388
self.upload_file(old_path, id)
389
if kind == 'symlink':
391
self.outf.write('Not renaming remote symlink %s to %s\n'
392
% (old_path, new_path))
394
self.rename_remote(old_path, new_path)
395
self.finish_renames()
396
self.finish_deletions()
398
for (path, id, old_kind, new_kind) in changes.kind_changed:
399
if self.is_ignored(path):
401
self.outf.write('Ignoring %s\n' % path)
403
if old_kind == 'file':
404
self.delete_remote_file(path)
405
elif old_kind == 'directory':
406
self.delete_remote_dir(path)
408
raise NotImplementedError
410
if new_kind == 'file':
411
self.upload_file(path, id)
412
elif new_kind is 'directory':
413
self.make_remote_dir(path)
415
raise NotImplementedError
417
for (path, id, kind) in changes.added:
418
if self.is_ignored(path):
420
self.outf.write('Ignoring %s\n' % path)
423
self.upload_file(path, id)
424
elif kind == 'directory':
425
self.make_remote_dir(path)
426
elif kind == 'symlink':
428
target = self.tree.path_content_summary(path)[3]
429
self.outf.write('Not uploading symlink %s -> %s\n'
432
raise NotImplementedError
434
# XXX: Add a test for exec_change
436
content_change, exec_change) in changes.modified:
437
if self.is_ignored(path):
439
self.outf.write('Ignoring %s\n' % path)
442
self.upload_file(path, id)
444
raise NotImplementedError
446
self.set_uploaded_revid(self.rev_id)
451
class CannotUploadToWorkingTree(errors.BzrCommandError):
453
_fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
456
class DivergedUploadedTree(errors.BzrCommandError):
458
_fmt = ("Your branch (%(revid)s)"
459
" and the uploaded tree (%(uploaded_revid)s) have diverged: ")
462
class cmd_upload(commands.Command):
463
"""Upload a working tree, as a whole or incrementally.
465
If no destination is specified use the last one used.
466
If no revision is specified upload the changes since the last upload.
468
Changes include files added, renamed, modified or removed.
470
_see_also = ['plugins/upload']
471
takes_args = ['location?']
476
option.Option('full', 'Upload the full working tree.'),
477
option.Option('quiet', 'Do not output what is being done.',
479
option.Option('directory',
480
help='Branch to upload from, '
481
'rather than the one containing the working directory.',
485
option.Option('auto',
486
'Trigger an upload from this branch whenever the tip '
490
def run(self, location=None, full=False, revision=None, remember=None,
491
directory=None, quiet=False, auto=None, overwrite=False
493
if directory is None:
496
if auto and not auto_hook_available:
497
raise BzrCommandError("Your version of bzr does not have the "
498
"hooks necessary for --auto to work")
501
relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
511
changes = wt.changes_from(wt.basis_tree())
513
if revision is None and changes.has_changed():
514
raise errors.UncommittedChanges(wt)
517
stored_loc = get_upload_location(branch)
518
if stored_loc is None:
519
raise errors.BzrCommandError(
520
'No upload location known or specified.')
522
# FIXME: Not currently tested
523
display_url = urlutils.unescape_for_display(stored_loc,
525
self.outf.write("Using saved location: %s\n" % display_url)
526
location = stored_loc
528
to_transport = transport.get_transport(location)
530
# Check that we are not uploading to a existing working tree.
532
to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport)
533
has_wt = to_bzr_dir.has_workingtree()
534
except errors.NotBranchError:
536
except errors.NotLocalUrl:
537
# The exception raised is a bit weird... but that's life.
541
raise CannotUploadToWorkingTree(url=location)
543
rev_id = branch.last_revision()
545
if len(revision) != 1:
546
raise errors.BzrCommandError(
547
'bzr upload --revision takes exactly 1 argument')
548
rev_id = revision[0].in_history(branch).rev_id
550
tree = branch.repository.revision_tree(rev_id)
552
uploader = BzrUploader(branch, to_transport, self.outf, tree,
556
prev_uploaded_rev_id = uploader.get_uploaded_revid()
557
graph = branch.repository.get_graph()
558
if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
559
raise DivergedUploadedTree(
560
revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
562
uploader.upload_full_tree()
564
uploader.upload_tree()
568
# We uploaded successfully, remember it
569
if get_upload_location(branch) is None or remember:
570
set_upload_location(branch, urlutils.unescape(to_transport.base))
572
set_upload_auto(branch, auto)
575
def auto_upload_hook(params):
576
source_branch = params.branch
577
destination = get_upload_location(source_branch)
578
if destination is None:
580
auto_upload = get_upload_auto(source_branch)
583
quiet = get_upload_auto_quiet(source_branch)
585
display_url = urlutils.unescape_for_display(
586
destination, osutils.get_terminal_encoding())
587
trace.note('Automatically uploading to %s', display_url)
588
to_transport = transport.get_transport(destination)
589
last_revision = source_branch.last_revision()
590
last_tree = source_branch.repository.revision_tree(last_revision)
591
uploader = BzrUploader(source_branch, to_transport, sys.stdout,
592
last_tree, last_revision, quiet=quiet)
593
uploader.upload_tree()
596
def install_auto_upload_hook():
597
branch.Branch.hooks.install_named_hook('post_change_branch_tip',
599
'Auto upload code from a branch when it is changed.')
602
if hasattr(branch.Branch.hooks, "install_named_hook"):
603
install_auto_upload_hook()
604
auto_hook_available = True
606
auto_hook_available = False
609
def load_tests(basic_tests, module, loader):
610
# This module shouldn't define any tests but I don't know how to report
611
# that. I prefer to update basic_tests with the other tests to detect
612
# unwanted tests and I think that's sufficient.
617
basic_tests.addTest(loader.loadTestsFromModuleNames(
618
["%s.%s" % (__name__, tmn) for tmn in testmod_names]))