summaryrefslogtreecommitdiff
path: root/git/db/py/base.py
blob: 127d828a86a746d5d55e2fa3825807526cfba18a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors
#
# This module is part of GitDB and is released under
# the New BSD License: http://www.opensource.org/licenses/bsd-license.php
"""Contains basic implementations for the interface building blocks"""
from git.db.interface import *

from git.util import (
    pool,
    join,
    isfile,
    normpath,
    abspath,
    dirname,
    LazyMixin,
    hex_to_bin,
    bin_to_hex,
    expandvars,
    expanduser,
    exists,
    is_git_dir,
)

from git.index import IndexFile
from git.config import GitConfigParser
from git.exc import (
    BadObject,
    AmbiguousObjectName,
    InvalidGitRepositoryError,
    NoSuchPathError
)

from async import ChannelThreadTask

from itertools import chain
import sys
import os


__all__ = ('PureObjectDBR', 'PureObjectDBW', 'PureRootPathDB', 'PureCompoundDB',
           'PureConfigurationMixin', 'PureRepositoryPathsMixin', 'PureAlternatesFileMixin',
           'PureIndexDB')


class PureObjectDBR(ObjectDBR):

    #{ Query Interface

    def has_object_async(self, reader):
        task = ChannelThreadTask(reader, str(self.has_object_async), lambda sha: (sha, self.has_object(sha)))
        return pool.add_task(task)

    def info_async(self, reader):
        task = ChannelThreadTask(reader, str(self.info_async), self.info)
        return pool.add_task(task)

    def stream_async(self, reader):
        # base implementation just uses the stream method repeatedly
        task = ChannelThreadTask(reader, str(self.stream_async), self.stream)
        return pool.add_task(task)

    def partial_to_complete_sha_hex(self, partial_hexsha):
        len_partial_hexsha = len(partial_hexsha)
        if len_partial_hexsha % 2 != 0:
            partial_binsha = hex_to_bin(partial_hexsha + "0")
        else:
            partial_binsha = hex_to_bin(partial_hexsha)
        # END assure successful binary conversion
        return self.partial_to_complete_sha(partial_binsha, len(partial_hexsha))

    #} END query interface


class PureObjectDBW(ObjectDBW):

    def __init__(self, *args, **kwargs):
        try:
            super(PureObjectDBW, self).__init__(*args, **kwargs)
        except TypeError:
            pass
        # END handle py 2.6
        self._ostream = None

    #{ Edit Interface
    def set_ostream(self, stream):
        cstream = self._ostream
        self._ostream = stream
        return cstream

    def ostream(self):
        return self._ostream

    def store_async(self, reader):
        task = ChannelThreadTask(reader, str(self.store_async), self.store)
        return pool.add_task(task)

    #} END edit interface


class PureRootPathDB(RootPathDB):

    def __init__(self, root_path):
        self._root_path = root_path
        super(PureRootPathDB, self).__init__(root_path)

    #{ Interface
    def root_path(self):
        return self._root_path

    def db_path(self, rela_path=None):
        if not rela_path:
            return self._root_path
        return join(self._root_path, rela_path)
    #} END interface


def _databases_recursive(database, output):
    """Fill output list with database from db, in order. Deals with Loose, Packed 
    and compound databases."""
    if isinstance(database, CompoundDB):
        compounds = list()
        dbs = database.databases()
        output.extend(db for db in dbs if not isinstance(db, CompoundDB))
        for cdb in (db for db in dbs if isinstance(db, CompoundDB)):
            _databases_recursive(cdb, output)
    else:
        output.append(database)
    # END handle database type


