/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 breezy/git/transportgit.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2018-11-16 18:26:22 UTC
  • mfrom: (7167.1.4 run-flake8)
  • Revision ID: breezy.the.bot@gmail.com-20181116182622-qw3gan3hz78a2imw
Add a flake8 test.

Merged from https://code.launchpad.net/~jelmer/brz/run-flake8/+merge/358902

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010-2018 Jelmer Vernooij <jelmer@jelmer.uk>
 
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""A Git repository implementation that uses a Bazaar transport."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from io import BytesIO
 
22
 
 
23
import os
 
24
import sys
 
25
 
 
26
from dulwich.errors import (
 
27
    NoIndexPresent,
 
28
    )
 
29
from dulwich.file import (
 
30
    GitFile,
 
31
    FileLocked,
 
32
    )
 
33
from dulwich.objects import (
 
34
    ShaFile,
 
35
    )
 
36
from dulwich.object_store import (
 
37
    PackBasedObjectStore,
 
38
    PACKDIR,
 
39
    )
 
40
from dulwich.pack import (
 
41
    MemoryPackIndex,
 
42
    PackData,
 
43
    Pack,
 
44
    iter_sha1,
 
45
    load_pack_index_file,
 
46
    write_pack_objects,
 
47
    write_pack_index_v2,
 
48
    )
 
49
from dulwich.repo import (
 
50
    BaseRepo,
 
51
    InfoRefsContainer,
 
52
    RefsContainer,
 
53
    BASE_DIRECTORIES,
 
54
    COMMONDIR,
 
55
    CONTROLDIR,
 
56
    INDEX_FILENAME,
 
57
    OBJECTDIR,
 
58
    SYMREF,
 
59
    check_ref_format,
 
60
    read_packed_refs_with_peeled,
 
61
    read_packed_refs,
 
62
    write_packed_refs,
 
63
    )
 
64
 
 
65
from .. import (
 
66
    osutils,
 
67
    transport as _mod_transport,
 
68
    urlutils,
 
69
    )
 
70
from ..errors import (
 
71
    AlreadyControlDirError,
 
72
    FileExists,
 
73
    LockBroken,
 
74
    LockContention,
 
75
    NotLocalUrl,
 
76
    NoSuchFile,
 
77
    ReadError,
 
78
    TransportNotPossible,
 
79
    )
 
80
 
 
81
from ..lock import LogicalLockResult
 
82
 
 
83
 
 
84
class TransportRefsContainer(RefsContainer):
 
85
    """Refs container that reads refs from a transport."""
 
86
 
 
87
    def __init__(self, transport, worktree_transport=None):
 
88
        self.transport = transport
 
89
        if worktree_transport is None:
 
90
            worktree_transport = transport
 
91
        self.worktree_transport = worktree_transport
 
92
        self._packed_refs = None
 
93
        self._peeled_refs = None
 
94
 
 
95
    def __repr__(self):
 
96
        return "%s(%r)" % (self.__class__.__name__, self.transport)
 
97
 
 
98
    def _ensure_dir_exists(self, path):
 
99
        for n in range(path.count("/")):
 
100
            dirname = "/".join(path.split("/")[:n+1])
 
101
            try:
 
102
                self.transport.mkdir(dirname)
 
103
            except FileExists:
 
104
                pass
 
105
 
 
106
    def subkeys(self, base):
 
107
        """Refs present in this container under a base.
 
108
 
 
109
        :param base: The base to return refs under.
 
110
        :return: A set of valid refs in this container under the base; the base
 
111
            prefix is stripped from the ref names returned.
 
112
        """
 
113
        keys = set()
 
114
        base_len = len(base) + 1
 
115
        for refname in self.allkeys():
 
116
            if refname.startswith(base):
 
117
                keys.add(refname[base_len:])
 
118
        return keys
 
119
 
 
120
    def allkeys(self):
 
121
        keys = set()
 
122
        try:
 
123
            self.worktree_transport.get_bytes("HEAD")
 
124
        except NoSuchFile:
 
125
            pass
 
126
        else:
 
127
            keys.add(b"HEAD")
 
128
        try:
 
129
            iter_files = list(self.transport.clone("refs").iter_files_recursive())
 
