/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-06-04 19:17:13 UTC
  • mfrom: (0.193.10 trunk)
  • mto: This revision was merged to the branch mainline in revision 6778.
  • Revision ID: jelmer@jelmer.uk-20170604191713-hau7dfsqsl035slm
Bundle the cvs plugin.

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
    bzrdir,
 
34
    errors,
 
35
    globbing,
 
36
    ignores,
 
37
    osutils,
 
38
    revision,
 
39
    revisionspec,
 
40
    trace,
 
41
    transport,
 
42
    urlutils,
 
43
    workingtree,
 
44
    )
 
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 = 0775
 
182
            else:
 
183
                mode = 0664
 
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 = 0775
 
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
        self.tree.lock_read()
 
295
        try:
 
296
            for relpath, ie in self.tree.iter_entries_by_dir():
 
297
                if relpath in ('', '.bzrignore', '.bzrignore-upload'):
 
298
                    # skip root ('')
 
299
                    # .bzrignore and .bzrignore-upload have no meaning outside
 
300
                    # a working tree so do not upload them
 
301
                    continue
 
302
                if self.is_ignored(relpath):
 
303
                    if not self.quiet:
 
304
                        self.outf.write('Ignoring %s\n' % relpath)
 
305
                    continue
 
306
                if ie.kind == 'file':
 
307
                    self.upload_file_robustly(relpath, ie.file_id)
 
308
                elif ie.kind == 'directory':
 
309
                    self.make_remote_dir_robustly(relpath)
 
310
                elif ie.kind == 'symlink':
 
311
                    if not self.quiet:
 
312
                        target = self.tree.path_content_summary(relpath)[3]
 
313
                        self.outf.write('Not uploading symlink %s -> %s\n'
 
314
                                        % (relpath, target))
 
315
                else:
 
316
                    raise NotImplementedError
 
317
            self.set_uploaded_revid(self.rev_id)
 
318
        finally:
 
319
            self.tree.unlock()
 
320
 
 
321
    def upload_tree(self):
 
322
        # If we can't find the revid file on the remote location, upload the
 
323
        # full tree instead
 
324
        rev_id = self.get_uploaded_revid()
 
325
 
 
326
        if rev_id == revision.NULL_REVISION:
 
327
            if not self.quiet:
 
328
                self.outf.write('No uploaded revision id found,'
 
329
                                ' switching to full upload\n')
 
330
            self.upload_full_tree()
 
331
            # We're done
 
332
            return
 
333
 
 
334
        # Check if the revision hasn't already been uploaded
 
335
        if rev_id == self.rev_id:
 
336
            if not self.quiet:
 
337
                self.outf.write('Remote location already up to date\n')
 
338
 
 
339
        from_tree = self.branch.repository.revision_tree(rev_id)
 
340
        self.to_transport.ensure_base() # XXX: Handle errors (add
 
341
                                        # --create-prefix option ?)
 
342
        changes = self.tree.changes_from(from_tree)
 
343
        self.tree.lock_read()
 
344
        try:
 
345
            for (path, id, kind) in changes.removed:
 
346
                if self.is_ignored(path):
 
347
                    if not self.quiet:
 
348
                        self.outf.write('Ignoring %s\n' % path)
 
349
                    continue
 
350
                if kind is 'file':
 
351
                    self.delete_remote_file(path)
 
352
                elif kind is  'directory':
 
353
                    self.delete_remote_dir_maybe(path)
 
354
                elif kind == 'symlink':
 
355
                    if not self.quiet:
 
356
                        target = self.tree.path_content_summary(path)[3]
 
357
                        self.outf.write('Not deleting remote symlink %s -> %s\n'
 
358
                                        % (path, target))
 
359
                else:
 
360
                    raise NotImplementedError
 
361
 
 
362
            for (old_path, new_path, id, kind,
 
363
                 content_change, exec_change) in changes.renamed:
 
