/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.165.1 by Jelmer Vernooij
Support lazily loading.
1
# Copyright (C) 2008, 2009, 2010 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 bzrlib import (
20
    branch,
21
    commands,
22
    lazy_import,
23
    option,
24
    )
25
lazy_import.lazy_import(globals(), """
26
import stat
27
import sys
28
29
from bzrlib import (
30
    bzrdir,
31
    errors,
32
    globbing,
33
    ignores,
34
    osutils,
35
    revision,
36
    revisionspec,
37
    trace,
38
    transport,
39
    urlutils,
40
    workingtree,
41
    )
42
""")
43
44
def _get_branch_option(branch, option):
45
    return branch.get_config().get_user_option(option)
46
47
# FIXME: Get rid of that as soon as we depend on a bzr API that includes
48
# get_user_option_as_bool
49
def _get_branch_bool_option(branch, option):
50
    conf = branch.get_config()
51
    if hasattr(conf, 'get_user_option_as_bool'):
52
        value = conf.get_user_option_as_bool(option)
53
    else:
54
        value = conf.get_user_option(option)
55
        if value is not None:
56
            if value.lower().strip() == 'true':
57
                value = True
58
            else:
59
                value = False
60
    return value
61
62
def _set_branch_option(branch, option, value):
63
    branch.get_config().set_user_option(option, value)
64
65
66
def get_upload_location(branch):
67
    return _get_branch_option(branch, 'upload_location')
68
69
70
def set_upload_location(branch, location):
71
    _set_branch_option(branch, 'upload_location', location)
72
73
74
# FIXME: Add more tests around invalid paths used here or relative paths that
75
# doesn't exist on remote (if only to get proper error messages)
76
def get_upload_revid_location(branch):
77
    loc =  _get_branch_option(branch, 'upload_revid_location')
78
    if loc is None:
79
        loc = '.bzr-upload.revid'
80
    return loc
81
82
83
def set_upload_revid_location(branch, location):
84
    _set_branch_option(branch, 'upload_revid_location', location)
85
86
87
def get_upload_auto(branch):
88
    auto = _get_branch_bool_option(branch, 'upload_auto')
89
    if auto is None:
90
        auto = False # Default to False if not specified
91
    return auto
92
93
94
def set_upload_auto(branch, auto):
95
    # FIXME: What's the point in allowing a boolean here instead of requiring
96
    # the callers to use strings instead ?
97
    if auto:
98
        auto_str = "True"
99
    else:
100
        auto_str = "False"
101
    _set_branch_option(branch, 'upload_auto', auto_str)
102
103
104
def get_upload_auto_quiet(branch):
105
    quiet = _get_branch_bool_option(branch, 'upload_auto_quiet')
106
    if quiet is None:
107
        quiet = False # Default to False if not specified
108
    return quiet
109
110
111
def set_upload_auto_quiet(branch, quiet):
112
    _set_branch_option(branch, 'upload_auto_quiet', quiet)
113
114
115
class BzrUploader(object):
116
117
    def __init__(self, branch, to_transport, outf, tree, rev_id,
118
                 quiet=False):
119
        self.branch = branch
120
        self.to_transport = to_transport
121
        self.outf = outf
122
        self.tree = tree
123
        self.rev_id = rev_id
124
        self.quiet = quiet
125
        self._pending_deletions = []
126
        self._pending_renames = []
127
        self._uploaded_revid = None
128
        self._ignored = None
129
130
    def _up_stat(self, relpath):
131
        return self.to_transport.stat(urlutils.escape(relpath))
132
133
    def _up_rename(self, old_path, new_path):
134
        return self.to_transport.rename(urlutils.escape(old_path),
135
                                        urlutils.escape(new_path))
136
137
    def _up_delete(self, relpath):
138
        return self.to_transport.delete(urlutils.escape(relpath))
139
140
    def _up_delete_tree(self, relpath):
141
        return self.to_transport.delete_tree(urlutils.escape(relpath))