130
            for filename in iter_files:
 
131
                unquoted_filename = urlutils.unquote_to_bytes(filename)
 
132
                refname = osutils.pathjoin(b"refs", unquoted_filename)
 
133
                if check_ref_format(refname):
 
134
                    keys.add(refname)
 
135
        except (TransportNotPossible, NoSuchFile):
 
136
            pass
 
137
        keys.update(self.get_packed_refs())
 
138
        return keys
 
139
 
 
140
    def get_packed_refs(self):
 
141
        """Get contents of the packed-refs file.
 
142
 
 
143
        :return: Dictionary mapping ref names to SHA1s
 
144
 
 
145
        :note: Will return an empty dictionary when no packed-refs file is
 
146
            present.
 
147
        """
 
148
        # TODO: invalidate the cache on repacking
 
149
        if self._packed_refs is None:
 
150
            # set both to empty because we want _peeled_refs to be
 
151
            # None if and only if _packed_refs is also None.
 
152
            self._packed_refs = {}
 
153
            self._peeled_refs = {}
 
154
            try:
 
155
                f = self.transport.get("packed-refs")
 
156
            except NoSuchFile:
 
157
                return {}
 
158
            try:
 
159
                first_line = next(iter(f)).rstrip()
 
160
                if (first_line.startswith(b"# pack-refs") and b" peeled" in
 
161
                        first_line):
 
162
                    for sha, name, peeled in read_packed_refs_with_peeled(f):
 
163
                        self._packed_refs[name] = sha
 
164
                        if peeled:
 
165
                            self._peeled_refs[name] = peeled
 
166
                else:
 
167
                    f.seek(0)
 
168
                    for sha, name in read_packed_refs(f):
 
169
                        self._packed_refs[name] = sha
 
170
            finally:
 
171
                f.close()
 
172
        return self._packed_refs
 
173
 
 
174
    def get_peeled(self, name):
 
175
        """Return the cached peeled value of a ref, if available.
 
176
 
 
177
        :param name: Name of the ref to peel
 
178
        :return: The peeled value of the ref. If the ref is known not point to a
 
179
            tag, this will be the SHA the ref refers to. If the ref may point to
 
180
            a tag, but no cached information is available, None is returned.
 
181
        """
 
182
        self.get_packed_refs()
 
183
        if self._peeled_refs is None or name not in self._packed_refs:
 
184
            # No cache: no peeled refs were read, or this ref is loose
 
185
            return None
 
186
        if name in self._peeled_refs:
 
187
            return self._peeled_refs[name]
 
188
        else:
 
189
            # Known not peelable
 
190
            return self[name]
 
191
 
 
192
    def read_loose_ref(self, name):
 
193
        """Read a reference file and return its contents.
 
194
 
 
195
        If the reference file a symbolic reference, only read the first line of
 
196
        the file. Otherwise, only read the first 40 bytes.
 
197
 
 
198
        :param name: the refname to read, relative to refpath
 
199
        :return: The contents of the ref file, or None if the file does not
 
200
            exist.
 
201
        :raises IOError: if any other error occurs
 
202
        """
 
203
        if name == b'HEAD':
 
204
            transport = self.worktree_transport
 
205
        else:
 
206
            transport = self.transport
 
207
        try:
 
208
            f = transport.get(urlutils.quote_from_bytes(name))
 
209
        except NoSuchFile:
 
210
            return None
 
211
        with f:
 
212
            header = f.read(len(SYMREF))
 
213
            if header == SYMREF:
 
214
                # Read only the first line
 
215
                return header + next(iter(f)).rstrip(b"\r\n")
 
216
            else:
 
217
                # Read only the first 40 bytes
 
218
                return header + f.read(40-len(SYMREF))
 
219
 
 
220
    def _remove_packed_ref(self, name):
 
221
        if self._packed_refs is None:
 
222
            return
 
223
        # reread cached refs from disk, while holding the lock
 
224
 
 
225
        self._packed_refs = None
 
226
        self.get_packed_refs()
 
227
 
 
228
        if name not in self._packed_refs:
 
229
            return
 
230
 
 
231
        del self._packed_refs[name]
 
232
        if name in self._peeled_refs:
 
233
            del self._peeled_refs[name]
 
