/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/plugins/upload/cmds.py

  • Committer: Martin
  • Date: 2017-06-09 16:31:49 UTC
  • mto: This revision was merged to the branch mainline in revision 6673.
  • Revision ID: gzlist@googlemail.com-20170609163149-liveiasey25480q6
Make InventoryDeltaError use string formatting, and repr for fileids

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2011, 2012 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""bzr-upload command implementations."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from ... import (
 
22
    branch,
 
23
    commands,
 
24
    config,
 
25
    lazy_import,
 
26
    option,
 
27
    )
 
28
lazy_import.lazy_import(globals(), """
 
29
import stat
 
30
import sys
 
31
 
 
32
from breezy import (
 
33
    controldir,
 
34
    errors,
 
35
    globbing,
 
36
    ignores,
 
37
    revision,
 
38
    transport,
 
39
    urlutils,
 
40
    )
 
41
""")
 
42
 
 
43
auto_option = config.Option(
 
44
    'upload_auto', default=False, from_unicode=config.bool_from_store,
 
45
    help="""\
 
46
Whether upload should occur when the tip of the branch changes.
 
47
""")
 
48
auto_quiet_option = config.Option(
 
49
    'upload_auto_quiet', default=False, from_unicode=config.bool_from_store,
 
50
    help="""\
 
51
Whether upload should occur quietly.
 
52
""")
 
53
location_option = config.Option(
 
54
    'upload_location', default=None,
 
55
    help="""\
 
56
The url to upload the working tree to.
 
57
""")
 
58
revid_location_option = config.Option(
 
59
    'upload_revid_location', default=u'.bzr-upload.revid',
 
60
    help="""\
 
61
The relative path to be used to store the uploaded revid.
 
62
 
 
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.
 
65
 
 
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.
 
71
 
 
72
For example, given the following layout:
 
73
 
 
74
  Project/
 
75
    private/
 
76
    public/
 
77
 
 
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
 
80
branch.conf file:
 
81
 
 
82
  upload_revid_location = private/.bzr-upload.revid
 
83
""")
 
84
 
 
85
 
 
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'
 
89
 
 
90
 
 
91
class BzrUploader(object):
 
92
 
 
93
    def __init__(self, branch, to_transport, outf, tree, rev_id,
 
94
                 quiet=False):
 
95
        self.branch = branch
 
96
        self.to_transport = to_transport
 
97
        self.outf = outf
 
98
        self.tree = tree
 
99
        self.rev_id = rev_id
 
100
        self.quiet = quiet
 
101
        self._pending_deletions = []
 
102
        self._pending_renames = []
 
103
        self._uploaded_revid = None
 
104
        self._ignored = None
 
105
 
 
106
    def _up_stat(self, relpath):
 
107
        return self.to_transport.stat(urlutils.escape(relpath))
 
108
 
 
109
    def _up_rename(self, old_path, new_path):
 
110
        return self.to_transport.rename(urlutils.escape(old_path),
 
111
                                        urlutils.escape(new_path))
 
112
 
 
113
    def _up_delete(self, relpath):
 
114
        return self.to_transport.delete(urlutils.escape(relpath))
 
115
 
 
116
    def _up_delete_tree(self, relpath):
 
117
        return self.to_transport.delete_tree(urlutils.escape(relpath))
 
118
 
 
119
    def _up_mkdir(self, relpath, mode):
 
120
        return self.to_transport.mkdir(urlutils.escape(relpath), mode)
 
121
 
 
122
    def _up_rmdir(self, relpath):
 
123
        return self.to_transport.rmdir(urlutils.escape(relpath))
 
124
 
 
125
    def _up_put_bytes(self, relpath, bytes, mode):
 
126
        self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
 
127
 
 
128
    def _up_get_bytes(self, relpath):
 
129
        return self.to_transport.get_bytes(urlutils.escape(relpath))
 
130
 
 
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
 
136
 
 
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')
 
141
            try:
 
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
 
147
 
 
148
    def _get_ignored(self):
 
149
        if self._ignored is None:
 
150
            try:
 
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,
 
154
                                                 ignore_file_path)
 
155
                ignored_patterns = ignores.parse_ignore_file(ignore_file)
 
156
            except errors.NoSuchId:
 
157
                ignored_patterns = []
 
158
            self._ignored = globbing.Globster(ignored_patterns)
 
159
        return self._ignored
 
160
 
 
161
    def is_ignored(self, relpath):
 
