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."""
27
lazy_import.lazy_import(globals(), """
41
auto_option = config.Option(
42
'upload_auto', default=False, from_unicode=config.bool_from_store,
44
Whether upload should occur when the tip of the branch changes.
46
auto_quiet_option = config.Option(
47
'upload_auto_quiet', default=False, from_unicode=config.bool_from_store,
49
Whether upload should occur quietly.
51
location_option = config.Option(
52
'upload_location', default=None,
54
The url to upload the working tree to.
56
revid_location_option = config.Option(
57
'upload_revid_location', default=u'.bzr-upload.revid',
59
The relative path to be used to store the uploaded revid.
61
The only bzr-related info uploaded with the working tree is the corresponding
62
revision id. The uploaded working tree is not linked to any other bzr data.
64
If the layout of your remote server is such that you can't write in the
65
root directory but only in the directories inside that root, you will need
66
to use the 'upload_revid_location' configuration variable to specify the
67
relative path to be used. That configuration variable can be specified in
68
locations.conf or branch.conf.
70
For example, given the following layout:
76
you may have write access in 'private' and 'public' but in 'Project'
77
itself. In that case, you can add the following in your locations.conf or
80
upload_revid_location = private/.bzr-upload.revid
84
# FIXME: Add more tests around invalid paths or relative paths that doesn't
85
# exist on remote (if only to get proper error messages) for
86
# 'upload_revid_location'
89
class BzrUploader(object):
91
def __init__(self, branch, to_transport, outf, tree, rev_id,
94
self.to_transport = to_transport
99
self._pending_deletions = []
100
self._pending_renames = []
101
self._uploaded_revid = None
104
def _up_stat(self, relpath):
105
return self.to_transport.stat(urlutils.escape(relpath))
107
def _up_rename(self, old_path, new_path):
108
return self.to_transport.rename(urlutils.escape(old_path),
109
urlutils.escape(new_path))
111
def _up_delete(self, relpath):
112
return self.to_transport.delete(urlutils.escape(relpath))
114
def _up_delete_tree(self, relpath):
115
return self.to_transport.delete_tree(urlutils.escape(relpath))
117
def _up_mkdir(self, relpath, mode):
118
return self.to_transport.mkdir(urlutils.escape(relpath), mode)
120
def _up_rmdir(self, relpath):
121
return self.to_transport.rmdir(urlutils.escape(relpath))
123
def _up_put_bytes(self, relpath, bytes, mode):
124
self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
126
def _up_get_bytes(self, relpath):
127
return self.to_transport.get_bytes(urlutils.escape(relpath))
129
def set_uploaded_revid(self, rev_id):
130
# XXX: Add tests for concurrent updates, etc.
131
revid_path = self.branch.get_config_stack().get('upload_revid_location')
132
self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
133
self._uploaded_revid = rev_id
135
def get_uploaded_revid(self):
136
if self._uploaded_revid is None:
137
revid_path = self.branch.get_config_stack(
138
).get('upload_revid_location')
140
self._uploaded_revid = self._up_get_bytes(revid_path)
141
except errors.NoSuchFile:
142
# We have not uploaded to here.
143
self._uploaded_revid = revision.NULL_REVISION
144
return self._uploaded_revid
146
def _get_ignored(self):
147
if self._ignored is None:
149
ignore_file_path = '.bzrignore-upload'
150
ignore_file = self.tree.get_file(ignore_file_path)
151
except errors.NoSuchFile:
152
ignored_patterns = []
154
ignored_patterns = ignores.parse_ignore_file(ignore_file)
155
self._ignored = globbing.Globster(ignored_patterns)
158
def is_ignored(self, relpath):
159
glob = self._get_ignored()
160
ignored = glob.match(relpath)
163
# We still need to check that all parents are not ignored
164
dir = os.path.dirname(relpath)
165
while dir and not ignored:
166
ignored = glob.match(dir)
168
dir = os.path.dirname(dir)
171
def upload_file(self, old_relpath, new_relpath, mode=None):
173
if self.tree.is_executable(new_relpath):
178
self.outf.write('Uploading %s\n' % old_relpath)
180
old_relpath, self.tree.get_file_text(new_relpath), mode)
182
def _force_clear(self, relpath):
184
st = self._up_stat(relpath)
185
if stat.S_ISDIR(st.st_mode):
186
# A simple rmdir may not be enough
188
self.outf.write('Clearing %s/%s\n' % (
189
self.to_transport.external_url(), relpath))
190
self._up_delete_tree(relpath)
191
elif stat.S_ISLNK(st.st_mode):
193
self.outf.write('Clearing %s/%s\n' % (
194
self.to_transport.external_url(), relpath))
195
self._up_delete(relpath)
196
except errors.PathError:
199
def upload_file_robustly(self, relpath, mode=None):
200
"""Upload a file, clearing the way on the remote side.
202
When doing a full upload, it may happen that a directory exists where
203
we want to put our file.
205
self._force_clear(relpath)
206
self.upload_file(relpath, relpath, mode)
208
def upload_symlink(self, relpath, target):
209
self.to_transport.symlink(target, relpath)
211
def upload_symlink_robustly(self, relpath, target):
212
"""Handle uploading symlinks.
214
self._force_clear(relpath)
215
# Target might not be there at this time; dummy file should be
216
# overwritten at some point, possibly by another upload.
217
target = osutils.normpath(osutils.pathjoin(
218
osutils.dirname(relpath),
221
self.upload_symlink(relpath, target)
223
def make_remote_dir(self, relpath, mode=None):
226
self._up_mkdir(relpath, mode)
228
def make_remote_dir_robustly(self, relpath, mode=None):
229
"""Create a remote directory, clearing the way on the remote side.
231
When doing a full upload, it may happen that a file exists where we
232
want to create our directory.
235
st = self._up_stat(relpath)
236
if not stat.S_ISDIR(st.st_mode):
238
self.outf.write('Deleting %s/%s\n' % (
239
self.to_transport.external_url(), relpath))
240
self._up_delete(relpath)
242
# Ok the remote dir already exists, nothing to do
244
except errors.PathError:
246
self.make_remote_dir(relpath, mode)
248
def delete_remote_file(self, relpath):
250
self.outf.write('Deleting %s\n' % relpath)
251
self._up_delete(relpath)
253
def delete_remote_dir(self, relpath):
255
self.outf.write('Deleting %s\n' % relpath)
256
self._up_rmdir(relpath)
257
# XXX: Add a test where a subdir is ignored but we still want to
258
# delete the dir -- vila 100106
260
def delete_remote_dir_maybe(self, relpath):
261
"""Try to delete relpath, keeping failures to retry later."""
263
self._up_rmdir(relpath)
264
# any kind of PathError would be OK, though we normally expect
266
except errors.PathError:
267
self._pending_deletions.append(relpath)
269
def finish_deletions(self):
270
if self._pending_deletions:
271
# Process the previously failed deletions in reverse order to
272
# delete children before parents
273
for relpath in reversed(self._pending_deletions):
274
self._up_rmdir(relpath)
275
# The following shouldn't be needed since we use it once per
276
# upload, but better safe than sorry ;-)
277
self._pending_deletions = []
279
def rename_remote(self, old_relpath, new_relpath):
280
"""Rename a remote file or directory taking care of collisions.
282
To avoid collisions during bulk renames, each renamed target is
283
temporarily assigned a unique name. When all renames have been done,
284
each target get its proper name.
286
# We generate a sufficiently random name to *assume* that
287
# no collisions will occur and don't worry about it (nor
293
stamp = '.tmp.%.9f.%d.%d' % (time.time(),
295
random.randint(0, 0x7FFFFFFF))
297
self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
298
self._up_rename(old_relpath, stamp)
299
self._pending_renames.append((stamp, new_relpath))
301
def finish_renames(self):
302
for (stamp, new_path) in self._pending_renames:
303
self._up_rename(stamp, new_path)
304
# The following shouldn't be needed since we use it once per upload,
305
# but better safe than sorry ;-)
306
self._pending_renames = []
308
def upload_full_tree(self):
309
self.to_transport.ensure_base() # XXX: Handle errors (add
310
# --create-prefix option ?)
311
with self.tree.lock_read():
312
for relpath, ie in self.tree.iter_entries_by_dir():
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)
324
elif ie.kind == 'symlink':
326
self.upload_symlink_robustly(
327
relpath, ie.symlink_target)
328
except errors.TransportNotPossible:
330
target = self.tree.path_content_summary(relpath)[3]
331
self.outf.write('Not uploading symlink %s -> %s\n'
333
elif ie.kind == 'directory':
334
self.make_remote_dir_robustly(relpath)
336
raise NotImplementedError
337
self.set_uploaded_revid(self.rev_id)
339
def upload_tree(self):
340
# If we can't find the revid file on the remote location, upload the
342
rev_id = self.get_uploaded_revid()
344
if rev_id == revision.NULL_REVISION:
346
self.outf.write('No uploaded revision id found,'
347
' switching to full upload\n')
348
self.upload_full_tree()
352
# Check if the revision hasn't already been uploaded
353
if rev_id == self.rev_id:
355
self.outf.write('Remote location already up to date\n')
357
from_tree = self.branch.repository.revision_tree(rev_id)
358
self.to_transport.ensure_base() # XXX: Handle errors (add
359
# --create-prefix option ?)
360
changes = self.tree.changes_from(from_tree)
361
with self.tree.lock_read():
362
for change in changes.removed:
363
if self.is_ignored(change.path[0]):
365
self.outf.write('Ignoring %s\n' % change.path[0])
367
if change.kind[0] == 'file':
368
self.delete_remote_file(change.path[0])
369
elif change.kind[0] == 'directory':
370
self.delete_remote_dir_maybe(change.path[0])
371
elif change.kind[0] == 'symlink':
372
self.delete_remote_file(change.path[0])
374
raise NotImplementedError
376
for change in changes.renamed:
377
if self.is_ignored(change.path[0]) and self.is_ignored(change.path[1]):
379
self.outf.write('Ignoring %s\n' % change.path[0])
380
self.outf.write('Ignoring %s\n' % change.path[1])
382
if change.changed_content:
383
# We update the change.path[0] content because renames and
384
# deletions are differed.
385
self.upload_file(change.path[0], change.path[1])
386
self.rename_remote(change.path[0], change.path[1])
387
self.finish_renames()
388
self.finish_deletions()
390
for change in changes.kind_changed:
391
if self.is_ignored(change.path[1]):
393
self.outf.write('Ignoring %s\n' % change.path[1])
395
if change.kind[0] in ('file', 'symlink'):
396
self.delete_remote_file(change.path[0])
397
elif change.kind[0] == 'directory':
398
self.delete_remote_dir(change.path[0])
400
raise NotImplementedError
402
if change.kind[1] == 'file':
403
self.upload_file(change.path[1], change.path[1])
404
elif change.kind[1] == 'symlink':
405
target = self.tree.get_symlink_target(change.path[1])
406
self.upload_symlink(change.path[1], target)
407
elif change.kind[1] == 'directory':
408
self.make_remote_dir(change.path[1])
410
raise NotImplementedError
412
for change in changes.added + changes.copied:
413
if self.is_ignored(change.path[1]):
415
self.outf.write('Ignoring %s\n' % change.path[1])
417
if change.kind[1] == 'file':
418
self.upload_file(change.path[1], change.path[1])
419
elif change.kind[1] == 'directory':
420
self.make_remote_dir(change.path[1])
421
elif change.kind[1] == 'symlink':
422
target = self.tree.get_symlink_target(change.path[1])
424
self.upload_symlink(change.path[1], target)
425
except errors.TransportNotPossible:
427
self.outf.write('Not uploading symlink %s -> %s\n'
428
% (change.path[1], target))
430
raise NotImplementedError
432
# XXX: Add a test for exec_change
433
for change in changes.modified:
434
if self.is_ignored(change.path[1]):
436
self.outf.write('Ignoring %s\n' % change.path[1])
438
if change.kind[1] == 'file':
439
self.upload_file(change.path[1], change.path[1])
440
elif change.kind[1] == 'symlink':
441
target = self.tree.get_symlink_target(change.path[1])
442
self.upload_symlink(change.path[1], target)
444
raise NotImplementedError
446
self.set_uploaded_revid(self.rev_id)
449
class CannotUploadToWorkingTree(errors.BzrCommandError):
451
_fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
454
class DivergedUploadedTree(errors.BzrCommandError):
456
_fmt = ("Your branch (%(revid)s)"
457
" and the uploaded tree (%(uploaded_revid)s) have diverged: ")
460
class cmd_upload(commands.Command):
461
"""Upload a working tree, as a whole or incrementally.
463
If no destination is specified use the last one used.
464
If no revision is specified upload the changes since the last upload.
466
Changes include files added, renamed, modified or removed.
468
_see_also = ['plugins/upload']
469
takes_args = ['location?']
474
option.Option('full', 'Upload the full working tree.'),
475
option.Option('quiet', 'Do not output what is being done.',
477
option.Option('directory',
478
help='Branch to upload from, '
479
'rather than the one containing the working directory.',
483
option.Option('auto',
484
'Trigger an upload from this branch whenever the tip '
488
def run(self, location=None, full=False, revision=None, remember=None,
489
directory=None, quiet=False, auto=None, overwrite=False
491
if directory is None:
495
relpath) = controldir.ControlDir.open_containing_tree_or_branch(
502
with locked.lock_read():
504
changes = wt.changes_from(wt.basis_tree())
506
if revision is None and changes.has_changed():
507
raise errors.UncommittedChanges(wt)
509
conf = branch.get_config_stack()
511
stored_loc = conf.get('upload_location')
512
if stored_loc is None:
513
raise errors.BzrCommandError(
514
'No upload location known or specified.')
516
# FIXME: Not currently tested
517
display_url = urlutils.unescape_for_display(stored_loc,
519
self.outf.write("Using saved location: %s\n" % display_url)
520
location = stored_loc
522
to_transport = transport.get_transport(location)
524
# Check that we are not uploading to a existing working tree.
526
to_bzr_dir = controldir.ControlDir.open_from_transport(
528
has_wt = to_bzr_dir.has_workingtree()
529
except errors.NotBranchError:
531
except errors.NotLocalUrl:
532
# The exception raised is a bit weird... but that's life.
536
raise CannotUploadToWorkingTree(url=location)
538
rev_id = branch.last_revision()
540
if len(revision) != 1:
541
raise errors.BzrCommandError(
542
'bzr upload --revision takes exactly 1 argument')
543
rev_id = revision[0].in_history(branch).rev_id
545
tree = branch.repository.revision_tree(rev_id)
547
uploader = BzrUploader(branch, to_transport, self.outf, tree,
551
prev_uploaded_rev_id = uploader.get_uploaded_revid()
552
graph = branch.repository.get_graph()
553
if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
554
raise DivergedUploadedTree(
555
revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
557
uploader.upload_full_tree()
559
uploader.upload_tree()
561
# We uploaded successfully, remember it
562
with branch.lock_write():
563
upload_location = conf.get('upload_location')
564
if upload_location is None or remember:
565
conf.set('upload_location',
566
urlutils.unescape(to_transport.base))
568
conf.set('upload_auto', auto)