234
        f = self.transport.open_write_stream("packed-refs")
 
235
        try:
 
236
            write_packed_refs(f, self._packed_refs, self._peeled_refs)
 
237
        finally:
 
238
            f.close()
 
239
 
 
240
    def set_symbolic_ref(self, name, other):
 
241
        """Make a ref point at another ref.
 
242
 
 
243
        :param name: Name of the ref to set
 
244
        :param other: Name of the ref to point at
 
245
        """
 
246
        self._check_refname(name)
 
247
        self._check_refname(other)
 
248
        if name != b'HEAD':
 
249
            transport = self.transport
 
250
            self._ensure_dir_exists(urlutils.quote_from_bytes(name))
 
251
        else:
 
252
            transport = self.worktree_transport
 
253
        transport.put_bytes(urlutils.quote_from_bytes(name), SYMREF + other + b'\n')
 
254
 
 
255
    def set_if_equals(self, name, old_ref, new_ref):
 
256
        """Set a refname to new_ref only if it currently equals old_ref.
 
257
 
 
258
        This method follows all symbolic references, and can be used to perform
 
259
        an atomic compare-and-swap operation.
 
260
 
 
261
        :param name: The refname to set.
 
262
        :param old_ref: The old sha the refname must refer to, or None to set
 
263
            unconditionally.
 
264
        :param new_ref: The new sha the refname will refer to.
 
265
        :return: True if the set was successful, False otherwise.
 
266
        """
 
267
        try:
 
268
            realnames, _ = self.follow(name)
 
269
            realname = realnames[-1]
 
270
        except (KeyError, IndexError):
 
271
            realname = name
 
272
        if realname == b'HEAD':
 
273
            transport = self.worktree_transport
 
274
        else:
 
275
            transport = self.transport
 
276
            self._ensure_dir_exists(urlutils.quote_from_bytes(realname))
 
277
        transport.put_bytes(urlutils.quote_from_bytes(realname), new_ref+b"\n")
 
278
        return True
 
279
 
 
280
    def add_if_new(self, name, ref):
 
281
        """Add a new reference only if it does not already exist.
 
282
 
 
283
        This method follows symrefs, and only ensures that the last ref in the
 
284
        chain does not exist.
 
285
 
 
286
        :param name: The refname to set.
 
287
        :param ref: The new sha the refname will refer to.
 
288
        :return: True if the add was successful, False otherwise.
 
289
        """
 
290
        try:
 
291
            realnames, contents = self.follow(name)
 
292
            if contents is not None:
 
293
                return False
 
294
            realname = realnames[-1]
 
295
        except (KeyError, IndexError):
 
296
            realname = name
 
297
        self._check_refname(realname)
 
298
        if realname == b'HEAD':
 
299
            transport = self.worktree_transport
 
300
        else:
 
301
            transport = self.transport
 
302
            self._ensure_dir_exists(urlutils.quote_from_bytes(realname))
 
303
        transport.put_bytes(urlutils.quote_from_bytes(realname), ref+b"\n")
 
304
        return True
 
305
 
 
306
    def remove_if_equals(self, name, old_ref):
 
307
        """Remove a refname only if it currently equals old_ref.
 
308
 
 
309
        This method does not follow symbolic references. It can be used to
 
310
        perform an atomic compare-and-delete operation.
 
311
 
 
312
        :param name: The refname to delete.
 
313
        :param old_ref: The old sha the refname must refer to, or None to delete
 
314
            unconditionally.
 
315
        :return: True if the delete was successful, False otherwise.
 
316
        """
 
317
        self._check_refname(name)
 
318
        # may only be packed
 
319
        if name == b'HEAD':
 
320
            transport = self.worktree_transport
 
321
        else:
 
322
            transport = self.transport
 
323
        try:
 
324
            transport.delete(urlutils.quote_from_bytes(name))
 
325
        except NoSuchFile:
 
326
            pass
 
327
        self._remove_packed_ref(name)
 
328
        return True
 
329
 
 
330
    def get(self, name, default=None):
 
331
        try:
 
332
            return self[name]
 
333
        except KeyError:
 
334
            return default
 
335
 
 
336
    def unlock_ref(self, name):
 
337
        if name == b"HEAD":
 
338
            transport = self.worktree_transport
 
339
        else:
 
340
            transport = self.transport
 
341
        lockname = name + b".lock"
 
342
        try:
 
343
            self.transport.delete(urlutils.quote_from_bytes(lockname))
 
344
        except NoSuchFile:
 
345
            pass
 
346
 
 
347
    def lock_ref(self, name):
 
348
        if name == b"HEAD":
 
349
            transport = self.worktree_transport
 
350
        else:
 
351
            transport = self.transport
 
352
        self._ensure_dir_exists(urlutils.quote_from_bytes(name))
 
353
        lockname = urlutils.quote_from_bytes(name + b".lock")
 
354
        try:
 
355
            local_path = self.transport.local_abspath(urlutils.quote_from_bytes(name))
 
356
        except NotLocalUrl:
 
357
            # This is racy, but what can we do?
 
358
            if self.transport.has(lockname):
 
359
                raise LockContention(name)
 
360
            lock_result = self.transport.put_bytes(lockname, b'Locked by brz-git')
 
361
            return LogicalLockResult(lambda: self.transport.delete(lockname))
 
362
        else:
 
363
            try:
 
364
                gf = GitFile(local_path, 'wb')
 
365
            except FileLocked as e:
 
366
                raise LockContention(name, e)
 
367
            else:
 
368
                def unlock():
 
369
                    try:
 
370
                        self.transport.delete(lockname)
 
371
                    except NoSuchFile:
 
372
                        raise LockBroken(lockname)
 
373
                    # GitFile.abort doesn't care if the lock has already disappeared
 
374
                    gf.abort()
 
375
                return LogicalLockResult(unlock)
 
376
 
 
377
 
 
378
# TODO(jelmer): Use upstream read_gitfile; unfortunately that expects strings
 
379
# rather than bytes..
 
380
def read_gitfile(f):
 
381
    """Read a ``.git`` file.
 
382
 
 
383
    The first line of the file should start with "gitdir: "
 
384
 
 
385
    :param f: File-like object to read from
 
386
    :return: A path
 
387
    """
 
388
    cs = f.read()
 
389
    if not cs.startswith(b"gitdir: "):
 
390
        raise ValueError("Expected file to start with 'gitdir: '")
 
391
    return cs[len(b"gitdir: "):].rstrip(b"\n")
 
392
 
 
393
 
 
394
class TransportRepo(BaseRepo):
 
395
 
 
396
    def __init__(self, transport, bare, refs_text=None):
 
397
        self.transport = transport
 
398
        self.bare = bare
 
399
        try:
 
400
            with transport.get(CONTROLDIR) as f:
 
401
                path = read_gitfile(f)
 
402
        except (ReadError, NoSuchFile):
 
403
            if self.bare:
 
404
                self._controltransport = self.transport
 
405
            else:
 
406
                self._controltransport = self.transport.clone('.git')
 
407
        else:
 
408
            self._controltransport = self.transport.clone(urlutils.quote_from_bytes(path))
 
409
        commondir = self.get_named_file(COMMONDIR)
 
410
        if commondir is not None:
 
411
            with commondir:
 
412
                commondir = os.path.join(
 
413
                    self.controldir(),
 
414
                    commondir.read().rstrip(b"\r\n").decode(
 
415
                        sys.getfilesystemencoding()))
 
416
                self._commontransport = \
 
417
                    _mod_transport.get_transport_from_path(commondir)
 
418
        else:
 
419
            self._commontransport = self._controltransport
 
420
        object_store = TransportObjectStore(
 
421
            self._commontransport.clone(OBJECTDIR))
 
422
        if refs_text is not None:
 
423
            refs_container = InfoRefsContainer(BytesIO(refs_text))
 
424
            try:
 
425
                head = TransportRefsContainer(self._commontransport).read_loose_ref("HEAD")
 
426
            except KeyError:
 
427
                pass
 
428
            else:
 
429
                refs_container._refs["HEAD"] = head
 
430
        else:
 
431
            refs_container = TransportRefsContainer(
 
432
                    self._commontransport, self._controltransport)
 
433
        super(TransportRepo, self).__init__(object_store,
 
434
                refs_container)
 
435
 
 
436
    def controldir(self):
 
437
        return self._controltransport.local_abspath('.')
 
438
 
 
439
    def commondir(self):
 
440
        return self._commontransport.local_abspath('.')
 
441
 
 
442
    @property
 
443
    def path(self):
 
444
        return self.transport.local_abspath('.')
 
445
 
 
446
    def _determine_file_mode(self):
 
447
        # Be consistent with bzr
 
448
        if sys.platform == 'win32':
 
449
            return False
 
450
        return True
 
451
 
 
452
    def get_named_file(self, path):
 
453
        """Get a file from the control dir with a specific name.
 
454
 
 
455
        Although the filename should be interpreted as a filename relative to
 
456
        the control dir in a disk-baked Repo, the object returned need not be
 
457
        pointing to a file in that location.
 
458
 
 
459
        :param path: The path to the file, relative to the control dir.
 
460
        :return: An open file object, or None if the file does not exist.
 
461
        """
 
462
        try:
 
463
            return self._controltransport.get(path.lstrip('/'))
 
464
        except NoSuchFile:
 
465
            return None
 
466
 
 
467
    def _put_named_file(self, relpath, contents):
 
468
        self._controltransport.put_bytes(relpath, contents)
 
469
 
 
470
    def index_path(self):
 
471
        """Return the path to the index file."""
 
472
        return self._controltransport.local_abspath(INDEX_FILENAME)
 
473
 
 
474
    def open_index(self):
 
475
        """Open the index for this repository."""
 
476
        from dulwich.index import Index
 
477
        if not self.has_index():
 
478
            raise NoIndexPresent()
 
479
        return Index(self.index_path())
 
480
 
 
481
    def has_index(self):
 
482
        """Check if an index is present."""
 
483
        # Bare repos must never have index files; non-bare repos may have a
 
484
        # missing index file, which is treated as empty.
 
485
        return not self.bare
 
486
 
 
487
    def get_config(self):
 
488
        from dulwich.config import ConfigFile
 
489
        try:
 
490
            with self._controltransport.get('config') as f:
 
491
                return ConfigFile.from_file(f)
 
492
        except NoSuchFile:
 
493
            return ConfigFile()
 
494
 
 
495
    def get_config_stack(self):
 
496
        from dulwich.config import StackedConfig
 
497
        backends = []
 
498
        p = self.get_config()
 
499
        if p is not None:
 
500
            backends.append(p)
 
501
            writable = p
 
502
        else:
 
503
            writable = None
 
504
        backends.extend(StackedConfig.default_backends())
 
505
        return StackedConfig(backends, writable=writable)
 
506
 
 
507
    def __repr__(self):
 
508
        return "<%s for %r>" % (self.__class__.__name__, self.transport)
 
509
 
 
510
    @classmethod
 
511
    def init(cls, transport, bare=False):
 
512
        if not bare:
 
513
            try:
 
514
                transport.mkdir(".git")
 
515
            except FileExists:
 
516
                raise AlreadyControlDirError(transport.base)
 
517
            control_transport = transport.clone(".git")
 
518
        else:
 
519
            control_transport = transport
 
520
        for d in BASE_DIRECTORIES:
 
521
            try:
 
522
                control_transport.mkdir("/".join(d))
 
523
            except FileExists:
 
524
                pass
 
525
        try:
 
526
            control_transport.mkdir(OBJECTDIR)
 
527
        except FileExists:
 
528
            raise AlreadyControlDirError(transport.base)
 
529
        TransportObjectStore.init(control_transport.clone(OBJECTDIR))
 
530
        ret = cls(transport, bare)
 
531
        ret.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
 
532
        ret._init_files(bare)
 
533
        return ret
 
534
 
 
535
 
 
536
class TransportObjectStore(PackBasedObjectStore):
 
537
    """Git-style object store that exists on disk."""
 
538
 
 
539
    def __init__(self, transport):
 
540
        """Open an object store.
 
541
 
 
542
        :param transport: Transport to open data from
 
543
        """
 
544
        super(TransportObjectStore, self).__init__()
 
545
        self.transport = transport
 
546
        self.pack_transport = self.transport.clone(PACKDIR)
 
547
        self._alternates = None
 
