~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-30 03:38:47 UTC
  • mfrom: (1393.2.4)
  • mto: (1185.14.2)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: mbp@sourcefrog.net-20050930033847-e78ce2a6670c1a29
- merge Transport from John into newformat

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import os
20
20
import errno
21
21
from warnings import warn
 
22
from cStringIO import StringIO
22
23
 
23
24
 
24
25
import bzrlib
29
30
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
30
31
                           NoSuchRevision, HistoryMissing, NotBranchError,
31
32
                           DivergedBranches, LockError, UnlistableStore,
32
 
                           UnlistableBranch)
 
33
                           UnlistableBranch, NoSuchFile)
33
34
from bzrlib.textui import show_status
34
35
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
35
36
from bzrlib.delta import compare_trees
36
37
from bzrlib.tree import EmptyTree, RevisionTree
37
38
from bzrlib.inventory import Inventory
38
 
from bzrlib.weavestore import WeaveStore
39
 
from bzrlib.store import copy_all, ImmutableStore
 
39
from bzrlib.store import copy_all
 
40
from bzrlib.store.compressed_text import CompressedTextStore
 
41
from bzrlib.store.text import TextStore
 
42
from bzrlib.store.weave import WeaveStore
 
43
from bzrlib.transport import Transport, get_transport
40
44
import bzrlib.xml5
41
45
import bzrlib.ui
42
46
 
55
59
    # XXX: leave this here for about one release, then remove it
56
60
    raise NotImplementedError('find_branch() is not supported anymore, '
57
61
                              'please use one of the new branch constructors')
58
 
 
59
62
def _relpath(base, path):
60
63
    """Return path relative to base, or raise exception.
61
64
 
83
86
    return os.sep.join(s)
84
87
        
85
88
 
86
 
def find_branch_root(f=None):
87
 
    """Find the branch root enclosing f, or pwd.
88
 
 
89
 
    f may be a filename or a URL.
90
 
 
91
 
    It is not necessary that f exists.
 
89
def find_branch_root(t):
 
90
    """Find the branch root enclosing the transport's base.
 
91
 
 
92
    t is a Transport object.
 
93
 
 
94
    It is not necessary that the base of t exists.
92
95
 
93
96
    Basically we keep looking up until we find the control directory or
94
97
    run into the root.  If there isn't one, raises NotBranchError.
95
98
    """
96
 
    if f == None:
97
 
        f = os.getcwd()
98
 
    elif hasattr(os.path, 'realpath'):
99
 
        f = os.path.realpath(f)
100
 
    else:
101
 
        f = os.path.abspath(f)
102
 
    if not os.path.exists(f):
103
 
        raise BzrError('%r does not exist' % f)
104
 
        
105
 
 
106
 
    orig_f = f
107
 
 
 
99
    orig_base = t.base
108
100
    while True:
109
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
110
 
            return f
111
 
        head, tail = os.path.split(f)
112
 
        if head == f:
 
101
        if t.has(bzrlib.BZRDIR):
 
102
            return t
 
103
        new_t = t.clone('..')
 
104
        if new_t.base == t.base:
113
105
            # reached the root, whatever that may be
114
 
            raise NotBranchError('%s is not in a branch' % orig_f)
115
 
        f = head
116
 
 
117
 
 
 
106
            raise NotBranchError('%s is not in a branch' % orig_base)
 
107
        t = new_t
118
108
 
119
109
 
120
110
######################################################################
136
126
        """Open a branch which may be of an old format.
137
127
        
138
128
        Only local branches are supported."""
139
 
        return LocalBranch(base, find_root=False, relax_version_check=True)
 
129
        return _Branch(base, relax_version_check=True)
140
130
        
141
131
    @staticmethod
142
132
    def open(base):
143
133
        """Open an existing branch, rooted at 'base' (url)"""
144
 
        if base and (base.startswith('http://') or base.startswith('https://')):
