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
28
lazy_import.lazy_import(globals(), """
43
auto_option = config.Option(
44
'upload_auto', default=False, from_unicode=config.bool_from_store,
46
Whether upload should occur when the tip of the branch changes.
48
auto_quiet_option = config.Option(
49
'upload_auto_quiet', default=False, from_unicode=config.bool_from_store,
51
Whether upload should occur quietly.
53
location_option = config.Option(
54
'upload_location', default=None,
56
The url to upload the working tree to.
58
revid_location_option = config.Option(
59
'upload_revid_location', default=u'.bzr-upload.revid',
61
The relative path to be used to store the uploaded revid.
63
The only bzr-related info uploaded with the working tree is the corresponding
64
revision id. The uploaded working tree is not linked to any other bzr data.
66
If the layout of your remote server is such that you can't write in the
67
root directory but only in the directories inside that root, you will need
68
to use the 'upload_revid_location' configuration variable to specify the
69
relative path to be used. That configuration variable can be specified in
70
locations.conf or branch.conf.
72
For example, given the following layout:
78
you may have write access in 'private' and 'public' but in 'Project'
79
itself. In that case, you can add the following in your locations.conf or
82
upload_revid_location = private/.bzr-upload.revid
86
# FIXME: Add more tests around invalid paths or relative paths that doesn't
87
# exist on remote (if only to get proper error messages) for
88
# 'upload_revid_location'
91
class BzrUploader(object):
93
def __init__(self, branch, to_transport, outf, tree, rev_id,
96
self.to_transport = to_transport
101
self._pending_deletions = []
102
self._pending_renames = []
103
self._uploaded_revid = None
106
def _up_stat(self, relpath):
107
return self.to_transport.stat(urlutils.escape(relpath))
109
def _up_rename(self, old_path, new_path):
110
return self.to_transport.rename(urlutils.escape(old_path),
111
urlutils.escape(new_path))
113
def _up_delete(self, relpath):
114
return self.to_transport.delete(urlutils.escape(relpath))
116
def _up_delete_tree(self, relpath):
117
return self.to_transport.delete_tree(urlutils.escape(relpath))
119
def _up_mkdir(self, relpath, mode):
120
return self.to_transport.mkdir(urlutils.escape(relpath), mode)
122
def _up_rmdir(self, relpath):
123
return self.to_transport.rmdir(urlutils.escape(relpath))
125
def _up_put_bytes(self, relpath, bytes, mode):
126
self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
128
def _up_get_bytes(self, relpath):
129
return self.to_transport.get_bytes(urlutils.escape(relpath))
131
def set_uploaded_revid(self, rev_id):
132
# XXX: Add tests for concurrent updates, etc.
133
revid_path = self.branch.get_config_stack().get('upload_revid_location')
134
self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
135
self._uploaded_revid = rev_id
137
def get_uploaded_revid(self):
138
if self._uploaded_revid is None:
139
revid_path = self.branch.get_config_stack(
140
).get('upload_revid_location')
142
self._uploaded_revid = self._up_get_bytes(revid_path)
143
except errors.NoSuchFile:
144
# We have not uploaded to here.
145
self._uploaded_revid = revision.NULL_REVISION
146
return self._uploaded_revid
148
def _get_ignored(self):
149
if self._ignored is None:
151
ignore_file_path = '.bzrignore-upload'
152
ignore_file_id = self.tree.path2id(ignore_file_path)
153
ignore_file = self.tree.get_file(ignore_file_id,
155
ignored_patterns = ignores.parse_ignore_file(ignore_file)
156
except errors.NoSuchId:
157
ignored_patterns = []
158
self._ignored = globbing.Globster(ignored_patterns)
161
def is_ignored(self, relpath):
162
glob = self._get_ignored()
163
ignored = glob.match(relpath)
166
# We still need to check that all parents are not ignored
167
dir = os.path.dirname(relpath)
168
while dir and not ignored:
169
ignored = glob.match(dir)
171
dir = os.path.dirname(dir)
174
def upload_file(self, relpath, id, mode=None):
176
if self.tree.is_executable(id):
181
self.outf.write('Uploading %s\n' % relpath)
182
self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
184
def upload_file_robustly(self, relpath, id, mode=None):
185
"""Upload a file, clearing the way on the remote side.
187
When doing a full upload, it may happen that a directory exists where
188
we want to put our file.
191
st = self._up_stat(relpath)
192
if stat.S_ISDIR(st.st_mode):
193
# A simple rmdir may not be enough
195
self.outf.write('Clearing %s/%s\n' % (
196
self.to_transport.external_url(), relpath))
197
self._up_delete_tree(relpath)
198
except errors.PathError:
200
self.upload_file(relpath, id, mode)
202
def make_remote_dir(self, relpath, mode=None):
205
self._up_mkdir(relpath, mode)
207
def make_remote_dir_robustly(self, relpath, mode=None):
208
"""Create a remote directory, clearing the way on the remote side.
210
When doing a full upload, it may happen that a file exists where we
211
want to create our directory.
214
st = self._up_stat(relpath)
215
if not stat.S_ISDIR(st.st_mode):
217
self.outf.write('Deleting %s/%s\n' % (
218
self.to_transport.external_url(), relpath))
219
self._up_delete(relpath)
221
# Ok the remote dir already exists, nothing to do
223
except errors.PathError:
225
self.make_remote_dir(relpath, mode)
227
def delete_remote_file(self, relpath):
229
self.outf.write('Deleting %s\n' % relpath)
230
self._up_delete(relpath)
232
def delete_remote_dir(self, relpath):
234
self.outf.write('Deleting %s\n' % relpath)
235
self._up_rmdir(relpath)
236
# XXX: Add a test where a subdir is ignored but we still want to
237
# delete the dir -- vila 100106
239
def delete_remote_dir_maybe(self, relpath):
240
"""Try to delete relpath, keeping failures to retry later."""
242
self._up_rmdir(relpath)
243
# any kind of PathError would be OK, though we normally expect
245
except errors.PathError:
246
self._pending_deletions.append(relpath)
248
def finish_deletions(self):
249
if self._pending_deletions:
250
# Process the previously failed deletions in reverse order to
251
# delete children before parents
252
for relpath in reversed(self._pending_deletions):
253
self._up_rmdir(relpath)
254
# The following shouldn't be needed since we use it once per
255
# upload, but better safe than sorry ;-)
256
self._pending_deletions = []
258
def rename_remote(self, old_relpath, new_relpath):
259
"""Rename a remote file or directory taking care of collisions.
261
To avoid collisions during bulk renames, each renamed target is
262
temporarily assigned a unique name. When all renames have been done,
263
each target get its proper name.
265
# We generate a sufficiently random name to *assume* that
266
# no collisions will occur and don't worry about it (nor
272
stamp = '.tmp.%.9f.%d.%d' % (time.time(),
274
random.randint(0,0x7FFFFFFF))
276
self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
277
self._up_rename(old_relpath, stamp)
278
self._pending_renames.append((stamp, new_relpath))
280
def finish_renames(self):
281
for (stamp, new_path) in self._pending_renames:
282
self._up_rename(stamp, new_path)
283
# The following shouldn't be needed since we use it once per upload,
284
# but better safe than sorry ;-)
285
self._pending_renames = []
287
def upload_full_tree(self):
288
self.to_transport.ensure_base() # XXX: Handle errors (add
289
# --create-prefix option ?)
290
self.tree.lock_read()
292
for relpath, ie in self.tree.iter_entries_by_dir():
293
if relpath in ('', '.bzrignore', '.bzrignore-upload'):
295
# .bzrignore and .bzrignore-upload have no meaning outside
296
# a working tree so do not upload them
298
if self.is_ignored(relpath):
300
self.outf.write('Ignoring %s\n' % relpath)
302
if ie.kind == 'file':
303
self.upload_file_robustly(relpath, ie.file_id)
304
elif ie.kind == 'directory':
305
self.make_remote_dir_robustly(relpath)
306
elif ie.kind == 'symlink':
308
target = self.tree.path_content_summary(relpath)[3]
309
self.outf.write('Not uploading symlink %s -> %s\n'
312
raise NotImplementedError
313
self.set_uploaded_revid(self.rev_id)
317
def upload_tree(self):
318
# If we can't find the revid file on the remote location, upload the
320
rev_id = self.get_uploaded_revid()
322
if rev_id == revision.NULL_REVISION:
324
self.outf.write('No uploaded revision id found,'
325
' switching to full upload\n')
326
self.upload_full_tree()
330
# Check if the revision hasn't already been uploaded
331
if rev_id == self.rev_id:
333
self.outf.write('Remote location already up to date\n')
335
from_tree = self.branch.repository.revision_tree(rev_id)
336
self.to_transport.ensure_base() # XXX: Handle errors (add
337
# --create-prefix option ?)
338
changes = self.tree.changes_from(from_tree)
339
self.tree.lock_read()
341
for (path, id, kind) in changes.removed:
342
if self.is_ignored(path):
344
self.outf.write('Ignoring %s\n' % path)
347
self.delete_remote_file(path)
348
elif kind is 'directory':
349
self.delete_remote_dir_maybe(path)
350
elif kind == 'symlink':
352
target = self.tree.path_content_summary(path)[3]
353
self.outf.write('Not deleting remote symlink %s -> %s\n'
356
raise NotImplementedError
358
for (old_path, new_path, id, kind,
359
content_change, exec_change) in changes.renamed:
360
if self.is_ignored(old_path) and self.is_ignored(new_path):
362
self.outf.write('Ignoring %s\n' % old_path)
363
self.outf.write('Ignoring %s\n' % new_path)
366
# We update the old_path content because renames and
367
# deletions are differed.
368
self.upload_file(old_path, id)
369
if kind == 'symlink':
371
self.outf.write('Not renaming remote symlink %s to %s\n'
372
% (old_path, new_path))
374
self.rename_remote(old_path, new_path)
375
self.finish_renames()
376
self.finish_deletions()
378
for (path, id, old_kind, new_kind) in changes.kind_changed:
379
if self.is_ignored(path):
381
self.outf.write('Ignoring %s\n' % path)
383
if old_kind == 'file':
384
self.delete_remote_file(path)
385
elif old_kind == 'directory':
386
self.delete_remote_dir(path)
388
raise NotImplementedError
390
if new_kind == 'file':
391
self.upload_file(path, id)
392
elif new_kind is 'directory':
393
self.make_remote_dir(path)
395
raise NotImplementedError
397
for (path, id, kind) in changes.added:
398
if self.is_ignored(path):
400
self.outf.write('Ignoring %s\n' % path)
403
self.upload_file(path, id)
404
elif kind == 'directory':
405
self.make_remote_dir(path)
406
elif kind == 'symlink':
408
target = self.tree.path_content_summary(path)[3]
409
self.outf.write('Not uploading symlink %s -> %s\n'
412
raise NotImplementedError
414
# XXX: Add a test for exec_change
416
content_change, exec_change) in changes.modified:
417
if self.is_ignored(path):
419
self.outf.write('Ignoring %s\n' % path)
422
self.upload_file(path, id)
424
raise NotImplementedError
426
self.set_uploaded_revid(self.rev_id)
431
class CannotUploadToWorkingTree(errors.BzrCommandError):
433
_fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
436
class DivergedUploadedTree(errors.BzrCommandError):
438
_fmt = ("Your branch (%(revid)s)"
439
" and the uploaded tree (%(uploaded_revid)s) have diverged: ")
442
class cmd_upload(commands.Command):
443
"""Upload a working tree, as a whole or incrementally.
445
If no destination is specified use the last one used.
446
If no revision is specified upload the changes since the last upload.
448
Changes include files added, renamed, modified or removed.
450
_see_also = ['plugins/upload']
451
takes_args = ['location?']
456
option.Option('full', 'Upload the full working tree.'),
457
option.Option('quiet', 'Do not output what is being done.',
459
option.Option('directory',
460
help='Branch to upload from, '
461
'rather than the one containing the working directory.',
465
option.Option('auto',
466
'Trigger an upload from this branch whenever the tip '
470
def run(self, location=None, full=False, revision=None, remember=None,
471
directory=None, quiet=False, auto=None, overwrite=False
473
if directory is None:
477
relpath) = controldir.ControlDir.open_containing_tree_or_branch(
488
changes = wt.changes_from(wt.basis_tree())
490
if revision is None and changes.has_changed():
491
raise errors.UncommittedChanges(wt)
493
conf = branch.get_config_stack()
495
stored_loc = conf.get('upload_location')
496
if stored_loc is None:
497
raise errors.BzrCommandError(
498
'No upload location known or specified.')
500
# FIXME: Not currently tested
501
display_url = urlutils.unescape_for_display(stored_loc,
503
self.outf.write("Using saved location: %s\n" % display_url)
504
location = stored_loc
506
to_transport = transport.get_transport(location)
508
# Check that we are not uploading to a existing working tree.
510
to_bzr_dir = controldir.ControlDir.open_from_transport(
512
has_wt = to_bzr_dir.has_workingtree()
513
except errors.NotBranchError:
515
except errors.NotLocalUrl:
516
# The exception raised is a bit weird... but that's life.
520
raise CannotUploadToWorkingTree(url=location)
522
rev_id = branch.last_revision()
524
if len(revision) != 1:
525
raise errors.BzrCommandError(
526
'bzr upload --revision takes exactly 1 argument')
527
rev_id = revision[0].in_history(branch).rev_id
529
tree = branch.repository.revision_tree(rev_id)
531
uploader = BzrUploader(branch, to_transport, self.outf, tree,
535
prev_uploaded_rev_id = uploader.get_uploaded_revid()
536
graph = branch.repository.get_graph()
537
if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
538
raise DivergedUploadedTree(
539
revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
541
uploader.upload_full_tree()
543
uploader.upload_tree()
547
# We uploaded successfully, remember it
550
upload_location = conf.get('upload_location')
551
if upload_location is None or remember:
552
conf.set('upload_location',
553
urlutils.unescape(to_transport.base))
555
conf.set('upload_auto', auto)