/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: Jelmer Vernooij
  • Date: 2017-09-01 07:15:43 UTC
  • mfrom: (6770.3.2 py3_test_cleanup)
  • Revision ID: jelmer@jelmer.uk-20170901071543-1t83321xkog9qrxh
Merge lp:~gz/brz/py3_test_cleanup

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
from ...sixish import (
 
44
    text_type,
 
45
    )
 
46
 
 
47
auto_option = config.Option(
 
48
    'upload_auto', default=False, from_unicode=config.bool_from_store,
 
49
    help="""\
 
50
Whether upload should occur when the tip of the branch changes.
 
51
""")
 
52
auto_quiet_option = config.Option(
 
53
    'upload_auto_quiet', default=False, from_unicode=config.bool_from_store,
 
54
    help="""\
 
55
Whether upload should occur quietly.
 
56
""")
 
57
location_option = config.Option(
 
58
    'upload_location', default=None,
 
59
    help="""\
 
60
The url to upload the working tree to.
 
61
""")
 
62
revid_location_option = config.Option(
 
63
    'upload_revid_location', default=u'.bzr-upload.revid',
 
64
    help="""\
 
65
The relative path to be used to store the uploaded revid.
 
66
 
 
67
The only bzr-related info uploaded with the working tree is the corresponding
 
68
revision id. The uploaded working tree is not linked to any other bzr data.
 
69
 
 
70
If the layout of your remote server is such that you can't write in the
 
71
root directory but only in the directories inside that root, you will need
 
72
to use the 'upload_revid_location' configuration variable to specify the
 
73
relative path to be used. That configuration variable can be specified in
 
74
locations.conf or branch.conf.
 
75
 
 
76
For example, given the following layout:
 
77
 
 
78
  Project/
 
79
    private/
 
80
    public/
 
81
 
 
82
you may have write access in 'private' and 'public' but in 'Project'
 
83
itself. In that case, you can add the following in your locations.conf or
 
84
branch.conf file:
 
85
 
 
86
  upload_revid_location = private/.bzr-upload.revid
 
87
""")
 
88
 
 
89
 
 
90
# FIXME: Add more tests around invalid paths or relative paths that doesn't
 
91
# exist on remote (if only to get proper error messages) for
 
92
# 'upload_revid_location'
 
93
 
 
94
 
 
95
class BzrUploader(object):
 
96
 
 
97
    def __init__(self, branch, to_transport, outf, tree, rev_id,
 
98
                 quiet=False):
 
99
        self.branch = branch
 
100
        self.to_transport = to_transport
 
101
        self.outf = outf
 
102
        self.tree = tree
 
103
        self.rev_id = rev_id
 
104
        self.quiet = quiet
 
105
        self._pending_deletions = []
 
106
        self._pending_renames = []
 
107
        self._uploaded_revid = None
 
108
        self._ignored = None
 
109
 
 
110
    def _up_stat(self, relpath):
 
111
        return self.to_transport.stat(urlutils.escape(relpath))
 
112
 
 
113
    def _up_rename(self, old_path, new_path):
 
114
        return self.to_transport.rename(urlutils.escape(old_path),
 
115
                                        urlutils.escape(new_path))
 
116
 
 
117
    def _up_delete(self, relpath):
 
118
        return self.to_transport.delete(urlutils.escape(relpath))
 
119
 
 
120
    def _up_delete_tree(self, relpath):
 
121
        return self.to_transport.delete_tree(urlutils.escape(relpath))
 
122
 
 
123
    def _up_mkdir(self, relpath, mode):
 
124
        return self.to_transport.mkdir(urlutils.escape(relpath), mode)
 
125
 
 
126
    def _up_rmdir(self, relpath):
 
127
        return self.to_transport.rmdir(urlutils.escape(relpath))
 
128
 
 
129
    def _up_put_bytes(self, relpath, bytes, mode):
 
130
        self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
 
131
 
 
132
    def _up_get_bytes(self, relpath):
 
133
        return self.to_transport.get_bytes(urlutils.escape(relpath))
 
134
 
 
135
    def set_uploaded_revid(self, rev_id):
 
136
        # XXX: Add tests for concurrent updates, etc.
 
137
        revid_path = self.branch.get_config_stack().get('upload_revid_location')
 
138
        self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
 
139
        self._uploaded_revid = rev_id
 
140
 
 
141
    def get_uploaded_revid(self):
 
142
        if self._uploaded_revid is None:
 
143
            revid_path = self.branch.get_config_stack(
 
144
                ).get('upload_revid_location')
 
