169
173
bzrlib.api.require_any_api(bzrlib, bzr_compatible_versions)
177
lazy_import.lazy_import(globals(), """
196
version_info = (1, 0, 0, 'final', 0)
197
plugin_name = 'upload'
200
def _get_branch_option(branch, option):
201
return branch.get_config().get_user_option(option)
203
# FIXME: Get rid of that as soon as we depend on a bzr API that includes
204
# get_user_option_as_bool
205
def _get_branch_bool_option(branch, option):
206
conf = branch.get_config()
207
if hasattr(conf, 'get_user_option_as_bool'):
208
value = conf.get_user_option_as_bool(option)
210
value = conf.get_user_option(option)
211
if value is not None:
212
if value.lower().strip() == 'true':
218
def _set_branch_option(branch, option, value):
219
branch.get_config().set_user_option(option, value)
222
def get_upload_location(branch):
223
return _get_branch_option(branch, 'upload_location')
226
def set_upload_location(branch, location):
227
_set_branch_option(branch, 'upload_location', location)
230
# FIXME: Add more tests around invalid paths used here or relative paths that
231
# doesn't exist on remote (if only to get proper error messages)
232
def get_upload_revid_location(branch):
233
loc = _get_branch_option(branch, 'upload_revid_location')
235
loc = '.bzr-upload.revid'
239
def set_upload_revid_location(branch, location):
240
_set_branch_option(branch, 'upload_revid_location', location)
243
def get_upload_auto(branch):
244
auto = _get_branch_bool_option(branch, 'upload_auto')
246
auto = False # Default to False if not specified
250
def set_upload_auto(branch, auto):
251
# FIXME: What's the point in allowing a boolean here instead of requiring
252
# the callers to use strings instead ?
257
_set_branch_option(branch, 'upload_auto', auto_str)
260
def get_upload_auto_quiet(branch):
261
quiet = _get_branch_bool_option(branch, 'upload_auto_quiet')
263
quiet = False # Default to False if not specified
267
def set_upload_auto_quiet(branch, quiet):
268
_set_branch_option(branch, 'upload_auto_quiet', quiet)
271
class BzrUploader(object):
273
def __init__(self, branch, to_transport, outf, tree, rev_id,
276
self.to_transport = to_transport
281
self._pending_deletions = []
282
self._pending_renames = []
283
self._uploaded_revid = None
286
def _up_stat(self, relpath):
287
return self.to_transport.stat(urlutils.escape(relpath))
289
def _up_rename(self, old_path, new_path):
290
return self.to_transport.rename(urlutils.escape(old_path),
291
urlutils.escape(new_path))
293
def _up_delete(self, relpath):
294
return self.to_transport.delete(urlutils.escape(relpath))
296
def _up_delete_tree(self, relpath):
297
return self.to_transport.delete_tree(urlutils.escape(relpath))
299
def _up_mkdir(self, relpath, mode):
300
return self.to_transport.mkdir(urlutils.escape(relpath), mode)
302
def _up_rmdir(self, relpath):
303
return self.to_transport.rmdir(urlutils.escape(relpath))
305
def _up_put_bytes(self, relpath, bytes, mode):
306
self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
308
def _up_get_bytes(self, relpath):
309
return self.to_transport.get_bytes(urlutils.escape(relpath))
311
def set_uploaded_revid(self, rev_id):
312
# XXX: Add tests for concurrent updates, etc.
313
revid_path = get_upload_revid_location(self.branch)
314
self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
315
self._uploaded_revid = rev_id
317
def get_uploaded_revid(self):
318
if self._uploaded_revid is None:
319
revid_path = get_upload_revid_location(self.branch)
321
self._uploaded_revid = self._up_get_bytes(revid_path)
322
except errors.NoSuchFile:
323
# We have not upload to here.
324
self._uploaded_revid = revision.NULL_REVISION
325
return self._uploaded_revid
327
def _get_ignored(self):
328
if self._ignored is None:
330
ignore_file = self.tree.get_file_by_path('.bzrignore-upload')
331
ignored_patterns = ignores.parse_ignore_file(ignore_file)
332
except errors.NoSuchId:
333
ignored_patterns = []
334
self._ignored = globbing.Globster(ignored_patterns)
337
def is_ignored(self, relpath):
338
glob = self._get_ignored()
339
ignored = glob.match(relpath)
342
# We still need to check that all parents are not ignored
343
dir = os.path.dirname(relpath)
344
while dir and not ignored:
345
ignored = glob.match(dir)
347
dir = os.path.dirname(dir)
350
def upload_file(self, relpath, id, mode=None):
352
if self.tree.is_executable(id):
357
self.outf.write('Uploading %s\n' % relpath)
358
self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
360
def upload_file_robustly(self, relpath, id, mode=None):
361
"""Upload a file, clearing the way on the remote side.
363
When doing a full upload, it may happen that a directory exists where
364
we want to put our file.
367
st = self._up_stat(relpath)
368
if stat.S_ISDIR(st.st_mode):
369
# A simple rmdir may not be enough
371
self.outf.write('Clearing %s/%s\n' % (
372
self.to_transport.external_url(), relpath))
373
self._up_delete_tree(relpath)
374
except errors.PathError:
376
self.upload_file(relpath, id, mode)
378
def make_remote_dir(self, relpath, mode=None):
381
self._up_mkdir(relpath, mode)
383
def make_remote_dir_robustly(self, relpath, mode=None):
384
"""Create a remote directory, clearing the way on the remote side.
386
When doing a full upload, it may happen that a file exists where we
387
want to create our directory.
390
st = self._up_stat(relpath)
391
if not stat.S_ISDIR(st.st_mode):
393
self.outf.write('Deleting %s/%s\n' % (
394
self.to_transport.external_url(), relpath))
395
self._up_delete(relpath)
397
# Ok the remote dir already exists, nothing to do
399
except errors.PathError:
401
self.make_remote_dir(relpath, mode)
403
def delete_remote_file(self, relpath):
405
self.outf.write('Deleting %s\n' % relpath)
406
self._up_delete(relpath)
408
def delete_remote_dir(self, relpath):
410
self.outf.write('Deleting %s\n' % relpath)
411
self._up_rmdir(relpath)
412
# XXX: Add a test where a subdir is ignored but we still want to
413
# delete the dir -- vila 100106
415
def delete_remote_dir_maybe(self, relpath):
416
"""Try to delete relpath, keeping failures to retry later."""
418
self._up_rmdir(relpath)
419
# any kind of PathError would be OK, though we normally expect
421
except errors.PathError:
422
self._pending_deletions.append(relpath)
424
def finish_deletions(self):
425
if self._pending_deletions:
426
# Process the previously failed deletions in reverse order to
427
# delete children before parents
428
for relpath in reversed(self._pending_deletions):
429
self._up_rmdir(relpath)
430
# The following shouldn't be needed since we use it once per
431
# upload, but better safe than sorry ;-)
432
self._pending_deletions = []
434
def rename_remote(self, old_relpath, new_relpath):
435
"""Rename a remote file or directory taking care of collisions.
437
To avoid collisions during bulk renames, each renamed target is
438
temporarily assigned a unique name. When all renames have been done,
439
each target get its proper name.
441
# We generate a sufficiently random name to *assume* that
442
# no collisions will occur and don't worry about it (nor
448
stamp = '.tmp.%.9f.%d.%d' % (time.time(),
450
random.randint(0,0x7FFFFFFF))
452
self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
453
self._up_rename(old_relpath, stamp)
454
self._pending_renames.append((stamp, new_relpath))
456
def finish_renames(self):
457
for (stamp, new_path) in self._pending_renames:
458
self._up_rename(stamp, new_path)
459
# The following shouldn't be needed since we use it once per upload,
460
# but better safe than sorry ;-)
461
self._pending_renames = []
463
def upload_full_tree(self):
464
self.to_transport.ensure_base() # XXX: Handle errors (add
465
# --create-prefix option ?)
466
self.tree.lock_read()
468
for relpath, ie in self.tree.inventory.iter_entries():
469
if relpath in ('', '.bzrignore', '.bzrignore-upload'):
471
# .bzrignore and .bzrignore-upload have no meaning outside
472
# a working tree so do not upload them
474
if self.is_ignored(relpath):
476
self.outf.write('Ignoring %s\n' % relpath)
478
if ie.kind == 'file':
479
self.upload_file_robustly(relpath, ie.file_id)
480
elif ie.kind == 'directory':
481
self.make_remote_dir_robustly(relpath)
482
elif ie.kind == 'symlink':
484
target = self.tree.path_content_summary(path)[3]
485
self.outf.write('Not uploading symlink %s -> %s\n'
488
raise NotImplementedError
489
self.set_uploaded_revid(self.rev_id)
493
def upload_tree(self):
494
# If we can't find the revid file on the remote location, upload the
496
rev_id = self.get_uploaded_revid()
498
if rev_id == revision.NULL_REVISION:
500
self.outf.write('No uploaded revision id found,'
501
' switching to full upload\n')
502
self.upload_full_tree()
506
# Check if the revision hasn't already been uploaded
507
if rev_id == self.rev_id:
509
self.outf.write('Remote location already up to date\n')
511
from_tree = self.branch.repository.revision_tree(rev_id)
512
self.to_transport.ensure_base() # XXX: Handle errors (add
513
# --create-prefix option ?)
514
changes = self.tree.changes_from(from_tree)
515
self.tree.lock_read()
517
for (path, id, kind) in changes.removed:
518
if self.is_ignored(path):
520
self.outf.write('Ignoring %s\n' % path)
523
self.delete_remote_file(path)
524
elif kind is 'directory':
525
self.delete_remote_dir_maybe(path)
526
elif kind == 'symlink':
528
target = self.tree.path_content_summary(path)[3]
529
self.outf.write('Not deleting remote symlink %s -> %s\n'
532
raise NotImplementedError
534
for (old_path, new_path, id, kind,
535
content_change, exec_change) in changes.renamed:
536
if self.is_ignored(old_path) and self.is_ignored(new_path):
538
self.outf.write('Ignoring %s\n' % old_path)
539
self.outf.write('Ignoring %s\n' % new_path)
542
# We update the old_path content because renames and
543
# deletions are differed.
544
self.upload_file(old_path, id)
545
if kind == 'symlink':
547
self.outf.write('Not renaming remote symlink %s to %s\n'
548
% (old_path, new_path))
550
self.rename_remote(old_path, new_path)
551
self.finish_renames()
552
self.finish_deletions()
554
for (path, id, old_kind, new_kind) in changes.kind_changed:
555
if self.is_ignored(path):
557
self.outf.write('Ignoring %s\n' % path)
559
if old_kind == 'file':
560
self.delete_remote_file(path)
561
elif old_kind == 'directory':
562
self.delete_remote_dir(path)
564
raise NotImplementedError
566
if new_kind == 'file':
567
self.upload_file(path, id)
568
elif new_kind is 'directory':
569
self.make_remote_dir(path)
571
raise NotImplementedError
573
for (path, id, kind) in changes.added:
574
if self.is_ignored(path):
576
self.outf.write('Ignoring %s\n' % path)
579
self.upload_file(path, id)
580
elif kind == 'directory':
581
self.make_remote_dir(path)
582
elif kind == 'symlink':
584
target = self.tree.path_content_summary(path)[3]
585
self.outf.write('Not uploading symlink %s -> %s\n'
588
raise NotImplementedError
590
# XXX: Add a test for exec_change
592
content_change, exec_change) in changes.modified:
593
if self.is_ignored(path):
595
self.outf.write('Ignoring %s\n' % path)
598
self.upload_file(path, id)
600
raise NotImplementedError
602
self.set_uploaded_revid(self.rev_id)
607
class CannotUploadToWorkingTree(errors.BzrCommandError):
609
_fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
612
class DivergedUploadedTree(errors.BzrCommandError):
614
_fmt = ("Your branch (%(revid)s)"
615
" and the uploaded tree (%(uploaded_revid)s) have diverged: ")
618
class cmd_upload(commands.Command):
619
"""Upload a working tree, as a whole or incrementally.
621
If no destination is specified use the last one used.
622
If no revision is specified upload the changes since the last upload.
624
Changes include files added, renamed, modified or removed.
626
_see_also = ['plugins/upload']
627
takes_args = ['location?']
632
option.Option('full', 'Upload the full working tree.'),
633
option.Option('quiet', 'Do not output what is being done.',
635
option.Option('directory',
636
help='Branch to upload from, '
637
'rather than the one containing the working directory.',
641
option.Option('auto',
642
'Trigger an upload from this branch whenever the tip '
646
def run(self, location=None, full=False, revision=None, remember=None,
647
directory=None, quiet=False, auto=None, overwrite=False
649
if directory is None:
652
if auto and not auto_hook_available:
653
raise BzrCommandError("Your version of bzr does not have the "
654
"hooks necessary for --auto to work")
657
relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
667
changes = wt.changes_from(wt.basis_tree())
669
if revision is None and changes.has_changed():
670
raise errors.UncommittedChanges(wt)
673
stored_loc = get_upload_location(branch)
674
if stored_loc is None:
675
raise errors.BzrCommandError(
676
'No upload location known or specified.')
678
# FIXME: Not currently tested
679
display_url = urlutils.unescape_for_display(stored_loc,
681
self.outf.write("Using saved location: %s\n" % display_url)
682
location = stored_loc
684
to_transport = transport.get_transport(location)
686
# Check that we are not uploading to a existing working tree.
688
to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport)
689
has_wt = to_bzr_dir.has_workingtree()
690
except errors.NotBranchError:
692
except errors.NotLocalUrl:
693
# The exception raised is a bit weird... but that's life.
697
raise CannotUploadToWorkingTree(url=location)
699
rev_id = branch.last_revision()
701
if len(revision) != 1:
702
raise errors.BzrCommandError(
703
'bzr upload --revision takes exactly 1 argument')
704
rev_id = revision[0].in_history(branch).rev_id
706
tree = branch.repository.revision_tree(rev_id)
708
uploader = BzrUploader(branch, to_transport, self.outf, tree,
712
prev_uploaded_rev_id = uploader.get_uploaded_revid()
713
graph = branch.repository.get_graph()
714
if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
715
raise DivergedUploadedTree(
716
revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
718
uploader.upload_full_tree()
720
uploader.upload_tree()
724
# We uploaded successfully, remember it
725
if get_upload_location(branch) is None or remember:
726
set_upload_location(branch, urlutils.unescape(to_transport.base))
728
set_upload_auto(branch, auto)
731
commands.register_command(cmd_upload)
175
from bzrlib.commands import plugin_cmds
177
plugin_cmds.register_lazy('cmd_upload', [], 'bzrlib.plugins.upload.cmds')
734
179
def auto_upload_hook(params):
186
from bzrlib.plugins.upload.cmds import (
190
get_upload_auto_quiet,
735
193
source_branch = params.branch
736
194
destination = get_upload_location(source_branch)
737
195
if destination is None: