/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.169.2 by Vincent Ladeuil
Fix deprecation warning about tree.inventory usage
1
# Copyright (C) 2011, 2012 Canonical Ltd
0.165.1 by Jelmer Vernooij
Support lazily loading.
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
6645.2.2 by Jelmer Vernooij
Merge lp:brz.
19
from __future__ import absolute_import
20
6645.2.1 by Jelmer Vernooij
Bundle the upload plugin.
21
from ... import (
0.165.1 by Jelmer Vernooij
Support lazily loading.
22
    branch,
23
    commands,
0.152.93 by Vincent Ladeuil
Migrate to config stacks
24
    config,
0.165.1 by Jelmer Vernooij
Support lazily loading.
25
    lazy_import,
26
    option,
27
    )
28
lazy_import.lazy_import(globals(), """
29
import stat
30
import sys
31
6645.2.1 by Jelmer Vernooij
Bundle the upload plugin.
32
from breezy import (
6667.2.1 by Jelmer Vernooij
Some cleanup; s/BzrDir/ControlDir/, remove some unused imports.
33
    controldir,
0.165.1 by Jelmer Vernooij
Support lazily loading.
34
    errors,
35
    globbing,
36
    ignores,
37
    revision,
38
    transport,
39
    urlutils,
40
    )
41
""")
42
6754.5.1 by Jelmer Vernooij
Fix some python3 compatibility issues that break 'make check-nodocs3' for me.
43
from ...sixish import (
44
    text_type,
45
    )
46
0.152.93 by Vincent Ladeuil
Migrate to config stacks
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'
0.165.1 by Jelmer Vernooij
Support lazily loading.
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.
0.152.93 by Vincent Ladeuil
Migrate to config stacks
137
        revid_path = self.branch.get_config_stack().get('upload_revid_location')
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
0.152.93 by Vincent Ladeuil
Migrate to config stacks
143
            revid_path = self.branch.get_config_stack(
144
                ).get('upload_revid_location')
0.165.1 by Jelmer Vernooij
Support lazily loading.
145
            try:
146
                self._uploaded_revid = self._up_get_bytes(revid_path)
147
            except errors.NoSuchFile:
0.152.93 by Vincent Ladeuil
Migrate to config stacks
148
                # We have not uploaded to here.
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
0.152.89 by Vincent Ladeuil
Cope with bzr.dev changes
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)
0.165.1 by Jelmer Vernooij
Support lazily loading.
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):
6754.5.1 by Jelmer Vernooij
Fix some python3 compatibility issues that break 'make check-nodocs3' for me.
181
                mode = 0o775
0.165.1 by Jelmer Vernooij
Support lazily loading.
182
            else:
6754.5.1 by Jelmer Vernooij
Fix some python3 compatibility issues that break 'make check-nodocs3' for me.
183
                mode = 0o664
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
6754.5.1 by Jelmer Vernooij
Fix some python3 compatibility issues that break 'make check-nodocs3' for me.
208
            mode = 0o775
0.165.1 by Jelmer Vernooij
Support lazily loading.
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 ?)
6754.8.4 by Jelmer Vernooij
Use new context stuff.
294
        with self.tree.lock_read():
0.169.2 by Vincent Ladeuil
Fix deprecation warning about tree.inventory usage
295
            for relpath, ie in self.tree.iter_entries_by_dir():
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
0.166.1 by Jonathan Paugh
Renamed identifier 'path' to 'relpath' in BzrUploader.upload_full_tree(). Only relpath was defined there, and this caused 'bzr upload --full' to die with error, rather than simply ignoring symlinks.
311
                        target = self.tree.path_content_summary(relpath)[3]
0.165.1 by Jelmer Vernooij
Support lazily loading.
312
                        self.outf.write('Not uploading symlink %s -> %s\n'
0.166.1 by Jonathan Paugh
Renamed identifier 'path' to 'relpath' in BzrUploader.upload_full_tree(). Only relpath was defined there, and this caused 'bzr upload --full' to die with error, rather than simply ignoring symlinks.
313
                                        % (relpath, target))
0.165.1 by Jelmer Vernooij
Support lazily loading.
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)
6754.8.4 by Jelmer Vernooij
Use new context stuff.
340
        with self.tree.lock_read():
0.165.1 by Jelmer Vernooij
Support lazily loading.
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',
6754.5.1 by Jelmer Vernooij
Fix some python3 compatibility issues that break 'make check-nodocs3' for me.
461
                      type=text_type,
0.165.1 by Jelmer Vernooij
Support lazily loading.
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,
6667.2.1 by Jelmer Vernooij
Some cleanup; s/BzrDir/ControlDir/, remove some unused imports.
475
         relpath) = controldir.ControlDir.open_containing_tree_or_branch(
476
             directory)
0.165.1 by Jelmer Vernooij
Support lazily loading.
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
0.152.93 by Vincent Ladeuil
Migrate to config stacks
491
            conf = branch.get_config_stack()
0.165.1 by Jelmer Vernooij
Support lazily loading.
492
            if location is None:
0.152.93 by Vincent Ladeuil
Migrate to config stacks
493
                stored_loc = conf.get('upload_location')
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
6667.2.1 by Jelmer Vernooij
Some cleanup; s/BzrDir/ControlDir/, remove some unused imports.
508
                to_bzr_dir = controldir.ControlDir.open_from_transport(
509
                        to_transport)
0.165.1 by Jelmer Vernooij
Support lazily loading.
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
0.152.93 by Vincent Ladeuil
Migrate to config stacks
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