145
            try:
 
146
                self._uploaded_revid = self._up_get_bytes(revid_path)
 
147
            except errors.NoSuchFile:
 
148
                # We have not uploaded to here.
 
149
                self._uploaded_revid = revision.NULL_REVISION
 
150
        return self._uploaded_revid
 
151
 
 
152
    def _get_ignored(self):
 
153
        if self._ignored is None:
 
154
            try:
 
155
                ignore_file_path = '.bzrignore-upload'
 
156
                ignore_file_id = self.tree.path2id(ignore_file_path)
 
157
                ignore_file = self.tree.get_file(ignore_file_id,
 
158
                                                 ignore_file_path)
 
159
                ignored_patterns = ignores.parse_ignore_file(ignore_file)
 
160
            except errors.NoSuchId:
 
161
                ignored_patterns = []
 
162
            self._ignored = globbing.Globster(ignored_patterns)
 
163
        return self._ignored
 
164
 
 
165
    def is_ignored(self, relpath):
 
166
        glob = self._get_ignored()
 
167
        ignored = glob.match(relpath)
 
168
        import os
 
169
        if not ignored:
 
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)
 
174
                if not ignored:
 
175
                    dir = os.path.dirname(dir)
 
176
        return ignored
 
177
 
 
178
    def upload_file(self, relpath, id, mode=None):
 
179
        if mode is None:
 
180
            if self.tree.is_executable(id):
 
181
                mode = 0o775
 
182
            else:
 
183
                mode = 0o664
 
184
        if not self.quiet:
 
185
            self.outf.write('Uploading %s\n' % relpath)
 
186
        self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
 
187
 
 
188
    def upload_file_robustly(self, relpath, id, mode=None):
 
189
        """Upload a file, clearing the way on the remote side.
 
190
 
 
191
        When doing a full upload, it may happen that a directory exists where
 
192
        we want to put our file.
 
193
        """
 
194
        try:
 
195
            st = self._up_stat(relpath)
 
196
            if stat.S_ISDIR(st.st_mode):
 
197
                # A simple rmdir may not be enough
 
198
                if not self.quiet:
 
199
                    self.outf.write('Clearing %s/%s\n' % (
 
200
                            self.to_transport.external_url(), relpath))
 
201
                self._up_delete_tree(relpath)
 
202
        except errors.PathError:
 
203
            pass
 
204
        self.upload_file(relpath, id, mode)
 
205
 
 
206
    def make_remote_dir(self, relpath, mode=None):
 
207
        if mode is None:
 
208
            mode = 0o775
 
209
        self._up_mkdir(relpath, mode)
 
210
 
 
211
    def make_remote_dir_robustly(self, relpath, mode=None):
 
212
        """Create a remote directory, clearing the way on the remote side.
 
213
 
 
214
        When doing a full upload, it may happen that a file exists where we
 
215
        want to create our directory.
 
216
        """
 
217
        try:
 
218
            st = self._up_stat(relpath)
 
219
            if not stat.S_ISDIR(st.st_mode):
 
220
                if not self.quiet:
 
221
                    self.outf.write('Deleting %s/%s\n' % (
 
222
                            self.to_transport.external_url(), relpath))
 
223
                self._up_delete(relpath)
 
224
            else:
 
225
                # Ok the remote dir already exists, nothing to do
 
226
                return
 
227
        except errors.PathError:
 
228
            pass
 
229
        self.make_remote_dir(relpath, mode)
 
230
 
 
231
    def delete_remote_file(self, relpath):
 
232
        if not self.quiet:
 
233
            self.outf.write('Deleting %s\n' % relpath)
 
234
        self._up_delete(relpath)
 
235
 
 
236
    def delete_remote_dir(self, relpath):
 
237
        if not self.quiet:
 
238
            self.outf.write('Deleting %s\n' % relpath)
 
239
        self._up_rmdir(relpath)
 
240
        # XXX: Add a test where a subdir is ignored but we still want to
 
241
        # delete the dir -- vila 100106
 
242
 
 
243
    def delete_remote_dir_maybe(self, relpath):
 
244
        """Try to delete relpath, keeping failures to retry later."""
 
245
        try:
 
246
            self._up_rmdir(relpath)
 
247
        # any kind of PathError would be OK, though we normally expect
 
248
        # DirectoryNotEmpty
 
249
        except errors.PathError:
 
250
            self._pending_deletions.append(relpath)
 
251
 
 
252
    def finish_deletions(self):
 
253
        if self._pending_deletions:
 
254
            # Process the previously failed deletions in reverse order to
 
255
            # delete children before parents
 
256
            for relpath in reversed(self._pending_deletions):
 
257
                self._up_rmdir(relpath)
 
258
            # The following shouldn't be needed since we use it once per
 
259
            # upload, but better safe than sorry ;-)
 
260
            self._pending_deletions = []
 
261
 
 
262
    def rename_remote(self, old_relpath, new_relpath):
 
263
        """Rename a remote file or directory taking care of collisions.
 
264
 
 
265
        To avoid collisions during bulk renames, each renamed target is
 
266
        temporarily assigned a unique name. When all renames have been done,
 
267
        each target get its proper name.
 
268
        """
 
269
        # We generate a sufficiently random name to *assume* that
 
270
        # no collisions will occur and don't worry about it (nor
 
271
        # handle it).
 
272
        import os
 
273
        import random
 
274
        import time
 
275
 
 
276
        stamp = '.tmp.%.9f.%d.%d' % (time.time(),
 
277
                                     os.getpid(),
 
278
                                     random.randint(0,0x7FFFFFFF))
 
279
        if not self.quiet:
 
280
            self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
 
281
        self._up_rename(old_relpath, stamp)
 
282
        self._pending_renames.append((stamp, new_relpath))
 
283
 
 
284
    def finish_renames(self):
 
285
        for (stamp, new_path) in self._pending_renames:
 
286
            self._up_rename(stamp, new_path)
 
287
        # The following shouldn't be needed since we use it once per upload,
 
288
        # but better safe than sorry ;-)
 
289
        self._pending_renames = []
 
290
 
 
291
    def upload_full_tree(self):
 
292
        self.to_transport.ensure_base() # XXX: Handle errors (add
 
293
                                        # --create-prefix option ?)
 
294
        with self.tree.lock_read():
 
295
            for relpath, ie in self.tree.iter_entries_by_dir():
 
296
                if relpath in ('', '.bzrignore', '.bzrignore-upload'):
 
297
                    # skip root ('')
 
298
                    # .bzrignore and .bzrignore-upload have no meaning outside
 
299
                    # a working tree so do not upload them
 
300
                    continue
 
301
                if self.is_ignored(relpath):
 
302
                    if not self.quiet:
 
303
                        self.outf.write('Ignoring %s\n' % relpath)
 
304
                    continue
 
305
                if ie.kind == 'file':
 
306
                    self.upload_file_robustly(relpath, ie.file_id)
 
307
                elif ie.kind == 'directory':
 
308
                    self.make_remote_dir_robustly(relpath)
 
309
                elif ie.kind == 'symlink':
 
310
                    if not self.quiet:
 
311
                        target = self.tree.path_content_summary(relpath)[3]
 
312
                        self.outf.write('Not uploading symlink %s -> %s\n'
 
313
                                        % (relpath, target))
 
314
                else:
 
315
                    raise NotImplementedError
 
316
            self.set_uploaded_revid(self.rev_id)
 
317
 
 
318
    def upload_tree(self):
 
319
        # If we can't find the revid file on the remote location, upload the
 
320
        # full tree instead
 
321
        rev_id = self.get_uploaded_revid()
 
322
 
 
323
        if rev_id == revision.NULL_REVISION:
 
324
            if not self.quiet:
 
325
                self.outf.write('No uploaded revision id found,'
 
326
                                ' switching to full upload\n')
 
327
            self.upload_full_tree()
 
328
            # We're done
 
329
            return
 
330
 
 
331
        # Check if the revision hasn't already been uploaded
 
332
        if rev_id == self.rev_id:
 
333
            if not self.quiet:
 
334
                self.outf.write('Remote location already up to date\n')
 
335
 
 
336
        from_tree = self.branch.repository.revision_tree(rev_id)
 
337
        self.to_transport.ensure_base() # XXX: Handle errors (add
 
338
                                        # --create-prefix option ?)
 
339
        changes = self.tree.changes_from(from_tree)
 
340
        with self.tree.lock_read():
 
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
 
 
428
 
 
429
class CannotUploadToWorkingTree(errors.BzrCommandError):
 
430
 
 
431
    _fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
 
432
 
 
433
 
 
434
class DivergedUploadedTree(errors.BzrCommandError):
 
435
 
 
436
    _fmt = ("Your branch (%(revid)s)"
 
437
            " and the uploaded tree (%(uploaded_revid)s) have diverged: ")
 
438
 
 
439
 
 
440
class cmd_upload(commands.Command):
 
441
    """Upload a working tree, as a whole or incrementally.
 
442
 
 
443
    If no destination is specified use the last one used.
 
444
    If no revision is specified upload the changes since the last upload.
 
445
 
 
446
    Changes include files added, renamed, modified or removed.
 
447
    """
 
448
    _see_also = ['plugins/upload']
 
449
    takes_args = ['location?']
 
450
    takes_options = [
 
451
        'revision',
 
452
        'remember',
 
453
        'overwrite',
 
454
        option.Option('full', 'Upload the full working tree.'),
 
455
        option.Option('quiet', 'Do not output what is being done.',
 
456
                       short_name='q'),
 
457
        option.Option('directory',
 
458
                      help='Branch to upload from, '
 
459
                      'rather than the one containing the working directory.',
 
460
                      short_name='d',
 
461
                      type=text_type,
 
462
                      ),
 
463
        option.Option('auto',
 
464
                      'Trigger an upload from this branch whenever the tip '
 
465
                      'revision changes.')
 
466
       ]
 
467
 
 
468
    def run(self, location=None, full=False, revision=None, remember=None,
 
469
            directory=None, quiet=False, auto=None, overwrite=False
 
470
            ):
 
471
        if directory is None:
 
472
            directory = u'.'
 
473
 
 
474
        (wt, branch,
 
475
         relpath) = controldir.ControlDir.open_containing_tree_or_branch(
 
476
             directory)
 
477
 
 
478
        if wt:
 
479
            wt.lock_read()
 
480
            locked = wt
 
481
        else:
 
482
            branch.lock_read()
 
483
            locked = branch
 
484
        try:
 
485
            if wt:
 
486
                changes = wt.changes_from(wt.basis_tree())
 
487
 
 
488
                if revision is None and  changes.has_changed():
 
489
                    raise errors.UncommittedChanges(wt)
 
490
 
 
491
            conf = branch.get_config_stack()
 
492
            if location is None:
 
493
                stored_loc = conf.get('upload_location')
 
494
                if stored_loc is None:
 
495
                    raise errors.BzrCommandError(
 
496
                        'No upload location known or specified.')
 
497
                else:
 
498
                    # FIXME: Not currently tested
 
499
                    display_url = urlutils.unescape_for_display(stored_loc,
 
500
                            self.outf.encoding)
 
501
                    self.outf.write("Using saved location: %s\n" % display_url)
 
502
                    location = stored_loc
 
503
 
 
504
            to_transport = transport.get_transport(location)
 
505
 
 
506
            # Check that we are not uploading to a existing working tree.
 
507
            try:
 
508
                to_bzr_dir = controldir.ControlDir.open_from_transport(
 
509
                        to_transport)
 
510
                has_wt = to_bzr_dir.has_workingtree()
 
511
            except errors.NotBranchError:
 
512
                has_wt = False
 
513
            except errors.NotLocalUrl:
 
514
                # The exception raised is a bit weird... but that's life.
 
515
                has_wt = True
 
516
 
 
517
            if has_wt:
 
518
                raise CannotUploadToWorkingTree(url=location)
 
519
            if revision is None:
 
520
                rev_id = branch.last_revision()
 
521
            else:
 
522
                if len(revision) != 1:
 
523
                    raise errors.BzrCommandError(
 
524
                        'bzr upload --revision takes exactly 1 argument')
 
525
                rev_id = revision[0].in_history(branch).rev_id
 
526
 
 
527
            tree = branch.repository.revision_tree(rev_id)
 
528
 
 
529
            uploader = BzrUploader(branch, to_transport, self.outf, tree,
 
530
                                   rev_id, quiet=quiet)
 
531
 
 
532
            if not overwrite:
 
533
                prev_uploaded_rev_id = uploader.get_uploaded_revid()
 
534
                graph = branch.repository.get_graph()
 
535
                if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
 
536
                    raise DivergedUploadedTree(
 
537
                        revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
 
538
            if full:
 
539
                uploader.upload_full_tree()
 
540
            else:
 
541
                uploader.upload_tree()
 
542
        finally:
 
543
            locked.unlock()
 
544
 
 
545
        # We uploaded successfully, remember it
 
546
        branch.lock_write()
 
547
        try:
 
548
            upload_location = conf.get('upload_location')
 
549
            if upload_location is None or remember:
 
550
                conf.set('upload_location',
 
551
                         urlutils.unescape(to_transport.base))
 
552
            if auto is not None:
 
553
                conf.set('upload_auto', auto)
 
554
        finally:
 
555
            branch.unlock()
 
556