145
 
            from bzrlib.remotebranch import RemoteBranch
146
 
            return RemoteBranch(base, find_root=False)
147
 
        else:
148
 
            return LocalBranch(base, find_root=False)
 
134
        t = get_transport(base)
 
135
        return _Branch(t)
149
136
 
150
137
    @staticmethod
151
138
    def open_containing(url):
153
140
        
154
141
        This probes for a branch at url, and searches upwards from there.
155
142
        """
156
 
        if url and (url.startswith('http://') or url.startswith('https://')):
157
 
            from bzrlib.remotebranch import RemoteBranch
158
 
            return RemoteBranch(url)
159
 
        else:
160
 
            return LocalBranch(url)
 
143
        t = get_transport(url)
 
144
        t = find_branch_root(t)
 
145
        return _Branch(t)
161
146
 
162
147
    @staticmethod
163
148
    def initialize(base):
164
149
        """Create a new branch, rooted at 'base' (url)"""
165
 
        if base and (base.startswith('http://') or base.startswith('https://')):
166
 
            from bzrlib.remotebranch import RemoteBranch
167
 
            return RemoteBranch(base, init=True)
168
 
        else:
169
 
            return LocalBranch(base, init=True)
 
150
        t = get_transport(base)
 
151
        return _Branch(t, init=True)
170
152
 
171
153
    def setup_caching(self, cache_root):
172
154
        """Subclasses that care about caching should override this, and set
174
156
        """
175
157
 
176
158
 
177
 
class LocalBranch(Branch):
 
159
class _Branch(Branch):
178
160
    """A branch stored in the actual filesystem.
179
161
 
180
162
    Note that it's "local" in the context of the filesystem; it doesn't
225
207
        except UnlistableStore:
226
208
            raise UnlistableBranch(from_store)
227
209
 
228
 
    def __init__(self, base, init=False, find_root=True,
 
210
    def __init__(self, transport, init=False,
229
211
                 relax_version_check=False):
230
212
        """Create new branch object at a particular location.
231
213
 
232
 
        base -- Base directory for the branch. May be a file:// url.
 
214
        transport -- A Transport object, defining how to access files.
 
215
                (If a string, transport.transport() will be used to
 
216
                create a Transport object)
233
217
        
234
218
        init -- If True, create new control files in a previously
235
219
             unversioned directory.  If False, the branch must already
236
220
             be versioned.
237
221
 
238
 
        find_root -- If true and init is false, find the root of the
239
 
             existing branch containing base.
240
 
 
241
222
        relax_version_check -- If true, the usual check for the branch
242
223
            version is not applied.  This is intended only for
243
224
            upgrade/recovery type use; it's not guaranteed that
246
227
        In the test suite, creation of new trees is tested using the
247
228
        `ScratchBranch` class.
248
229
        """
 
230
        assert isinstance(transport, Transport)
 
231
        self._transport = transport
249
232
        if init:
250
 
            self.base = os.path.realpath(base)
251
233
            self._make_control()
252
 
        elif find_root:
253
 
            self.base = find_branch_root(base)
254
 
        else:
255
 
            if base.startswith("file://"):
256
 
                base = base[7:]
257
 
            self.base = os.path.realpath(base)
258
 
            if not isdir(self.controlfilename('.')):
259
 
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
260
 
                                     ['use "bzr init" to initialize a '
261
 
                                      'new working tree'])
262
234
        self._check_format(relax_version_check)
263
 
        cfn = self.controlfilename
 
235
 
 
236
        def get_store(name, compressed=True):
 
237
            relpath = self._rel_controlfilename(name)
 
238
            if compressed:
 
239
                store = CompressedTextStore(self._transport.clone(relpath))
 
240
            else:
 
241
                store = TextStore(self._transport.clone(relpath))
 
242
            if self._transport.should_cache():
 
243
                from meta_store import CachedStore
 
