/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
0.152.93 by Vincent Ladeuil
Migrate to config stacks
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'
0.165.1 by Jelmer Vernooij
Support lazily loading.
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.
0.152.93 by Vincent Ladeuil
Migrate to config stacks
133
        revid_path = self.branch.get_config_stack().get('upload_revid_location')
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
0.152.93 by Vincent Ladeuil
Migrate to config stacks
139
            revid_path = self.branch.get_config_stack(
140
                ).get('upload_revid_location')
0.165.1 by Jelmer Vernooij
Support lazily loading.
141
            try:
142
                self._uploaded_revid = self._up_get_bytes(revid_path)
143
            except errors.NoSuchFile:
0.152.93 by Vincent Ladeuil
Migrate to config stacks
144
                # We have not uploaded to here.
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
0.152.89 by Vincent Ladeuil
Cope with bzr.dev changes
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)
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
0.169.2 by Vincent Ladeuil
Fix deprecation warning about tree.inventory usage
292
            for relpath, ie in self.tree.iter_entries_by_dir():
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
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.
308
                        target = self.tree.path_content_summary(relpath)[3]
0.165.1 by Jelmer Vernooij
Support lazily loading.
309
                        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.
310
                                        % (relpath, target))
0.165.1 by Jelmer Vernooij
Support lazily loading.
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,
6667.2.1 by Jelmer Vernooij
Some cleanup; s/BzrDir/ControlDir/, remove some unused imports.
477
         relpath) = controldir.ControlDir.open_containing_tree_or_branch(
478
             directory)
0.165.1 by Jelmer Vernooij
Support lazily loading.
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
0.152.93 by Vincent Ladeuil
Migrate to config stacks
493
            conf = branch.get_config_stack()
0.165.1 by Jelmer Vernooij
Support lazily loading.
494
            if location is None:
0.152.93 by Vincent Ladeuil
Migrate to config stacks
495
                stored_loc = conf.get('upload_location')
0.165.1 by Jelmer Vernooij
Support lazily loading.
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:
6667.2.1 by Jelmer Vernooij
Some cleanup; s/BzrDir/ControlDir/, remove some unused imports.
510
                to_bzr_dir = controldir.ControlDir.open_from_transport(
511
                        to_transport)
0.165.1 by Jelmer Vernooij
Support lazily loading.
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
0.152.93 by Vincent Ladeuil
Migrate to config stacks
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