class PureCompoundDB(CompoundDB, PureObjectDBR, LazyMixin, CachingDB):

    def _set_cache_(self, attr):
        if attr == '_dbs':
            self._dbs = list()
        else:
            super(PureCompoundDB, self)._set_cache_(attr)

    #{ PureObjectDBR interface

    def has_object(self, sha):
        for db in self._dbs:
            if db.has_object(sha):
                return True
        # END for each db
        return False

    def info(self, sha):
        for db in self._dbs:
            try:
                return db.info(sha)
            except BadObject:
                pass
        # END for each db

    def stream(self, sha):
        for db in self._dbs:
            try:
                return db.stream(sha)
            except BadObject:
                pass
        # END for each db

    def size(self):
        return reduce(lambda x, y: x + y, (db.size() for db in self._dbs), 0)

    def sha_iter(self):
        return chain(*(db.sha_iter() for db in self._dbs))

    #} END object DBR Interface

    #{ Interface

    def databases(self):
        return tuple(self._dbs)

    def update_cache(self, force=False):
        # something might have changed, clear everything
        stat = False
        for db in self._dbs:
            if isinstance(db, CachingDB):
                stat |= db.update_cache(force)
            # END if is caching db
        # END for each database to update
        return stat

    def partial_to_complete_sha_hex(self, partial_hexsha):
        len_partial_hexsha = len(partial_hexsha)
        if len_partial_hexsha % 2 != 0:
            partial_binsha = hex_to_bin(partial_hexsha + "0")
        else:
            partial_binsha = hex_to_bin(partial_hexsha)
        # END assure successful binary conversion

        candidate = None
        for db in self._dbs:
            full_bin_sha = None
            try:
                if hasattr(db, 'partial_to_complete_sha_hex'):
                    full_bin_sha = db.partial_to_complete_sha_hex(partial_hexsha)
                else:
                    full_bin_sha = db.partial_to_complete_sha(partial_binsha, len_partial_hexsha)
                # END handle database type
            except BadObject:
                continue
            # END ignore bad objects
            if full_bin_sha:
                if candidate and candidate != full_bin_sha:
                    raise AmbiguousObjectName(partial_hexsha)
                candidate = full_bin_sha
            # END handle candidate
        # END for each db
        if not candidate:
            raise BadObject(partial_binsha)
        return candidate

    def partial_to_complete_sha(self, partial_binsha, hex_len):
        """Simple adaptor to feed into our implementation"""
        return self.partial_to_complete_sha_hex(bin_to_hex(partial_binsha)[:hex_len])
    #} END interface


class PureRepositoryPathsMixin(RepositoryPathsMixin):
    # slots has no effect here, its just to keep track of used attrs
    __slots__ = ("_git_path", '_bare', '_working_tree_dir')

    #{ Configuration
    repo_dir = '.git'
    objs_dir = 'objects'
    #} END configuration

    #{ Subclass Interface
    def _initialize(self, path):
        epath = abspath(expandvars(expanduser(path or os.getcwd())))

        if not exists(epath):
            raise NoSuchPathError(epath)
        # END check file

        self._working_tree_dir = None
        self._git_path = None
        curpath = epath

        # walk up the path to find the .git dir
        while curpath:
            if is_git_dir(curpath):
                self._git_path = curpath
                self._working_tree_dir = os.path.dirname(curpath)
                break
            gitpath = join(curpath, self.repo_dir)
            if is_git_dir(gitpath):
                self._git_path = gitpath
                self._working_tree_dir = curpath
                break
            curpath, dummy = os.path.split(curpath)
            if not dummy:
                break
        # END while curpath

        if self._git_path is None:
            raise InvalidGitRepositoryError(epath)
        # END path not found

        self._bare = self._working_tree_dir is None
        if hasattr(self, 'config_reader'):
            try:
                self._bare = self.config_reader("repository").getboolean('core', 'bare')
            except Exception:
                # lets not assume the option exists, although it should
                pass
            # END handle exception
        # END check bare flag
        self._working_tree_dir = self._bare and None or self._working_tree_dir

    #} end subclass interface

    #{ Object Interface

    def __eq__(self, rhs):
        if hasattr(rhs, 'git_dir'):
            return self.git_dir == rhs.git_dir
        return False

    def __ne__(self, rhs):
        return not self.__eq__(rhs)

    def __hash__(self):
        return hash(self.git_dir)

    def __repr__(self):
        return "%s(%r)" % (type(self).__name__, self.git_dir)

    #} END object interface

    #{ Interface

    @property
    def is_bare(self):
        return self._bare

    @property
    def git_dir(self):
        return self._git_path

    @property
    def working_tree_dir(self):
        if self._working_tree_dir is None:
            raise AssertionError("Repository at %s is bare and does not have a working tree directory" % self.git_dir)
        # END assertion
        return dirname(self.git_dir)

    @property
    def objects_dir(self):
        return join(self.git_dir, self.objs_dir)

    @property
    def working_dir(self):
        if self.is_bare:
            return self.git_dir
        else:
            return self.working_tree_dir
        # END handle bare state

    def _mk_description():
        def _get_description(self):
            filename = join(self.git_dir, 'description')
            return file(filename).read().rstrip()

        def _set_description(self, descr):
            filename = join(self.git_dir, 'description')
            file(filename, 'w').write(descr + '\n')

        return property(_get_description, _set_description, "Descriptive text for the content of the repository")

    description = _mk_description()
    del(_mk_description)

    #} END interface