364
                if self.is_ignored(old_path) and self.is_ignored(new_path):
 
365
                    if not self.quiet:
 
366
                        self.outf.write('Ignoring %s\n' % old_path)
 
367
                        self.outf.write('Ignoring %s\n' % new_path)
 
368
                    continue
 
369
                if content_change:
 
370
                    # We update the old_path content because renames and
 
371
                    # deletions are differed.
 
372
                    self.upload_file(old_path, id)
 
373
                if kind == 'symlink':
 
374
                    if not self.quiet:
 
375
                        self.outf.write('Not renaming remote symlink %s to %s\n'
 
376
                                        % (old_path, new_path))
 
377
                else:
 
378
                    self.rename_remote(old_path, new_path)
 
379
            self.finish_renames()
 
380
            self.finish_deletions()
 
381
 
 
382
            for (path, id, old_kind, new_kind) in changes.kind_changed:
 
383
                if self.is_ignored(path):
 
384
                    if not self.quiet:
 
385
                        self.outf.write('Ignoring %s\n' % path)
 
386
                    continue
 
387
                if old_kind == 'file':
 
388
                    self.delete_remote_file(path)
 
389
                elif old_kind ==  'directory':
 
390
                    self.delete_remote_dir(path)
 
391
                else:
 
392
                    raise NotImplementedError
 
393
 
 
394
                if new_kind == 'file':
 
395
                    self.upload_file(path, id)
 
396
                elif new_kind is 'directory':
 
397
                    self.make_remote_dir(path)
 
398
                else:
 
399
                    raise NotImplementedError
 
400
 
 
401
            for (path, id, kind) in changes.added:
 
402
                if self.is_ignored(path):
 
403
                    if not self.quiet:
 
404
                        self.outf.write('Ignoring %s\n' % path)
 
405
                    continue
 
406
                if kind == 'file':
 
407
                    self.upload_file(path, id)
 
408
                elif kind == 'directory':
 
409
                    self.make_remote_dir(path)
 
410
                elif kind == 'symlink':
 
411
                    if not self.quiet:
 
412
                        target = self.tree.path_content_summary(path)[3]
 
413
                        self.outf.write('Not uploading symlink %s -> %s\n'
 
414
                                        % (path, target))
 
415
                else:
 
416
                    raise NotImplementedError
 
417
 
 
418
            # XXX: Add a test for exec_change
 
419
            for (path, id, kind,
 
420
                 content_change, exec_change) in changes.modified:
 
421
                if self.is_ignored(path):
 
422
                    if not self.quiet:
 
423
                        self.outf.write('Ignoring %s\n' % path)
 
424
                    continue
 
425
                if kind is 'file':
 
426
                    self.upload_file(path, id)
 
427
                else:
 
428
                    raise NotImplementedError
 
429
 
 
430
            self.set_uploaded_revid(self.rev_id)
 
431
        finally:
 
432
            self.tree.unlock()
 
433
 
 
434
 
 
435
class CannotUploadToWorkingTree(errors.BzrCommandError):
 
436
 
 
437
    _fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
 
438
 
 
439
 
 
440
class DivergedUploadedTree(errors.BzrCommandError):
 
441
 
 
442
    _fmt = ("Your branch (%(revid)s)"
 
443
            " and the uploaded tree (%(uploaded_revid)s) have diverged: ")
 
444
 
 
445
 
 
446
class cmd_upload(commands.Command):
 
447
    """Upload a working tree, as a whole or incrementally.
 
448
 
 
449
    If no destination is specified use the last one used.
 
450
    If no revision is specified upload the changes since the last upload.
 
451
 
 
452
    Changes include files added, renamed, modified or removed.
 
453
    """
 
454
    _see_also = ['plugins/upload']
 
455
    takes_args = ['location?']
 