244
                cache_path = os.path.join(self.cache_root, name)
 
245
                os.mkdir(cache_path)
 
246
                store = CachedStore(store, cache_path)
 
247
            return store
 
248
        def get_weave(name):
 
249
            relpath = self._rel_controlfilename(name)
 
250
            ws = WeaveStore(self._transport.clone(relpath))
 
251
            if self._transport.should_cache():
 
252
                ws.enable_cache = True
 
253
            return ws
 
254
 
264
255
        if self._branch_format == 4:
265
 
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
266
 
            self.text_store = ImmutableStore(cfn('text-store'))
 
256
            self.inventory_store = get_store('inventory-store')
 
257
            self.text_store = get_store('text-store')
 
258
            self.revision_store = get_store('revision-store')
267
259
        elif self._branch_format == 5:
268
 
            self.control_weaves = WeaveStore(cfn([]))
269
 
            self.weave_store = WeaveStore(cfn('weaves'))
270
 
            if init:
271
 
                # FIXME: Unify with make_control_files
272
 
                self.control_weaves.put_empty_weave('inventory')
273
 
                self.control_weaves.put_empty_weave('ancestry')
274
 
        self.revision_store = ImmutableStore(cfn('revision-store'))
275
 
 
 
260
            self.control_weaves = get_weave([])
 
261
            self.weave_store = get_weave('weaves')
 
262
            self.revision_store = get_store('revision-store', compressed=False)
276
263
 
277
264
    def __str__(self):
278
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
265
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
279
266
 
280
267
 
281
268
    __repr__ = __str__
288
275
            warn("branch %r was not explicitly unlocked" % self)
289
276
            self._lock.unlock()
290
277
 
 
278
        # TODO: It might be best to do this somewhere else,
 
279
        # but it is nice for a Branch object to automatically
 
280
        # cache it's information.
 
281
        # Alternatively, we could have the Transport objects cache requests
 
282
        # See the earlier discussion about how major objects (like Branch)
 
283
        # should never expect their __del__ function to run.
 
284
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
285
            try:
 
286
                import shutil
 
287
                shutil.rmtree(self.cache_root)
 
288
            except:
 
289
                pass
 
290
            self.cache_root = None
 
291
 
 
292
    def _get_base(self):
 
293
        if self._transport:
 
294
            return self._transport.base
 
295
        return None
 
296
 
 
297
    base = property(_get_base)
 
298
 
 
299
 
291
300
    def lock_write(self):
 
301
        # TODO: Upgrade locking to support using a Transport,
 
302
        # and potentially a remote locking protocol
292
303
        if self._lock_mode:
293
304
            if self._lock_mode != 'w':
294
305
                raise LockError("can't upgrade to a write lock from %r" %
295
306
                                self._lock_mode)
296
307
            self._lock_count += 1
297
308
        else:
298
 
            from bzrlib.lock import WriteLock
299
 
 
300
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
309
            self._lock = self._transport.lock_write(
 
310
                    self._rel_controlfilename('branch-lock'))
301
311
            self._lock_mode = 'w'
302
312
            self._lock_count = 1
303
313
 
308
318
                   "invalid lock mode %r" % self._lock_mode
309
319
            self._lock_count += 1
310
320
        else:
311
 
            from bzrlib.lock import ReadLock
312
 
 
313
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
321
            self._lock = self._transport.lock_read(
 
322
                    self._rel_controlfilename('branch-lock'))
314
323
            self._lock_mode = 'r'
315
324
            self._lock_count = 1
316
325
                        
327
336
 
328
337
    def abspath(self, name):
329
338
        """Return absolute filename for something in the branch"""
330
 
        return os.path.join(self.base, name)
 
339
        return self._transport.abspath(name)
331
340
 
332
341
    def relpath(self, path):
333
342
        """Return path relative to this branch of something inside it.
334
343
 
335
344
        Raises an error if path is not in this branch."""