162
        glob = self._get_ignored()
 
163
        ignored = glob.match(relpath)
 
164
        import os
 
165
        if not ignored:
 
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)
 
170
                if not ignored:
 
171
                    dir = os.path.dirname(dir)
 
172
        return ignored
 
173
 
 
174
    def upload_file(self, relpath, id, mode=None):
 
175
        if mode is None:
 
176
            if self.tree.is_executable(id):
 
177
                mode = 0775
 
178
            else:
 
179
                mode = 0664
 
180
        if not self.quiet:
 
181
            self.outf.write('Uploading %s\n' % relpath)
 
182
        self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
 
183
 
 
184
    def upload_file_robustly(self, relpath, id, mode=None):
 
185
        """Upload a file, clearing the way on the remote side.
 
186
 
 
187
        When doing a full upload, it may happen that a directory exists where
 
188
        we want to put our file.
 
189
        """
 
190
        try:
 
191
            st = self._up_stat(relpath)
 
192
            if stat.S_ISDIR(st.st_mode):
 
193
                # A simple rmdir may not be enough
 
194
                if not self.quiet:
 
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:
 
199
            pass
 
200
        self.upload_file(relpath, id, mode)
 
201
 
 
202
    def make_remote_dir(self, relpath, mode=None):
 
203
        if mode is None:
 
204
            mode = 0775
 
205
        self._up_mkdir(relpath, mode)
 
206
 
 
207
    def make_remote_dir_robustly(self, relpath, mode=None):
 
208
        """Create a remote directory, clearing the way on the remote side.
 
209
 
 
210
        When doing a full upload, it may happen that a file exists where we
 
211
        want to create our directory.
 
212
        """
 
213
        try:
 
214
            st = self._up_stat(relpath)
 
215
            if not stat.S_ISDIR(st.st_mode):
 
216
                if not self.quiet:
 
217
                    self.outf.write('Deleting %s/%s\n' % (
 
218
                            self.to_transport.external_url(), relpath))
 
219
                self._up_delete(relpath)
 
220
            else:
 
221
                # Ok the remote dir already exists, nothing to do
 
222
                return
 
223
        except errors.PathError:
 
224
            pass
 
225
        self.make_remote_dir(relpath, mode)
 
226
 
 
227
    def delete_remote_file(self, relpath):
 
228
        if not self.quiet:
 
229
            self.outf.write('Deleting %s\n' % relpath)
 
230
        self._up_delete(relpath)
 
231
 
 
232
    def delete_remote_dir(self, relpath):
 
233
        if not self.quiet:
 
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
 
238
 
 
239
    def delete_remote_dir_maybe(self, relpath):
 
240
        """Try to delete relpath, keeping failures to retry later."""
 
241
        try:
 
242
            self._up_rmdir(relpath)
 
243
        # any kind of PathError would be OK, though we normally expect
 
244
        # DirectoryNotEmpty
 
245
        except errors.PathError:
 
246
            self._pending_deletions.append(relpath)
 
247
 
 
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 = []
 
257
 
 
258
    def rename_remote(self, old_relpath, new_relpath):
 
259
        """Rename a remote file or directory taking care of collisions.
 
260
 
 
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.
 
264
        """
 
265
        # We generate a sufficiently random name to *assume* that
 
266
        # no collisions will occur and don't worry about it (nor
 
267
        # handle it).
 
268
        import os
 
269
        import random
 
270
        import time
 
271
 
 
272
        stamp = '.tmp.%.9f.%d.%d' % (time.time(),
 
273
                                     os.getpid(),
 
274
                                     random.randint(0,0x7FFFFFFF))
 
275
        if not self.quiet:
 
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))
 
279
 
 
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 = []
 
286
 
 
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()
 
291
        try:
 
292
            for relpath, ie in self.tree.iter_entries_by_dir():
 
293
                if relpath in ('', '.bzrignore', '.bzrignore-upload'):
 
294
                    # skip root ('')
 
295
                    # .bzrignore and .bzrignore-upload have no meaning outside
 
296
                    # a working tree so do not upload them
 
297
                    continue
 
298
                if self.is_ignored(relpath):
 
299
                    if not self.quiet:
 
300
                        self.outf.write('Ignoring %s\n' % relpath)
 
301
                    continue
 
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':
 
307
                    if not self.quiet:
 
308
                        target = self.tree.path_content_summary(relpath)[3]
 
309
                        self.outf.write('Not uploading symlink %s -> %s\n'
 
310
                                        % (relpath, target))
 
