/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 cmds.py

(jelmer) More lazy loading

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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:
 
328
                        target = self.tree.path_content_summary(path)[3]
 
329
                        self.outf.write('Not uploading symlink %s -> %s\n'
 
330
                                        % (path, target))
 
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
        if auto and not auto_hook_available:
 
497
            raise BzrCommandError("Your version of bzr does not have the "
 
498
                    "hooks necessary for --auto to work")
 
499
 
 
500
        (wt, branch,
 
501
         relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
 
502
 
 
503
        if wt:
 
504
            wt.lock_read()
 
505
            locked = wt
 
506
        else:
 
507
            branch.lock_read()
 
508
            locked = branch
 
509
        try:
 
510
            if wt:
 
511
                changes = wt.changes_from(wt.basis_tree())
 
512
 
 
513
                if revision is None and  changes.has_changed():
 
514
                    raise errors.UncommittedChanges(wt)
 
515
 
 
516
            if location is None:
 
517
                stored_loc = get_upload_location(branch)
 
518
                if stored_loc is None:
 
519
                    raise errors.BzrCommandError(
 
520
                        'No upload location known or specified.')
 
521
                else:
 
522
                    # FIXME: Not currently tested
 
523
                    display_url = urlutils.unescape_for_display(stored_loc,
 
524
                            self.outf.encoding)
 
525
                    self.outf.write("Using saved location: %s\n" % display_url)
 
526
                    location = stored_loc
 
527
 
 
528
            to_transport = transport.get_transport(location)
 
529
 
 
530
            # Check that we are not uploading to a existing working tree.
 
531
            try:
 
532
                to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport)
 
533
                has_wt = to_bzr_dir.has_workingtree()
 
534
            except errors.NotBranchError:
 
535
                has_wt = False
 
536
            except errors.NotLocalUrl:
 
537
                # The exception raised is a bit weird... but that's life.
 
538
                has_wt = True
 
539
 
 
540
            if has_wt:
 
541
                raise CannotUploadToWorkingTree(url=location)
 
542
            if revision is None:
 
543
                rev_id = branch.last_revision()
 
544
            else:
 
545
                if len(revision) != 1:
 
546
                    raise errors.BzrCommandError(
 
547
                        'bzr upload --revision takes exactly 1 argument')
 
548
                rev_id = revision[0].in_history(branch).rev_id
 
549
 
 
550
            tree = branch.repository.revision_tree(rev_id)
 
551
 
 
552
            uploader = BzrUploader(branch, to_transport, self.outf, tree,
 
553
                                   rev_id, quiet=quiet)
 
554
 
 
555
            if not overwrite:
 
556
                prev_uploaded_rev_id = uploader.get_uploaded_revid()
 
557
                graph = branch.repository.get_graph()
 
558
                if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
 
559
                    raise DivergedUploadedTree(
 
560
                        revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
 
561
            if full:
 
562
                uploader.upload_full_tree()
 
563
            else:
 
564
                uploader.upload_tree()
 
565
        finally:
 
566
            locked.unlock()
 
567
 
 
568
        # We uploaded successfully, remember it
 
569
        if get_upload_location(branch) is None or remember:
 
570
            set_upload_location(branch, urlutils.unescape(to_transport.base))
 
571
        if auto is not None:
 
572
            set_upload_auto(branch, auto)
 
573
 
 
574
 
 
575
def auto_upload_hook(params):
 
576
    source_branch = params.branch
 
577
    destination = get_upload_location(source_branch)
 
578
    if destination is None:
 
579
        return
 
580
    auto_upload = get_upload_auto(source_branch)
 
581
    if not auto_upload:
 
582
        return
 
583
    quiet = get_upload_auto_quiet(source_branch)
 
584
    if not quiet:
 
585
        display_url = urlutils.unescape_for_display(
 
586
            destination, osutils.get_terminal_encoding())
 
587
        trace.note('Automatically uploading to %s', display_url)
 
588
    to_transport = transport.get_transport(destination)
 
589
    last_revision = source_branch.last_revision()
 
590
    last_tree = source_branch.repository.revision_tree(last_revision)
 
591
    uploader = BzrUploader(source_branch, to_transport, sys.stdout,
 
592
                           last_tree, last_revision, quiet=quiet)
 
593
    uploader.upload_tree()
 
594
 
 
595
 
 
596
def install_auto_upload_hook():
 
597
    branch.Branch.hooks.install_named_hook('post_change_branch_tip',
 
598
            auto_upload_hook,
 
599
            'Auto upload code from a branch when it is changed.')
 
600
 
 
601
 
 
602
if hasattr(branch.Branch.hooks, "install_named_hook"):
 
603
    install_auto_upload_hook()
 
604
    auto_hook_available = True
 
605
else:
 
606
    auto_hook_available = False
 
607
 
 
608
 
 
609
def load_tests(basic_tests, module, loader):
 
610
    # This module shouldn't define any tests but I don't know how to report
 
611
    # that. I prefer to update basic_tests with the other tests to detect
 
612
    # unwanted tests and I think that's sufficient.
 
613
 
 
614
    testmod_names = [
 
615
        'tests',
 
616
        ]
 
617
    basic_tests.addTest(loader.loadTestsFromModuleNames(
 
618
            ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
 
619
    return basic_tests