336
 
        return _relpath(self.base, path)
 
345
        return self._transport.relpath(path)
 
346
 
 
347
 
 
348
    def _rel_controlfilename(self, file_or_path):
 
349
        if isinstance(file_or_path, basestring):
 
350
            file_or_path = [file_or_path]
 
351
        return [bzrlib.BZRDIR] + file_or_path
337
352
 
338
353
    def controlfilename(self, file_or_path):
339
354
        """Return location relative to branch."""
340
 
        if isinstance(file_or_path, basestring):
341
 
            file_or_path = [file_or_path]
342
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
355
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
343
356
 
344
357
 
345
358
    def controlfile(self, file_or_path, mode='r'):
353
366
        Controlfiles should almost never be opened in write mode but
354
367
        rather should be atomically copied and replaced using atomicfile.
355
368
        """
356
 
 
357
 
        fn = self.controlfilename(file_or_path)
358
 
 
359
 
        if mode == 'rb' or mode == 'wb':
360
 
            return file(fn, mode)
361
 
        elif mode == 'r' or mode == 'w':
362
 
            # open in binary mode anyhow so there's no newline translation;
363
 
            # codecs uses line buffering by default; don't want that.
364
 
            import codecs
365
 
            return codecs.open(fn, mode + 'b', 'utf-8',
366
 
                               buffering=60000)
 
369
        import codecs
 
370
 
 
371
        relpath = self._rel_controlfilename(file_or_path)
 
372
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
373
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
374
        if mode == 'rb': 
 
375
            return self._transport.get(relpath)
 
376
        elif mode == 'wb':
 
377
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
378
        elif mode == 'r':
 
379
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
380
        elif mode == 'w':
 
381
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
367
382
        else:
368
383
            raise BzrError("invalid controlfile mode %r" % mode)
369
384
 
 
385
    def put_controlfile(self, path, f, encode=True):
 
386
        """Write an entry as a controlfile.
 
387
 
 
388
        :param path: The path to put the file, relative to the .bzr control
 
389
                     directory
 
390
        :param f: A file-like or string object whose contents should be copied.
 
391
        :param encode:  If true, encode the contents as utf-8
 
392
        """
 
393
        self.put_controlfiles([(path, f)], encode=encode)
 
394
 
 
395
    def put_controlfiles(self, files, encode=True):
 
396
        """Write several entries as controlfiles.
 
397
 
 
398
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
399
                      underneath the bzr control directory
 
400
        :param encode:  If true, encode the contents as utf-8
 