311
                else:
 
312
                    raise NotImplementedError
 
313
            self.set_uploaded_revid(self.rev_id)
 
314
        finally:
 
315
            self.tree.unlock()
 
316
 
 
317
    def upload_tree(self):
 
318
        # If we can't find the revid file on the remote location, upload the
 
319
        # full tree instead
 
320
        rev_id = self.get_uploaded_revid()
 
321
 
 
322
        if rev_id == revision.NULL_REVISION:
 
323
            if not self.quiet:
 
324
                self.outf.write('No uploaded revision id found,'
 
325
                                ' switching to full upload\n')
 
326
            self.upload_full_tree()
 
327
            # We're done
 
328
            return
 
329
 
 
330
        # Check if the revision hasn't already been uploaded
 
331
        if rev_id == self.rev_id:
 
332
            if not self.quiet:
 
333
                self.outf.write('Remote location already up to date\n')
 
334
 
 
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()
 
340
        try:
 
341
            for (path, id, kind) in changes.removed:
 
342
                if self.is_ignored(path):
 
343
                    if not self.quiet:
 
344
                        self.outf.write('Ignoring %s\n' % path)
 
345
                    continue
 
346
                if kind is 'file':
 
347
                    self.delete_remote_file(path)
 
348
                elif kind is  'directory':
 
349
                    self.delete_remote_dir_maybe(path)
 
350
                elif kind == 'symlink':
 
351
                    if not self.quiet:
 
352
                        target = self.tree.path_content_summary(path)[3]
 
353
                        self.outf.write('Not deleting remote symlink %s -> %s\n'
 
354
                                        % (path, target))
 
355
                else:
 
356
                    raise NotImplementedError
 
357
 
 
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):
 
361
                    if not self.quiet:
 
362
                        self.outf.write('Ignoring %s\n' % old_path)
 
363
                        self.outf.write('Ignoring %s\n' % new_path)
 
364
                    continue
 
365
                if content_change:
 
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':
 
370
                    if not self.quiet:
 
371
                        self.outf.write('Not renaming remote symlink %s to %s\n'
 
372
                                        % (old_path, new_path))
 
373
                else:
 
374
                    self.rename_remote(old_path, new_path)
 
375
            self.finish_renames()
 
376
            self.finish_deletions()
 
377
 
 
378
            for (path, id, old_kind, new_kind) in changes.kind_changed:
 
379
                if self.is_ignored(path):
 
380
                    if not self.quiet:
 
381
                        self.outf.write('Ignoring %s\n' % path)
 
382
                    continue
 
383
                if old_kind == 'file':
 
384
                    self.delete_remote_file(path)
 
385
                elif old_kind ==  'directory':
 
386
                    self.delete_remote_dir(path)
 
387
                else:
 
388
                    raise NotImplementedError
 
389
 
 
390
                if new_kind == 'file':
 
391
                    self.upload_file(path, id)
 
392
                elif new_kind is 'directory':
 
393
                    self.make_remote_dir(path)
 
394
                else:
 
395
                    raise NotImplementedError
 
396
 
 
397
            for (path, id, kind) in changes.added:
 
398
                if self.is_ignored(path):
 
399
                    if not self.quiet:
 
400
                        self.outf.write('Ignoring %s\n' % path)
 
401
                    continue
 
402
                if kind == 'file':
 
403
                    self.upload_file(path, id)
 
404
                elif kind == 'directory':
 
405
                    self.make_remote_dir(path)
 
406
                elif kind == 'symlink':
 
407
                    if not self.quiet:
 
408
                        target = self.tree.path_content_summary(path)[3]
 
409
                        self.outf.write('Not uploading symlink %s -> %s\n'
 
410
                                        % (path, target))
 
411
                else:
 
412
                    raise NotImplementedError
 
413
 
 
414
            # XXX: Add a test for exec_change
 
415
            for (path, id, kind,
 
416
                 content_change, exec_change) in changes.modified:
 
417
                if self.is_ignored(path):
 
418
                    if not self.quiet:
 
419
                        self.outf.write('Ignoring %s\n' % path)
 
420
                    continue
 
421
                if kind is 'file':
 
422
                    self.upload_file(path, id)
 
423
                else:
 
424
                    raise NotImplementedError
 
425
 
 
426
            self.set_uploaded_revid(self.rev_id)
 
427
        finally:
 
428
            self.tree.unlock()
 