456
    takes_options = [
 
457
        'revision',
 
458
        'remember',
 
459
        'overwrite',
 
460
        option.Option('full', 'Upload the full working tree.'),
 
461
        option.Option('quiet', 'Do not output what is being done.',
 
462
                       short_name='q'),
 
463
        option.Option('directory',
 
464
                      help='Branch to upload from, '
 
465
                      'rather than the one containing the working directory.',
 
466
                      short_name='d',
 
467
                      type=unicode,
 
468
                      ),
 
469
        option.Option('auto',
 
470
                      'Trigger an upload from this branch whenever the tip '
 
471
                      'revision changes.')
 
472
       ]
 
473
 
 
474
    def run(self, location=None, full=False, revision=None, remember=None,
 
475
            directory=None, quiet=False, auto=None, overwrite=False
 
476
            ):
 
477
        if directory is None:
 
478
            directory = u'.'
 
479
 
 
480
        (wt, branch,
 
481
         relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
 
482
 
 
483
        if wt:
 
484
            wt.lock_read()
 
485
            locked = wt
 
486
        else:
 
487
            branch.lock_read()
 
488
            locked = branch
 
489
        try:
 
490
            if wt:
 
491
                changes = wt.changes_from(wt.basis_tree())
 
492
 
 
493
                if revision is None and  changes.has_changed():
 
494
                    raise errors.UncommittedChanges(wt)
 
495
 
 
496
            conf = branch.get_config_stack()
 
497
            if location is None:
 
498
                stored_loc = conf.get('upload_location')
 
499
                if stored_loc is None:
 
500
                    raise errors.BzrCommandError(
 
501
                        'No upload location known or specified.')
 
502
                else:
 
503
                    # FIXME: Not currently tested
 
504
                    display_url = urlutils.unescape_for_display(stored_loc,
 
505
                            self.outf.encoding)
 
506
                    self.outf.write("Using saved location: %s\n" % display_url)
 
507
                    location = stored_loc
 
508
 
 
509
            to_transport = transport.get_transport(location)
 
510
 
 
511
            # Check that we are not uploading to a existing working tree.
 
512
            try:
 
513
                to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport)
 
514
                has_wt = to_bzr_dir.has_workingtree()
 
515
            except errors.NotBranchError:
 
516
                has_wt = False
 
517
            except errors.NotLocalUrl:
 
518
                # The exception raised is a bit weird... but that's life.
 
519
                has_wt = True
 
520
 
 
521
            if has_wt:
 
522
                raise CannotUploadToWorkingTree(url=location)
 
523
            if revision is None:
 
524
                rev_id = branch.last_revision()
 
525
            else:
 
526
                if len(revision) != 1:
 
527
                    raise errors.BzrCommandError(
 
528
                        'bzr upload --revision takes exactly 1 argument')
 
529
                rev_id = revision[0].in_history(branch).rev_id
 
530
 
 
531
            tree = branch.repository.revision_tree(rev_id)
 
532
 
 
533
            uploader = BzrUploader(branch, to_transport, self.outf, tree,
 
534
                                   rev_id, quiet=quiet)
 
535
 
 
536
            if not overwrite:
 
537
                prev_uploaded_rev_id = uploader.get_uploaded_revid()
 
538
                graph = branch.repository.get_graph()
 
539
                if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
 
540
                    raise DivergedUploadedTree(
 
541
                        revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
 
542
            if full:
 
543
                uploader.upload_full_tree()
 
544
            else:
 
545
                uploader.upload_tree()
 
546
        finally:
 
547
            locked.unlock()
 
548
 
 
549
        # We uploaded successfully, remember it
 
550
        branch.lock_write()
 
551
        try:
 
552
            upload_location = conf.get('upload_location')
 
553
            if upload_location is None or remember:
 
554
                conf.set('upload_location',
 
555
                         urlutils.unescape(to_transport.base))
 
556
            if auto is not None:
 
557
                conf.set('upload_auto', auto)
 
558
        finally:
 
559
            branch.unlock()
 
560