548
 
 
549
    def __eq__(self, other):
 
550
        if not isinstance(other, TransportObjectStore):
 
551
            return False
 
552
        return self.transport == other.transport
 
553
 
 
554
    def __repr__(self):
 
555
        return "%s(%r)" % (self.__class__.__name__, self.transport)
 
556
 
 
557
    @property
 
558
    def alternates(self):
 
559
        if self._alternates is not None:
 
560
            return self._alternates
 
561
        self._alternates = []
 
562
        for path in self._read_alternate_paths():
 
563
            # FIXME: Check path
 
564
            t = _mod_transport.get_transport_from_path(path)
 
565
            self._alternates.append(self.__class__(t))
 
566
        return self._alternates
 
567
 
 
568
    def _read_alternate_paths(self):
 
569
        try:
 
570
            f = self.transport.get("info/alternates")
 
571
        except NoSuchFile:
 
572
            return []
 
573
        ret = []
 
574
        with f:
 
575
            for l in f.read().splitlines():
 
576
                if l[0] == b"#":
 
577
                    continue
 
578
                if os.path.isabs(l):
 
579
                    continue
 
580
                ret.append(l)
 
581
            return ret
 
582
 
 
583
    @property
 
584
    def packs(self):
 
585
        # FIXME: Never invalidates.
 
586
        if not self._pack_cache:
 
587
            self._update_pack_cache()
 
588
        return self._pack_cache.values()
 
589
 
 
590
    def _update_pack_cache(self):
 
591
        for pack in self._load_packs():
 
592
            self._pack_cache[pack._basename] = pack
 
593
 
 
594
    def _pack_names(self):
 
595
        try:
 
596
            return self.pack_transport.list_dir(".")
 
597
        except TransportNotPossible:
 
598
            try:
 
599
                f = self.transport.get('info/packs')
 
600
            except NoSuchFile:
 
601
                # Hmm, warn about running 'git update-server-info' ?
 
602
                return iter([])
 
603
            else:
 
604
                # TODO(jelmer): Move to top-level after dulwich
 
605
                # 0.19.7 is released.
 
606
                from dulwich.object_store import read_packs_file
 
607
                with f:
 
608
                    return read_packs_file(f)
 
609
        except NoSuchFile:
 
610
            return iter([])
 
611
 
 
612
    def _remove_pack(self, pack):
 
613
        self.pack_transport.delete(os.path.basename(pack.index.path))
 
614
        self.pack_transport.delete(pack.data.filename)
 
615
 
 
616
    def _load_packs(self):
 
617
        ret = []
 
618
        for name in self._pack_names():
 
619
            if name.startswith("pack-") and name.endswith(".pack"):
 
620
                try:
 
621
                    size = self.pack_transport.stat(name).st_size
 
622
                except TransportNotPossible:
 
623
                    f = self.pack_transport.get(name)
 
624
                    pd = PackData(name, f, size=len(contents))
 
625
                else:
 
626
                    pd = PackData(name, self.pack_transport.get(name),
 
627
                            size=size)
 
628
                idxname = name.replace(".pack", ".idx")
 
629
                idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
 
630
                pack = Pack.from_objects(pd, idx)
 
631
                pack._basename = idxname[:-4]
 
632
                ret.append(pack)
 
633
        return ret
 
634
 
 
635
    def _iter_loose_objects(self):
 
636
        for base in self.transport.list_dir('.'):
 
637
            if len(base) != 2:
 
638
                continue
 
639
            for rest in self.transport.list_dir(base):
 
640
                yield (base+rest).encode(sys.getfilesystemencoding())
 
641
 
 
642
    def _split_loose_object(self, sha):
 
643
        return (sha[:2], sha[2:])
 
644
 
 
645
    def _remove_loose_object(self, sha):
 
646
        path = osutils.joinpath(self._split_loose_object(sha))
 
647
        self.transport.delete(urlutils.quote_from_bytes(path))
 
648
 
 
649
    def _get_loose_object(self, sha):
 
650
        path = osutils.joinpath(self._split_loose_object(sha))
 
651
        try:
 
652
            with self.transport.get(urlutils.quote_from_bytes(path)) as f:
 
653
                return ShaFile.from_file(f)
 
654
        except NoSuchFile:
 