429
 
 
430
 
 
431
class CannotUploadToWorkingTree(errors.BzrCommandError):
 
432
 
 
433
    _fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
 
434
 
 
435
 
 
436
class DivergedUploadedTree(errors.BzrCommandError):
 
437
 
 
438
    _fmt = ("Your branch (%(revid)s)"
 
439
            " and the uploaded tree (%(uploaded_revid)s) have diverged: ")
 
440
 
 
441
 
 
442
class cmd_upload(commands.Command):
 
443
    """Upload a working tree, as a whole or incrementally.
 
444
 
 
445
    If no destination is specified use the last one used.
 
446
    If no revision is specified upload the changes since the last upload.
 
447
 
 
448
    Changes include files added, renamed, modified or removed.
 
449
    """
 
450
    _see_also = ['plugins/upload']
 
451
    takes_args = ['location?']
 
452
    takes_options = [
 
453
        'revision',
 
454
        'remember',
 
455
        'overwrite',
 
456
        option.Option('full', 'Upload the full working tree.'),
 
457
        option.Option('quiet', 'Do not output what is being done.',
 
458
                       short_name='q'),
 
459
        option.Option('directory',
 
460
                      help='Branch to upload from, '
 
461
                      'rather than the one containing the working directory.',
 
462
                      short_name='d',
 
463
                      type=unicode,
 
464
                      ),
 
465
        option.Option('auto',
 
466
                      'Trigger an upload from this branch whenever the tip '
 
467
                      'revision changes.')
 
468
       ]
 
469
 
 
470
    def run(self, location=None, full=False, revision=None, remember=None,
 
471
            directory=None, quiet=False, auto=None, overwrite=False
 
472
            ):
 
473
        if directory is None:
 
474
            directory = u'.'
 
475
 
 
476
        (wt, branch,
 
477
         relpath) = controldir.ControlDir.open_containing_tree_or_branch(
 
478
             directory)
 
479
 
 
480
        if wt:
 
481
            wt.lock_read()
 
482
            locked = wt
 
483
        else:
 
484
            branch.lock_read()
 
485
            locked = branch
 
486
        try:
 
487
            if wt:
 
488
                changes = wt.changes_from(wt.basis_tree())
 
489
 
 
490
                if revision is None and  changes.has_changed():
 
491
                    raise errors.UncommittedChanges(wt)
 
492
 
 
493
            conf = branch.get_config_stack()
 
494
            if location is None:
 
495
                stored_loc = conf.get('upload_location')
 
496
                if stored_loc is None:
 
497
                    raise errors.BzrCommandError(
 
498
                        'No upload location known or specified.')
 
499
                else:
 
500
                    # FIXME: Not currently tested
 
501
                    display_url = urlutils.unescape_for_display(stored_loc,
 
502
                            self.outf.encoding)
 
503
                    self.outf.write("Using saved location: %s\n" % display_url)
 
504
                    location = stored_loc
 
505
 
 
506
            to_transport = transport.get_transport(location)
 
507
 
 
508
            # Check that we are not uploading to a existing working tree.
 
509
            try:
 
510
                to_bzr_dir = controldir.ControlDir.open_from_transport(
 
511
                        to_transport)
 
512
                has_wt = to_bzr_dir.has_workingtree()
 
513
            except errors.NotBranchError:
 
514
                has_wt = False
 
515
            except errors.NotLocalUrl:
 
516
                # The exception raised is a bit weird... but that's life.
 
517
                has_wt = True
 
518
 
 
519
            if has_wt:
 
520
                raise CannotUploadToWorkingTree(url=location)
 
521
            if revision is None:
 
522
                rev_id = branch.last_revision()
 
523
            else:
 
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
 
528
 
 
529
            tree = branch.repository.revision_tree(rev_id)
 
530
 
 
531
            uploader = BzrUploader(branch, to_transport, self.outf, tree,
 
532
                                   rev_id, quiet=quiet)
 
533
 
 
534
            if not overwrite:
 
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)
 
540
            if full:
 
541
                uploader.upload_full_tree()
 
542
            else:
 
543
                uploader.upload_tree()
 
544
        finally:
 
545
            locked.unlock()
 
546
 
 
547
        # We uploaded successfully, remember it
 
548
        branch.lock_write()
 
549
        try:
 
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))
 
554
            if auto is not None:
 
555
                conf.set('upload_auto', auto)
 
556
        finally:
 
557
            branch.unlock()
 
558