1
# Copyright (C) 2011, 2012 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."""
19
from __future__ import absolute_import
29
lazy_import.lazy_import(globals(), """
44
from ...sixish import (
48
auto_option = config.Option(
49
'upload_auto', default=False, from_unicode=config.bool_from_store,
51
Whether upload should occur when the tip of the branch changes.
53
auto_quiet_option = config.Option(
54
'upload_auto_quiet', default=False, from_unicode=config.bool_from_store,
56
Whether upload should occur quietly.
58
location_option = config.Option(
59
'upload_location', default=None,
61
The url to upload the working tree to.
63
revid_location_option = config.Option(
64
'upload_revid_location', default=u'.bzr-upload.revid',
66
The relative path to be used to store the uploaded revid.
68
The only bzr-related info uploaded with the working tree is the corresponding
69
revision id. The uploaded working tree is not linked to any other bzr data.
71
If the layout of your remote server is such that you can't write in the
72
root directory but only in the directories inside that root, you will need
73
to use the 'upload_revid_location' configuration variable to specify the
74
relative path to be used. That configuration variable can be specified in
75
locations.conf or branch.conf.
77
For example, given the following layout:
83
you may have write access in 'private' and 'public' but in 'Project'
84
itself. In that case, you can add the following in your locations.conf or
87
upload_revid_location = private/.bzr-upload.revid
91
# FIXME: Add more tests around invalid paths or relative paths that doesn't
92
# exist on remote (if only to get proper error messages) for
93
# 'upload_revid_location'
96
class BzrUploader(object):
98
def __init__(self, branch, to_transport, outf, tree, rev_id,
101
self.to_transport = to_transport
106
self._pending_deletions = []
107
self._pending_renames = []
108
self._uploaded_revid = None
111
def _up_stat(self, relpath):
112
return self.to_transport.stat(urlutils.escape(relpath))
114
def _up_rename(self, old_path, new_path):
115
return self.to_transport.rename(urlutils.escape(old_path),
116
urlutils.escape(new_path))
118
def _up_delete(self, relpath):
119
return self.to_transport.delete(urlutils.escape(relpath))
121
def _up_delete_tree(self, relpath):
122
return self.to_transport.delete_tree(urlutils.escape(relpath))
124
def _up_mkdir(self, relpath, mode):
125
return self.to_transport.mkdir(urlutils.escape(relpath), mode)
127
def _up_rmdir(self, relpath):
128
return self.to_transport.rmdir(urlutils.escape(relpath))
130
def _up_put_bytes(self, relpath, bytes, mode):
131
self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
133
def _up_get_bytes(self, relpath):
134
return self.to_transport.get_bytes(urlutils.escape(relpath))
136
def set_uploaded_revid(self, rev_id):
137
# XXX: Add tests for concurrent updates, etc.
138
revid_path = self.branch.get_config_stack().get('upload_revid_location')
139
self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
140
self._uploaded_revid = rev_id
142
def get_uploaded_revid(self):
143
if self._uploaded_revid is None:
144
revid_path = self.branch.get_config_stack(
145
).get('upload_revid_location')
147
self._uploaded_revid = self._up_get_bytes(revid_path)
148
except errors.NoSuchFile:
149
# We have not uploaded to here.
150
self._uploaded_revid = revision.NULL_REVISION
151
return self._uploaded_revid
153
def _get_ignored(self):
154
if self._ignored is None:
156
ignore_file_path = '.bzrignore-upload'
157
ignore_file = self.tree.get_file(ignore_file_path)
158
except errors.NoSuchFile:
159
ignored_patterns = []
161
ignored_patterns = ignores.parse_ignore_file(ignore_file)
162
self._ignored = globbing.Globster(ignored_patterns)
165
def is_ignored(self, relpath):
166
glob = self._get_ignored()
167
ignored = glob.match(relpath)
170
# We still need to check that all parents are not ignored
171
dir = os.path.dirname(relpath)
172
while dir and not ignored:
173
ignored = glob.match(dir)
175
dir = os.path.dirname(dir)
178
def upload_file(self, old_relpath, new_relpath, id, mode=None):
180
if self.tree.is_executable(new_relpath, id):
185
self.outf.write('Uploading %s\n' % old_relpath)
186
self._up_put_bytes(old_relpath, self.tree.get_file_text(new_relpath, id), mode)
188
def _force_clear(self, relpath):
190
st = self._up_stat(relpath)
191
if stat.S_ISDIR(st.st_mode):
192
# A simple rmdir may not be enough
194
self.outf.write('Clearing %s/%s\n' % (
195
self.to_transport.external_url(), relpath))
196
self._up_delete_tree(relpath)
197
elif stat.S_ISLNK(st.st_mode):
199
self.outf.write('Clearing %s/%s\n' % (
200
self.to_transport.external_url(), relpath))
201
self._up_delete(relpath)
202
except errors.PathError:
205
def upload_file_robustly(self, relpath, id, mode=None):
206
"""Upload a file, clearing the way on the remote side.
208
When doing a full upload, it may happen that a directory exists where
209
we want to put our file.
211
self._force_clear(relpath)
212
self.upload_file(relpath, relpath, id, mode)
214
def upload_symlink(self, relpath, target):
215
self.to_transport.symlink(target, relpath)
217
def upload_symlink_robustly(self, relpath, target):
218
"""Handle uploading symlinks.
220
self._force_clear(relpath)
221
# Target might not be there at this time; dummy file should be
222
# overwritten at some point, possibly by another upload.
223
target = osutils.normpath(osutils.pathjoin(
224
osutils.dirname(relpath),
227
self.upload_symlink(relpath, target)
229
def make_remote_dir(self, relpath, mode=None):
232
self._up_mkdir(relpath, mode)
234
def make_remote_dir_robustly(self, relpath, mode=None):
235
"""Create a remote directory, clearing the way on the remote side.
237
When doing a full upload, it may happen that a file exists where we
238
want to create our directory.
241
st = self._up_stat(relpath)
242
if not stat.S_ISDIR(st.st_mode):
244
self.outf.write('Deleting %s/%s\n' % (
245
self.to_transport.external_url(), relpath))
246
self._up_delete(relpath)
248
# Ok the remote dir already exists, nothing to do
250
except errors.PathError:
252
self.make_remote_dir(relpath, mode)
254
def delete_remote_file(self, relpath):
256
self.outf.write('Deleting %s\n' % relpath)
257
self._up_delete(relpath)
259
def delete_remote_dir(self, relpath):
261
self.outf.write('Deleting %s\n' % relpath)
262
self._up_rmdir(relpath)
263
# XXX: Add a test where a subdir is ignored but we still want to
264
# delete the dir -- vila 100106
266
def delete_remote_dir_maybe(self, relpath):
267
"""Try to delete relpath, keeping failures to retry later."""
269
self._up_rmdir(relpath)
270
# any kind of PathError would be OK, though we normally expect
272
except errors.PathError:
273
self._pending_deletions.append(relpath)
275
def finish_deletions(self):
276
if self._pending_deletions:
277
# Process the previously failed deletions in reverse order to
278
# delete children before parents
279
for relpath in reversed(self._pending_deletions):
280
self._up_rmdir(relpath)
281
# The following shouldn't be needed since we use it once per
282
# upload, but better safe than sorry ;-)
283
self._pending_deletions = []
285
def rename_remote(self, old_relpath, new_relpath):
286
"""Rename a remote file or directory taking care of collisions.
288
To avoid collisions during bulk renames, each renamed target is
289
temporarily assigned a unique name. When all renames have been done,
290
each target get its proper name.
292
# We generate a sufficiently random name to *assume* that
293
# no collisions will occur and don't worry about it (nor
299
stamp = '.tmp.%.9f.%d.%d' % (time.time(),
301
random.randint(0, 0x7FFFFFFF))
303
self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
304
self._up_rename(old_relpath, stamp)
305
self._pending_renames.append((stamp, new_relpath))
307
def finish_renames(self):
308
for (stamp, new_path) in self._pending_renames:
309
self._up_rename(stamp, new_path)
310
# The following shouldn't be needed since we use it once per upload,
311
# but better safe than sorry ;-)
312
self._pending_renames = []
314
def upload_full_tree(self):
315
self.to_transport.ensure_base() # XXX: Handle errors (add
316
# --create-prefix option ?)
317
with self.tree.lock_read():
318
for relpath, ie in self.tree.iter_entries_by_dir():
319
if relpath in ('', '.bzrignore', '.bzrignore-upload'):
321
# .bzrignore and .bzrignore-upload have no meaning outside
322
# a working tree so do not upload them
324
if self.is_ignored(relpath):
326
self.outf.write('Ignoring %s\n' % relpath)
328
if ie.kind == 'file':
329
self.upload_file_robustly(relpath, ie.file_id)
330
elif ie.kind == 'symlink':
332
self.upload_symlink_robustly(relpath, ie.symlink_target)
333
except errors.TransportNotPossible:
335
target = self.tree.path_content_summary(relpath)[3]
336
self.outf.write('Not uploading symlink %s -> %s\n'
338
elif ie.kind == 'directory':
339
self.make_remote_dir_robustly(relpath)
341
raise NotImplementedError
342
self.set_uploaded_revid(self.rev_id)
344
def upload_tree(self):
345
# If we can't find the revid file on the remote location, upload the
347
rev_id = self.get_uploaded_revid()
349
if rev_id == revision.NULL_REVISION:
351
self.outf.write('No uploaded revision id found,'
352
' switching to full upload\n')
353
self.upload_full_tree()
357
# Check if the revision hasn't already been uploaded
358
if rev_id == self.rev_id:
360
self.outf.write('Remote location already up to date\n')
362
from_tree = self.branch.repository.revision_tree(rev_id)
363
self.to_transport.ensure_base() # XXX: Handle errors (add
364
# --create-prefix option ?)
365
changes = self.tree.changes_from(from_tree)
366
with self.tree.lock_read():
367
for (path, id, kind) in changes.removed:
368
if self.is_ignored(path):
370
self.outf.write('Ignoring %s\n' % path)
373
self.delete_remote_file(path)
374
elif kind is 'directory':
375
self.delete_remote_dir_maybe(path)
376
elif kind == 'symlink':
377
self.delete_remote_file(path)
379
raise NotImplementedError
381
for (old_path, new_path, id, kind,
382
content_change, exec_change) in changes.renamed:
383
if self.is_ignored(old_path) and self.is_ignored(new_path):
385
self.outf.write('Ignoring %s\n' % old_path)
386
self.outf.write('Ignoring %s\n' % new_path)
389
# We update the old_path content because renames and
390
# deletions are differed.
391
self.upload_file(old_path, new_path, id)
392
self.rename_remote(old_path, new_path)
393
self.finish_renames()
394
self.finish_deletions()
396
for (path, id, old_kind, new_kind) in changes.kind_changed:
397
if self.is_ignored(path):
399
self.outf.write('Ignoring %s\n' % path)
401
if old_kind in ('file', 'symlink'):
402
self.delete_remote_file(path)
403
elif old_kind == 'directory':
404
self.delete_remote_dir(path)
406
raise NotImplementedError
408
if new_kind == 'file':
409
self.upload_file(path, path, id)
410
elif new_kind == 'symlink':
411
target = self.tree.get_symlink_target(path)
412
self.upload_symlink(path, target)
413
elif new_kind is 'directory':
414
self.make_remote_dir(path)
416
raise NotImplementedError
418
for (path, id, kind) in changes.added:
419
if self.is_ignored(path):
421
self.outf.write('Ignoring %s\n' % path)
424
self.upload_file(path, path, id)
425
elif kind == 'directory':
426
self.make_remote_dir(path)
427
elif kind == 'symlink':
428
target = self.tree.get_symlink_target(path)
430
self.upload_symlink(path, target)
431
except errors.TransportNotPossible:
433
self.outf.write('Not uploading symlink %s -> %s\n'
436
raise NotImplementedError
438
# XXX: Add a test for exec_change
440
content_change, exec_change) in changes.modified:
441
if self.is_ignored(path):
443
self.outf.write('Ignoring %s\n' % path)
446
self.upload_file(path, path, id)
447
elif kind == 'symlink':
448
target = self.tree.get_symlink_target(path)
449
self.upload_symlink(path, target)
451
raise NotImplementedError
453
self.set_uploaded_revid(self.rev_id)
456
class CannotUploadToWorkingTree(errors.BzrCommandError):
458
_fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
461
class DivergedUploadedTree(errors.BzrCommandError):
463
_fmt = ("Your branch (%(revid)s)"
464
" and the uploaded tree (%(uploaded_revid)s) have diverged: ")
467
class cmd_upload(commands.Command):
468
"""Upload a working tree, as a whole or incrementally.
470
If no destination is specified use the last one used.
471
If no revision is specified upload the changes since the last upload.
473
Changes include files added, renamed, modified or removed.
475
_see_also = ['plugins/upload']
476
takes_args = ['location?']
481
option.Option('full', 'Upload the full working tree.'),
482
option.Option('quiet', 'Do not output what is being done.',
484
option.Option('directory',
485
help='Branch to upload from, '
486
'rather than the one containing the working directory.',
490
option.Option('auto',
491
'Trigger an upload from this branch whenever the tip '
495
def run(self, location=None, full=False, revision=None, remember=None,
496
directory=None, quiet=False, auto=None, overwrite=False
498
if directory is None:
502
relpath) = controldir.ControlDir.open_containing_tree_or_branch(
513
changes = wt.changes_from(wt.basis_tree())
515
if revision is None and changes.has_changed():
516
raise errors.UncommittedChanges(wt)
518
conf = branch.get_config_stack()
520
stored_loc = conf.get('upload_location')
521
if stored_loc is None:
522
raise errors.BzrCommandError(
523
'No upload location known or specified.')
525
# FIXME: Not currently tested
526
display_url = urlutils.unescape_for_display(stored_loc,
528
self.outf.write("Using saved location: %s\n" % display_url)
529
location = stored_loc
531
to_transport = transport.get_transport(location)
533
# Check that we are not uploading to a existing working tree.
535
to_bzr_dir = controldir.ControlDir.open_from_transport(
537
has_wt = to_bzr_dir.has_workingtree()
538
except errors.NotBranchError:
540
except errors.NotLocalUrl:
541
# The exception raised is a bit weird... but that's life.
545
raise CannotUploadToWorkingTree(url=location)
547
rev_id = branch.last_revision()
549
if len(revision) != 1:
550
raise errors.BzrCommandError(
551
'bzr upload --revision takes exactly 1 argument')
552
rev_id = revision[0].in_history(branch).rev_id
554
tree = branch.repository.revision_tree(rev_id)
556
uploader = BzrUploader(branch, to_transport, self.outf, tree,
560
prev_uploaded_rev_id = uploader.get_uploaded_revid()
561
graph = branch.repository.get_graph()
562
if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
563
raise DivergedUploadedTree(
564
revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
566
uploader.upload_full_tree()
568
uploader.upload_tree()
572
# We uploaded successfully, remember it
573
with branch.lock_write():
574
upload_location = conf.get('upload_location')
575
if upload_location is None or remember:
576
conf.set('upload_location',
577
urlutils.unescape(to_transport.base))
579
conf.set('upload_auto', auto)