class PureConfigurationMixin(ConfigurationMixin):

    #{ Configuration
    system_config_file_name = "gitconfig"
    repo_config_file_name = "config"
    #} END

    def __new__(cls, *args, **kwargs):
        """This is just a stupid workaround for the evil py2.6 change which makes mixins quite impossible"""
        return super(PureConfigurationMixin, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        """Verify prereqs"""
        try:
            super(PureConfigurationMixin, self).__init__(*args, **kwargs)
        except TypeError:
            pass
        # END handle code-breaking change in python 2.6
        assert hasattr(self, 'git_dir')

    def _path_at_level(self, level):
        # we do not support an absolute path of the gitconfig on windows ,
        # use the global config instead
        if sys.platform == "win32" and level == "system":
            level = "global"
        # END handle windows

        if level == "system":
            return "/etc/%s" % self.system_config_file_name
        elif level == "global":
            return normpath(expanduser("~/.%s" % self.system_config_file_name))
        elif level == "repository":
            return join(self.git_dir, self.repo_config_file_name)
        # END handle level

        raise ValueError("Invalid configuration level: %r" % level)

    #{ Interface

    def config_reader(self, config_level=None):
        files = None
        if config_level is None:
            files = [self._path_at_level(f) for f in self.config_level]
        else:
            files = [self._path_at_level(config_level)]
        # END handle level
        return GitConfigParser(files, read_only=True)

    def config_writer(self, config_level="repository"):
        return GitConfigParser(self._path_at_level(config_level), read_only=False)

    #} END interface


class PureIndexDB(IndexDB):
    #{ Configuration
    IndexCls = IndexFile
    #} END configuration

    @property
    def index(self):
        return self.IndexCls(self)


class PureAlternatesFileMixin(object):

    """Utility able to read and write an alternates file through the alternates property
    It needs to be part of a type with the git_dir or db_path property.

    The file by default is assumed to be located at the default location as imposed
    by the standard git repository layout"""

    #{ Configuration
    alternates_filepath = os.path.join('info', 'alternates')    # relative path to alternates file

    #} END configuration

    def __init__(self, *args, **kwargs):
        try:
            super(PureAlternatesFileMixin, self).__init__(*args, **kwargs)
        except TypeError:
            pass
        # END handle py2.6 code breaking changes
        self._alternates_path()  # throws on incompatible type

    #{ Interface

    def _alternates_path(self):
        if hasattr(self, 'git_dir'):
            return join(self.git_dir, 'objects', self.alternates_filepath)
        elif hasattr(self, 'db_path'):
            return self.db_path(self.alternates_filepath)
        else:
            raise AssertionError("This mixin requires a parent type with either the git_dir property or db_path method")
        # END handle path

    def _get_alternates(self):
        """The list of alternates for this repo from which objects can be retrieved

        :return: list of strings being pathnames of alternates"""
        alternates_path = self._alternates_path()

        if os.path.exists(alternates_path):
            try:
                f = open(alternates_path)
                alts = f.read()
            finally:
                f.close()
            return alts.strip().splitlines()
        else:
            return list()
        # END handle path exists

    def _set_alternates(self, alts):
        """Sets the alternates

        :parm alts:
            is the array of string paths representing the alternates at which 
            git should look for objects, i.e. /home/user/repo/.git/objects

        :raise NoSuchPathError:
        :note:
            The method does not check for the existance of the paths in alts
            as the caller is responsible."""
        alternates_path = self._alternates_path()
        if not alts:
            if isfile(alternates_path):
                os.remove(alternates_path)
        else:
            try:
                f = open(alternates_path, 'w')
                f.write("\n".join(alts))
            finally:
                f.close()
            # END file handling
        # END alts handling

    alternates = property(_get_alternates, _set_alternates,
                          doc="Retrieve a list of alternates paths or set a list paths to be used as alternates")

    #} END interface