142
143
    def _up_mkdir(self, relpath, mode):
144
        return self.to_transport.mkdir(urlutils.escape(relpath), mode)
145
146
    def _up_rmdir(self, relpath):
147
        return self.to_transport.rmdir(urlutils.escape(relpath))
148
149
    def _up_put_bytes(self, relpath, bytes, mode):
150
        self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
151
152
    def _up_get_bytes(self, relpath):
153
        return self.to_transport.get_bytes(urlutils.escape(relpath))
154
155
    def set_uploaded_revid(self, rev_id):
156
        # XXX: Add tests for concurrent updates, etc.
157
        revid_path = get_upload_revid_location(self.branch)
158
        self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
159
        self._uploaded_revid = rev_id
160
161
    def get_uploaded_revid(self):
162
        if self._uploaded_revid is None:
163
            revid_path = get_upload_revid_location(self.branch)
164
            try:
165
                self._uploaded_revid = self._up_get_bytes(revid_path)
166
            except errors.NoSuchFile:
167
                # We have not upload to here.
168
                self._uploaded_revid = revision.NULL_REVISION
169
        return self._uploaded_revid
170
171
    def _get_ignored(self):
172
        if self._ignored is None:
173
            try:
174
                ignore_file = self.tree.get_file_by_path('.bzrignore-upload')
175
                ignored_patterns = ignores.parse_ignore_file(ignore_file)
176
            except errors.NoSuchId:
177
                ignored_patterns = []
178
            self._ignored = globbing.Globster(ignored_patterns)
179
        return self._ignored
180
181
    def is_ignored(self, relpath):
182
        glob = self._get_ignored()
183
        ignored = glob.match(relpath)
184
        import os
185
        if not ignored:
186
            # We still need to check that all parents are not ignored
187
            dir = os.path.dirname(relpath)
188
            while dir and not ignored:
189
                ignored = glob.match(dir)
190
                if not ignored:
191
                    dir = os.path.dirname(dir)
192
        return ignored
193
194
    def upload_file(self, relpath, id, mode=None):
195
        if mode is None:
196
            if self.tree.is_executable(id):
197
                mode = 0775
198
            else:
199
                mode = 0664
200
        if not self.quiet:
201
            self.outf.write('Uploading %s\n' % relpath)
202
        self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
203
204
    def upload_file_robustly(self, relpath, id, mode=None):
205
        """Upload a file, clearing the way on the remote side.
206
207
        When doing a full upload, it may happen that a directory exists where
208
        we want to put our file.
209
        """
210
        try:
211
            st = self._up_stat(relpath)
212
            if stat.S_ISDIR(st.st_mode):
213
                # A simple rmdir may not be enough
214
                if not self.quiet:
215
                    self.outf.write('Clearing %s/%s\n' % (
216
                            self.to_transport.external_url(), relpath))
217
                self._up_delete_tree(relpath)
218
        except errors.PathError:
219
            pass
220
        self.upload_file(relpath, id, mode)
221
222
    def make_remote_dir(self, relpath, mode=None):
223
        if mode is None:
224
            mode = 0775
225
        self._up_mkdir(relpath, mode)
226
227
    def make_remote_dir_robustly(self, relpath, mode=None):
228
        """Create a remote directory, clearing the way on the remote side.
229
230
        When doing a full upload, it may happen that a file exists where we
231
        want to create our directory.
232
        """
233
        try:
234
            st = self._up_stat(relpath)
235
            if not stat.S_ISDIR(st.st_mode):
236
                if not self.quiet:
237
                    self.outf.write('Deleting %s/%s\n' % (
238
                            self.to_transport.external_url(), relpath))
239
                self._up_delete(relpath)
240
            else:
241
                # Ok the remote dir already exists, nothing to do
242
                return
243
        except errors.PathError:
244
            pass
245
        self.make_remote_dir(relpath, mode)
246
247
    def delete_remote_file(self, relpath):