655
            return None
 
656
 
 
657
    def add_object(self, obj):
 
658
        """Add a single object to this object store.
 
659
 
 
660
        :param obj: Object to add
 
661
        """
 
662
        (dir, file) = self._split_loose_object(obj.id)
 
663
        try:
 
664
            self.transport.mkdir(urlutils.quote_from_bytes(dir))
 
665
        except FileExists:
 
666
            pass
 
667
        path = urlutils.quote_from_bytes(osutils.pathjoin(dir, file))
 
668
        if self.transport.has(path):
 
669
            return # Already there, no need to write again
 
670
        self.transport.put_bytes(path, obj.as_legacy_object())
 
671
 
 
672
    def move_in_pack(self, f):
 
673
        """Move a specific file containing a pack into the pack directory.
 
674
 
 
675
        :note: The file should be on the same file system as the
 
676
            packs directory.
 
677
 
 
678
        :param path: Path to the pack file.
 
679
        """
 
680
        f.seek(0)
 
681
        p = PackData("", f, len(f.getvalue()))
 
682
        entries = p.sorted_entries()
 
683
        basename = "pack-%s" % iter_sha1(entry[0] for entry in entries).decode('ascii')
 
684
        p._filename = basename + ".pack"
 
685
        f.seek(0)
 
686
        self.pack_transport.put_file(basename + ".pack", f)
 
687
        idxfile = self.pack_transport.open_write_stream(basename + ".idx")
 
688
        try:
 
689
            write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
 
690
        finally:
 
691
            idxfile.close()
 
692
        idxfile = self.pack_transport.get(basename + ".idx")
 
693
        idx = load_pack_index_file(basename+".idx", idxfile)
 
694
        final_pack = Pack.from_objects(p, idx)
 
695
        final_pack._basename = basename
 
696
        self._add_known_pack(basename, final_pack)
 
697
        return final_pack
 
698
 
 
699
    def move_in_thin_pack(self, f):
 
700
        """Move a specific file containing a pack into the pack directory.
 
701
 
 
702
        :note: The file should be on the same file system as the
 
703
            packs directory.
 
704
 
 
705
        :param path: Path to the pack file.
 
706
        """
 
707
        f.seek(0)
 
708
        p = Pack('', resolve_ext_ref=self.get_raw)
 
709
        p._data = PackData.from_file(f, len(f.getvalue()))
 
710
        p._data.pack = p
 
711
        p._idx_load = lambda: MemoryPackIndex(p.data.sorted_entries(), p.data.get_stored_checksum())
 
712
 
 
713
        pack_sha = p.index.objects_sha1()
 
714
 
 
715
        datafile = self.pack_transport.open_write_stream(
 
716
                "pack-%s.pack" % pack_sha.decode('ascii'))
 
717
        try:
 
718
            entries, data_sum = write_pack_objects(datafile, p.pack_tuples())
 
719
        finally:
 
720
            datafile.close()
 
721
        entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()])
 
722
        idxfile = self.pack_transport.open_write_stream(
 
723
            "pack-%s.idx" % pack_sha.decode('ascii'))
 
724
        try:
 
725
            write_pack_index_v2(idxfile, entries, data_sum)
 
726
        finally:
 
727
            idxfile.close()
 
728
        # TODO(jelmer): Just add new pack to the cache
 
729
        self._flush_pack_cache()
 
730
 
 
731
    def add_pack(self):
 
732
        """Add a new pack to this object store.
 
733
 
 
734
        :return: Fileobject to write to and a commit function to
 
735
            call when the pack is finished.
 
736
        """
 
737
        f = BytesIO()
 
738
        def commit():
 
739
            if len(f.getvalue()) > 0:
 
740
                return self.move_in_pack(f)
 
741
            else:
 
742
                return None
 
743
        def abort():
 
744
            return None
 
745
        return f, commit, abort
 
746
 
 
747
    @classmethod
 
748
    def init(cls, transport):
 
749
        try:
 
750
            transport.mkdir('info')
 
751
        except FileExists:
 
752
            pass
 
753
        try:
 
754
            transport.mkdir(PACKDIR)
 
755
        except FileExists:
 
756
            pass
 
757
        return cls(transport)