401
        """
 
402
        import codecs
 
403
        ctrl_files = []
 
404
        for path, f in files:
 
405
            if encode:
 
406
                if isinstance(f, basestring):
 
407
                    f = f.encode('utf-8', 'replace')
 
408
                else:
 
409
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
410
            path = self._rel_controlfilename(path)
 
411
            ctrl_files.append((path, f))
 
412
        self._transport.put_multi(ctrl_files)
 
413
 
370
414
    def _make_control(self):
371
 
        os.mkdir(self.controlfilename([]))
372
 
        self.controlfile('README', 'w').write(
373
 
            "This is a Bazaar-NG control directory.\n"
374
 
            "Do not change any files in this directory.\n")
375
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
376
 
        for d in ('text-store', 'revision-store',
377
 
                  'weaves'):
378
 
            os.mkdir(self.controlfilename(d))
379
 
        for f in ('revision-history',
380
 
                  'branch-name',
381
 
                  'branch-lock',
382
 
                  'pending-merges'):
383
 
            self.controlfile(f, 'w').write('')
384
 
        mutter('created control directory in ' + self.base)
385
 
 
 
415
        from bzrlib.inventory import Inventory
 
416
        from bzrlib.weavefile import write_weave_v5
 
417
        from bzrlib.weave import Weave
 
418
        
 
419
        # Create an empty inventory
 
420
        sio = StringIO()
386
421
        # if we want per-tree root ids then this is the place to set
387
422
        # them; they're not needed for now and so ommitted for
388
423
        # simplicity.
389
 
        f = self.controlfile('inventory','w')
390
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
 
424
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
425
        empty_inv = sio.getvalue()
 
426
        sio = StringIO()
 
427
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
428
        empty_weave = sio.getvalue()
391
429
 
 
430
        dirs = [[], 'revision-store', 'weaves']
 
431
        files = [('README', 
 
432
            "This is a Bazaar-NG control directory.\n"
 
433
            "Do not change any files in this directory.\n"),
 
434
            ('branch-format', BZR_BRANCH_FORMAT_5),
 
435
            ('revision-history', ''),
 
436
            ('branch-name', ''),
 
437
            ('branch-lock', ''),
 
438
            ('pending-merges', ''),
 
439
            ('inventory', empty_inv),
 
440
            ('inventory.weave', empty_weave),
 
441
            ('ancestry.weave', empty_weave)
 
442
        ]
 
443
        cfn = self._rel_controlfilename
 
444
        self._transport.mkdir_multi([cfn(d) for d in dirs])
 
445
        self.put_controlfiles(files)
 
446
        mutter('created control directory in ' + self._transport.base)
392
447
 
393
448
    def _check_format(self, relax_version_check):
394
449
        """Check this branch format is supported.
401
456
        """
402
457
        try:
403
458
            fmt = self.controlfile('branch-format', 'r').read()
404
 
        except IOError, e:
405
 
            if e.errno == errno.ENOENT:
406
 
                raise NotBranchError(self.base)
407
 
            else:
408
 
                raise
 
459
        except NoSuchFile:
 
460
            raise NotBranchError(self.base)
409
461
 
410
462
        if fmt == BZR_BRANCH_FORMAT_5:
411
463
            self._branch_format = 5
416
468
            and self._branch_format != 5):
417
469
            raise BzrError('sorry, branch format %r not supported' % fmt,
418
470
                           ['use a different bzr version',
419
 
                            'or remove the .bzr directory and "bzr init" again'])
 
471
                            'or remove the .bzr directory'
 
472
                            ' and "bzr init" again'])
420
473
 
421
474
    def get_root_id(self):
422
475
        """Return the id of this branches root"""
453
506
        That is to say, the inventory describing changes underway, that
454
507
        will be committed to the next revision.
455
508
        """
456
 
        from bzrlib.atomicfile import AtomicFile
457
 
        
 
509
        from cStringIO import StringIO
458
510
        self.lock_write()
459
511
        try:
460
 
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
461
 
            try:
462
 
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
463
 
                f.commit()
464
 
            finally:
465
 
                f.close()
 
512
            sio = StringIO()
 
513
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
514
            sio.seek(0)
 
515
            # Transport handles atomicity
 
516
            self.put_controlfile('inventory', sio)
466
517
        finally:
467
518
            self.unlock()
468
519
        
633
684
 
634
685
 
635
686
    def append_revision(self, *revision_ids):
636
 
        from bzrlib.atomicfile import AtomicFile
637
 
 
638
687
        for revision_id in revision_ids:
639
688
            mutter("add {%s} to revision-history" % revision_id)
640
689
 
641
690
        rev_history = self.revision_history()
642
691
        rev_history.extend(revision_ids)
643
692
 
644
 
        f = AtomicFile(self.controlfilename('revision-history'))
 
693
        self.lock_write()
645
694
        try:
646
 
            for rev_id in rev_history:
647
 
                print >>f, rev_id
648
 
            f.commit()
 
695
            self.put_controlfile('revision-history', '\n'.join(rev_history))
649
696
        finally:
650
 
            f.close()
 
697
            self.unlock()
651
698
 
652
699
 
653
700
    def has_revision(self, revision_id):
714
761
 