248
        if not self.quiet:
249
            self.outf.write('Deleting %s\n' % relpath)
250
        self._up_delete(relpath)
251
252
    def delete_remote_dir(self, relpath):
253
        if not self.quiet:
254
            self.outf.write('Deleting %s\n' % relpath)
255
        self._up_rmdir(relpath)
256
        # XXX: Add a test where a subdir is ignored but we still want to
257
        # delete the dir -- vila 100106
258
259
    def delete_remote_dir_maybe(self, relpath):
260
        """Try to delete relpath, keeping failures to retry later."""
261
        try:
262
            self._up_rmdir(relpath)
263
        # any kind of PathError would be OK, though we normally expect
264
        # DirectoryNotEmpty
265
        except errors.PathError:
266
            self._pending_deletions.append(relpath)
267
268
    def finish_deletions(self):
269
        if self._pending_deletions:
270
            # Process the previously failed deletions in reverse order to
271
            # delete children before parents
272
            for relpath in reversed(self._pending_deletions):
273
                self._up_rmdir(relpath)
274
            # The following shouldn't be needed since we use it once per
275
            # upload, but better safe than sorry ;-)
276
            self._pending_deletions = []
277
278
    def rename_remote(self, old_relpath, new_relpath):
279
        """Rename a remote file or directory taking care of collisions.
280
281
        To avoid collisions during bulk renames, each renamed target is
282
        temporarily assigned a unique name. When all renames have been done,
283
        each target get its proper name.
284
        """
285
        # We generate a sufficiently random name to *assume* that
286
        # no collisions will occur and don't worry about it (nor
287
        # handle it).
288
        import os
289
        import random
290
        import time
291
292
        stamp = '.tmp.%.9f.%d.%d' % (time.time(),
293
                                     os.getpid(),
294
                                     random.randint(0,0x7FFFFFFF))
295
        if not self.quiet:
296
            self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
297
        self._up_rename(old_relpath, stamp)
298
        self._pending_renames.append((stamp, new_relpath))
299
300
    def finish_renames(self):
301
        for (stamp, new_path) in self._pending_renames:
302
            self._up_rename(stamp, new_path)
303
        # The following shouldn't be needed since we use it once per upload,
304
        # but better safe than sorry ;-)
305
        self._pending_renames = []
306
307
    def upload_full_tree(self):
308
        self.to_transport.ensure_base() # XXX: Handle errors (add
309
                                        # --create-prefix option ?)
310
        self.tree.lock_read()
311
        try:
312
            for relpath, ie in self.tree.inventory.iter_entries():
313
                if relpath in ('', '.bzrignore', '.bzrignore-upload'):
314
                    # skip root ('')
315
                    # .bzrignore and .bzrignore-upload have no meaning outside
316
                    # a working tree so do not upload them
317
                    continue
318
                if self.is_ignored(relpath):
319
                    if not self.quiet:
320
                        self.outf.write('Ignoring %s\n' % relpath)
321
                    continue
322
                if ie.kind == 'file':
323
                    self.upload_file_robustly(relpath, ie.file_id)
324
                elif ie.kind == 'directory':
325
                    self.make_remote_dir_robustly(relpath)
326
                elif ie.kind == 'symlink':
327
                    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.
328
                        target = self.tree.path_content_summary(relpath)[3]
0.165.1 by Jelmer Vernooij
Support lazily loading.
329
                        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.
330
                                        % (relpath, target))
0.165.1 by Jelmer Vernooij
Support lazily loading.
331
                else:
332
                    raise NotImplementedError
333
            self.set_uploaded_revid(self.rev_id)
334
        finally:
335
            self.tree.unlock()
336
337
    def upload_tree(self):
338
        # If we can't find the revid file on the remote location, upload the
339
        # full tree instead
340
        rev_id = self.get_uploaded_revid()
341
342
        if rev_id == revision.NULL_REVISION:
343
            if not self.quiet:
344
                self.outf.write('No uploaded revision id found,'
345
                                ' switching to full upload\n')
346
            self.upload_full_tree()
347
            # We're done
348
            return
349
350
        # Check if the revision hasn't already been uploaded
351
        if rev_id == self.rev_id:
352
            if not self.quiet:
353
                self.outf.write('Remote location already up to date\n')
354
355
        from_tree = self.branch.repository.revision_tree(rev_id)
356
        self.to_transport.ensure_base() # XXX: Handle errors (add
357
                                        # --create-prefix option ?)
358
        changes = self.tree.changes_from(from_tree)
359
        self.tree.lock_read()
360
        try:
361
            for (path, id, kind) in changes.removed:
362
                if self.is_ignored(path):
363
                    if not self.quiet:
364
                        self.outf.write('Ignoring %s\n' % path)
365
                    continue
366
                if kind is 'file':
367
                    self.delete_remote_file(path)
368
                elif kind is  'directory':
369
                    self.delete_remote_dir_maybe(path)
370
                elif kind == 'symlink':
371
                    if not self.quiet:
372
                        target = self.tree.path_content_summary(path)[3]
373
                        self.outf.write('Not deleting remote symlink %s -> %s\n'
374
                                        % (path, target))
375
                else:
376
                    raise NotImplementedError
377
378
            for (old_path, new_path, id, kind,
379
                 content_change, exec_change) in changes.renamed:
380
                if self.is_ignored(old_path) and self.is_ignored(new_path):
381
                    if not self.quiet:
382
                        self.outf.write('Ignoring %s\n' % old_path)
383
                        self.outf.write('Ignoring %s\n' % new_path)
384
                    continue
385
                if content_change:
386
                    # We update the old_path content because renames and
387
                    # deletions are differed.
388
                    self.upload_file(old_path, id)
389
                if kind == 'symlink':
390
                    if not self.quiet:
391
                        self.outf.write('Not renaming remote symlink %s to %s\n'
392
                                        % (old_path, new_path))
393
                else:
394
                    self.rename_remote(old_path, new_path)
395
            self.finish_renames()
396
            self.finish_deletions()
397
398
            for (path, id, old_kind, new_kind) in changes.kind_changed:
399
                if self.is_ignored(path):
400
                    if not self.quiet:
401
                        self.outf.write('Ignoring %s\n' % path)
402
                    continue
403
                if old_kind == 'file':
404
                    self.delete_remote_file(path)
405
                elif old_kind ==  'directory':
406
                    self.delete_remote_dir(path)
407
                else:
408
                    raise NotImplementedError
409
410
                if new_kind == 'file':
411
                    self.upload_file(path, id)
412
                elif new_kind is 'directory':
413
                    self.make_remote_dir(path)
414
                else:
415
                    raise NotImplementedError
416
417
            for (path, id, kind) in changes.added:
418
                if self.is_ignored(path):
419
                    if not self.quiet:
420
                        self.outf.write('Ignoring %s\n' % path)
421
                    continue
422
                if kind == 'file':
423
                    self.upload_file(path, id)
424
                elif kind == 'directory':
425
                    self.make_remote_dir(path)
426
                elif kind == 'symlink':
427
                    if not self.quiet:
428
                        target = self.tree.path_content_summary(path)[3]
429
                        self.outf.write('Not uploading symlink %s -> %s\n'
430
                                        % (path, target))
431
                else:
432
                    raise NotImplementedError
433
434
            # XXX: Add a test for exec_change
435
            for (path, id, kind,
436
                 content_change, exec_change) in changes.modified:
437
                if self.is_ignored(path):
438
                    if not self.quiet:
439
                        self.outf.write('Ignoring %s\n' % path)
440
                    continue
441
                if kind is 'file':
442
                    self.upload_file(path, id)
443
                else:
444
                    raise NotImplementedError
445
446
            self.set_uploaded_revid(self.rev_id)
447
        finally:
448
            self.tree.unlock()