715
762
        return compare_trees(old_tree, new_tree)
716
763
 
717
 
 
718
764
    def get_revision_sha1(self, revision_id):
719
765
        """Hash the stored value of a revision, and return it."""
720
766
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
951
997
    def working_tree(self):
952
998
        """Return a `Tree` for the working copy."""
953
999
        from bzrlib.workingtree import WorkingTree
954
 
        return WorkingTree(self.base, self.read_working_inventory())
 
1000
        # TODO: In the future, WorkingTree should utilize Transport
 
1001
        return WorkingTree(self._transport.base, self.read_working_inventory())
955
1002
 
956
1003
 
957
1004
    def basis_tree(self):
1131
1178
        These are revisions that have been merged into the working
1132
1179
        directory but not yet committed.
1133
1180
        """
1134
 
        cfn = self.controlfilename('pending-merges')
1135
 
        if not os.path.exists(cfn):
 
1181
        cfn = self._rel_controlfilename('pending-merges')
 
1182
        if not self._transport.has(cfn):
1136
1183
            return []
1137
1184
        p = []
1138
1185
        for l in self.controlfile('pending-merges', 'r').readlines():
1140
1187
        return p
1141
1188
 
1142
1189
 
1143
 
    def add_pending_merge(self, revision_id):
1144
 
        validate_revision_id(revision_id)
 
1190
    def add_pending_merge(self, *revision_ids):
1145
1191
        # TODO: Perhaps should check at this point that the
1146
1192
        # history of the revision is actually present?
 
1193
        for rev_id in revision_ids:
 
1194
            validate_revision_id(rev_id)
 
1195
 
1147
1196
        p = self.pending_merges()
1148
 
        if revision_id in p:
1149
 
            return
1150
 
        p.append(revision_id)
1151
 
        self.set_pending_merges(p)
1152
 
 
 
1197
        updated = False
 
1198
        for rev_id in revision_ids:
 
1199
            if rev_id in p:
 
1200
                continue
 
1201
            p.append(rev_id)
 
1202
            updated = True
 
1203
        if updated:
 
1204
            self.set_pending_merges(p)
1153
1205
 
1154
1206
    def set_pending_merges(self, rev_list):
1155
 
        from bzrlib.atomicfile import AtomicFile
1156
1207
        self.lock_write()
1157
1208
        try:
1158
 
            f = AtomicFile(self.controlfilename('pending-merges'))
1159
 
            try:
1160
 
                for l in rev_list:
1161
 
                    print >>f, l
1162
 
                f.commit()
1163
 
            finally:
1164
 
                f.close()
 
1209
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1165
1210
        finally:
1166
1211
            self.unlock()
1167
1212
 
1218
1263
        
1219
1264
 
1220
1265
 
1221
 
class ScratchBranch(LocalBranch):
 
1266
class ScratchBranch(_Branch):
1222
1267
    """Special test class: a branch that cleans up after itself.
1223
1268
 
1224
1269
    >>> b = ScratchBranch()
1241
1286
        if base is None:
1242
1287
            base = mkdtemp()
1243
1288
            init = True
1244
 
        LocalBranch.__init__(self, base, init=init)
 
1289
        if isinstance(base, basestring):
 
1290
            base = get_transport(base)
 
1291
        _Branch.__init__(self, base, init=init)
1245
1292
        for d in dirs:
1246
 
            os.mkdir(self.abspath(d))
 
1293
            self._transport.mkdir(d)
1247
1294
            
1248
1295
        for f in files:
1249
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1296
            self._transport.put(f, 'content of %s' % f)
1250
1297
 
1251
1298
 
1252
1299
    def clone(self):
1289
1336
                for name in files:
1290
1337
                    os.chmod(os.path.join(root, name), 0700)
1291
1338
            rmtree(self.base)
1292
 
        self.base = None
 
1339
        self._transport = None
1293
1340
 
1294
1341
    
1295
1342