449
450
451
class CannotUploadToWorkingTree(errors.BzrCommandError):
452
453
    _fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
454
455
456
class DivergedUploadedTree(errors.BzrCommandError):
457
458
    _fmt = ("Your branch (%(revid)s)"
459
            " and the uploaded tree (%(uploaded_revid)s) have diverged: ")
460
461
462
class cmd_upload(commands.Command):
463
    """Upload a working tree, as a whole or incrementally.
464
465
    If no destination is specified use the last one used.
466
    If no revision is specified upload the changes since the last upload.
467
468
    Changes include files added, renamed, modified or removed.
469
    """
470
    _see_also = ['plugins/upload']
471
    takes_args = ['location?']
472
    takes_options = [
473
        'revision',
474
        'remember',
475
        'overwrite',
476
        option.Option('full', 'Upload the full working tree.'),
477
        option.Option('quiet', 'Do not output what is being done.',
478
                       short_name='q'),
479
        option.Option('directory',
480
                      help='Branch to upload from, '
481
                      'rather than the one containing the working directory.',
482
                      short_name='d',
483
                      type=unicode,
484
                      ),
485
        option.Option('auto',
486
                      'Trigger an upload from this branch whenever the tip '
487
                      'revision changes.')
488
       ]
489
490
    def run(self, location=None, full=False, revision=None, remember=None,
491
            directory=None, quiet=False, auto=None, overwrite=False
492
            ):
493
        if directory is None:
494
            directory = u'.'
495
496
        (wt, branch,
497
         relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
498
499
        if wt:
500
            wt.lock_read()
501
            locked = wt
502
        else:
503
            branch.lock_read()
504
            locked = branch
505
        try:
506
            if wt:
507
                changes = wt.changes_from(wt.basis_tree())
508
509
                if revision is None and  changes.has_changed():
510
                    raise errors.UncommittedChanges(wt)
511
512
            if location is None:
513
                stored_loc = get_upload_location(branch)
514
                if stored_loc is None:
515
                    raise errors.BzrCommandError(
516
                        'No upload location known or specified.')
517
                else:
518
                    # FIXME: Not currently tested
519
                    display_url = urlutils.unescape_for_display(stored_loc,
520
                            self.outf.encoding)
521
                    self.outf.write("Using saved location: %s\n" % display_url)
522
                    location = stored_loc
523
524
            to_transport = transport.get_transport(location)
525
526
            # Check that we are not uploading to a existing working tree.
527
            try:
528
                to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport)
529
                has_wt = to_bzr_dir.has_workingtree()
530
            except errors.NotBranchError:
531
                has_wt = False
532
            except errors.NotLocalUrl:
533
                # The exception raised is a bit weird... but that's life.
534
                has_wt = True
535
536
            if has_wt:
537
                raise CannotUploadToWorkingTree(url=location)
538
            if revision is None:
539
                rev_id = branch.last_revision()
540
            else:
541
                if len(revision) != 1:
542
                    raise errors.BzrCommandError(
543
                        'bzr upload --revision takes exactly 1 argument')
544
                rev_id = revision[0].in_history(branch).rev_id
545
546
            tree = branch.repository.revision_tree(rev_id)
547
548
            uploader = BzrUploader(branch, to_transport, self.outf, tree,
549
                                   rev_id, quiet=quiet)
550
551
            if not overwrite:
552
                prev_uploaded_rev_id = uploader.get_uploaded_revid()
553
                graph = branch.repository.get_graph()
554
                if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
555
                    raise DivergedUploadedTree(
556
                        revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
557
            if full:
558
                uploader.upload_full_tree()
559
            else:
560
                uploader.upload_tree()
561
        finally:
562
            locked.unlock()
563
564
        # We uploaded successfully, remember it
565
        if get_upload_location(branch) is None or remember:
566
            set_upload_location(branch, urlutils.unescape(to_transport.base))
567
        if auto is not None:
568
            set_upload_auto(branch, auto)