12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001 |
- # repo.py -- For dealing with git repositories.
- # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
- # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; version 2
- # of the License or (at your option) any later version of
- # the License.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- # MA 02110-1301, USA.
- """Repository access.
- This module contains the base class for git repositories
- (BaseRepo) and an implementation which uses a repository on
- local disk (Repo).
- """
- from io import BytesIO
- import errno
- import os
- import sys
- from dulwich.errors import (
- NoIndexPresent,
- NotBlobError,
- NotCommitError,
- NotGitRepository,
- NotTreeError,
- NotTagError,
- CommitError,
- RefFormatError,
- HookError,
- )
- from dulwich.file import (
- GitFile,
- )
- from dulwich.object_store import (
- DiskObjectStore,
- MemoryObjectStore,
- ObjectStoreGraphWalker,
- )
- from dulwich.objects import (
- check_hexsha,
- Blob,
- Commit,
- ShaFile,
- Tag,
- Tree,
- )
- from dulwich.hooks import (
- PreCommitShellHook,
- PostCommitShellHook,
- CommitMsgShellHook,
- )
- from dulwich.refs import (
- check_ref_format,
- RefsContainer,
- DictRefsContainer,
- InfoRefsContainer,
- DiskRefsContainer,
- read_packed_refs,
- read_packed_refs_with_peeled,
- write_packed_refs,
- SYMREF,
- )
- import warnings
- OBJECTDIR = b'objects'
- REFSDIR = b'refs'
- REFSDIR_TAGS = b'tags'
- REFSDIR_HEADS = b'heads'
- INDEX_FILENAME = b'index'
- BASE_DIRECTORIES = [
- [b'branches'],
- [REFSDIR],
- [REFSDIR, REFSDIR_TAGS],
- [REFSDIR, REFSDIR_HEADS],
- [b'hooks'],
- [b'info']
- ]
- def parse_graftpoints(graftpoints):
- """Convert a list of graftpoints into a dict
- :param graftpoints: Iterator of graftpoint lines
- Each line is formatted as:
- <commit sha1> <parent sha1> [<parent sha1>]*
- Resulting dictionary is:
- <commit sha1>: [<parent sha1>*]
- https://git.wiki.kernel.org/index.php/GraftPoint
- """
- grafts = {}
- for l in graftpoints:
- raw_graft = l.split(None, 1)
- commit = raw_graft[0]
- if len(raw_graft) == 2:
- parents = raw_graft[1].split()
- else:
- parents = []
- for sha in [commit] + parents:
- check_hexsha(sha, 'Invalid graftpoint')
- grafts[commit] = parents
- return grafts
- def serialize_graftpoints(graftpoints):
- """Convert a dictionary of grafts into string
- The graft dictionary is:
- <commit sha1>: [<parent sha1>*]
- Each line is formatted as:
- <commit sha1> <parent sha1> [<parent sha1>]*
- https://git.wiki.kernel.org/index.php/GraftPoint
- """
- graft_lines = []
- for commit, parents in graftpoints.items():
- if parents:
- graft_lines.append(commit + b' ' + b' '.join(parents))
- else:
- graft_lines.append(commit)
- return b'\n'.join(graft_lines)
- class BaseRepo(object):
- """Base class for a git repository.
- :ivar object_store: Dictionary-like object for accessing
- the objects
- :ivar refs: Dictionary-like object with the refs in this
- repository
- """
- def __init__(self, object_store, refs):
- """Open a repository.
- This shouldn't be called directly, but rather through one of the
- base classes, such as MemoryRepo or Repo.
- :param object_store: Object store to use
- :param refs: Refs container to use
- """
- self.object_store = object_store
- self.refs = refs
- self._graftpoints = {}
- self.hooks = {}
- def _init_files(self, bare):
- """Initialize a default set of named files."""
- from dulwich.config import ConfigFile
- self._put_named_file(b'description', b"Unnamed repository")
- f = BytesIO()
- cf = ConfigFile()
- cf.set(b"core", b"repositoryformatversion", b"0")
- cf.set(b"core", b"filemode", b"true")
- cf.set(b"core", b"bare", bare)
- cf.set(b"core", b"logallrefupdates", True)
- cf.write_to_file(f)
- self._put_named_file(b'config', f.getvalue())
- self._put_named_file(os.path.join(b'info', b'exclude'), b'')
- def get_named_file(self, path):
- """Get a file from the control dir with a specific name.
- Although the filename should be interpreted as a filename relative to
- the control dir in a disk-based Repo, the object returned need not be
- pointing to a file in that location.
- :param path: The path to the file, relative to the control dir.
- :return: An open file object, or None if the file does not exist.
- """
- raise NotImplementedError(self.get_named_file)
- def _put_named_file(self, path, contents):
- """Write a file to the control dir with the given name and contents.
- :param path: The path to the file, relative to the control dir.
- :param contents: A string to write to the file.
- """
- raise NotImplementedError(self._put_named_file)
- def open_index(self):
- """Open the index for this repository.
- :raise NoIndexPresent: If no index is present
- :return: The matching `Index`
- """
- raise NotImplementedError(self.open_index)
- def fetch(self, target, determine_wants=None, progress=None):
- """Fetch objects into another repository.
- :param target: The target repository
- :param determine_wants: Optional function to determine what refs to
- fetch.
- :param progress: Optional progress function
- :return: The local refs
- """
- if determine_wants is None:
- determine_wants = target.object_store.determine_wants_all
- target.object_store.add_objects(
- self.fetch_objects(determine_wants, target.get_graph_walker(),
- progress))
- return self.get_refs()
- def fetch_objects(self, determine_wants, graph_walker, progress,
- get_tagged=None):
- """Fetch the missing objects required for a set of revisions.
- :param determine_wants: Function that takes a dictionary with heads
- and returns the list of heads to fetch.
- :param graph_walker: Object that can iterate over the list of revisions
- to fetch and has an "ack" method that will be called to acknowledge
- that a revision is present.
- :param progress: Simple progress function that will be called with
- updated progress strings.
- :param get_tagged: Function that returns a dict of pointed-to sha -> tag
- sha for including tags.
- :return: iterator over objects, with __len__ implemented
- """
- wants = determine_wants(self.get_refs())
- if not isinstance(wants, list):
- raise TypeError("determine_wants() did not return a list")
- shallows = getattr(graph_walker, 'shallow', frozenset())
- unshallows = getattr(graph_walker, 'unshallow', frozenset())
- if wants == []:
- # TODO(dborowitz): find a way to short-circuit that doesn't change
- # this interface.
- if shallows or unshallows:
- # Do not send a pack in shallow short-circuit path
- return None
- return []
- haves = self.object_store.find_common_revisions(graph_walker)
- # Deal with shallow requests separately because the haves do
- # not reflect what objects are missing
- if shallows or unshallows:
- haves = [] # TODO: filter the haves commits from iter_shas.
- # the specific commits aren't missing.
- def get_parents(commit):
- if commit.id in shallows:
- return []
- return self.get_parents(commit.id, commit)
- return self.object_store.iter_shas(
- self.object_store.find_missing_objects(
- haves, wants, progress,
- get_tagged,
- get_parents=get_parents))
- def get_graph_walker(self, heads=None):
- """Retrieve a graph walker.
- A graph walker is used by a remote repository (or proxy)
- to find out which objects are present in this repository.
- :param heads: Repository heads to use (optional)
- :return: A graph walker object
- """
- if heads is None:
- heads = self.refs.as_dict(b'refs/heads').values()
- return ObjectStoreGraphWalker(heads, self.get_parents)
- def get_refs(self):
- """Get dictionary with all refs.
- :return: A ``dict`` mapping ref names to SHA1s
- """
- return self.refs.as_dict()
- def head(self):
- """Return the SHA1 pointed at by HEAD."""
- return self.refs[b'HEAD']
- def _get_object(self, sha, cls):
- assert len(sha) in (20, 40)
- ret = self.get_object(sha)
- if not isinstance(ret, cls):
- if cls is Commit:
- raise NotCommitError(ret)
- elif cls is Blob:
- raise NotBlobError(ret)
- elif cls is Tree:
- raise NotTreeError(ret)
- elif cls is Tag:
- raise NotTagError(ret)
- else:
- raise Exception("Type invalid: %r != %r" % (
- ret.type_name, cls.type_name))
- return ret
- def get_object(self, sha):
- """Retrieve the object with the specified SHA.
- :param sha: SHA to retrieve
- :return: A ShaFile object
- :raise KeyError: when the object can not be found
- """
- return self.object_store[sha]
- def get_parents(self, sha, commit=None):
- """Retrieve the parents of a specific commit.
- If the specific commit is a graftpoint, the graft parents
- will be returned instead.
- :param sha: SHA of the commit for which to retrieve the parents
- :param commit: Optional commit matching the sha
- :return: List of parents
- """
- try:
- return self._graftpoints[sha]
- except KeyError:
- if commit is None:
- commit = self[sha]
- return commit.parents
- def get_config(self):
- """Retrieve the config object.
- :return: `ConfigFile` object for the ``.git/config`` file.
- """
- raise NotImplementedError(self.get_config)
- def get_description(self):
- """Retrieve the description for this repository.
- :return: String with the description of the repository
- as set by the user.
- """
- raise NotImplementedError(self.get_description)
- def set_description(self, description):
- """Set the description for this repository.
- :param description: Text to set as description for this repository.
- """
- raise NotImplementedError(self.set_description)
- def get_config_stack(self):
- """Return a config stack for this repository.
- This stack accesses the configuration for both this repository
- itself (.git/config) and the global configuration, which usually
- lives in ~/.gitconfig.
- :return: `Config` instance for this repository
- """
- from dulwich.config import StackedConfig
- backends = [self.get_config()] + StackedConfig.default_backends()
- return StackedConfig(backends, writable=backends[0])
- def get_peeled(self, ref):
- """Get the peeled value of a ref.
- :param ref: The refname to peel.
- :return: The fully-peeled SHA1 of a tag object, after peeling all
- intermediate tags; if the original ref does not point to a tag, this
- will equal the original SHA1.
- """
- cached = self.refs.get_peeled(ref)
- if cached is not None:
- return cached
- return self.object_store.peel_sha(self.refs[ref]).id
- def get_walker(self, include=None, *args, **kwargs):
- """Obtain a walker for this repository.
- :param include: Iterable of SHAs of commits to include along with their
- ancestors. Defaults to [HEAD]
- :param exclude: Iterable of SHAs of commits to exclude along with their
- ancestors, overriding includes.
- :param order: ORDER_* constant specifying the order of results. Anything
- other than ORDER_DATE may result in O(n) memory usage.
- :param reverse: If True, reverse the order of output, requiring O(n)
- memory.
- :param max_entries: The maximum number of entries to yield, or None for
- no limit.
- :param paths: Iterable of file or subtree paths to show entries for.
- :param rename_detector: diff.RenameDetector object for detecting
- renames.
- :param follow: If True, follow path across renames/copies. Forces a
- default rename_detector.
- :param since: Timestamp to list commits after.
- :param until: Timestamp to list commits before.
- :param queue_cls: A class to use for a queue of commits, supporting the
- iterator protocol. The constructor takes a single argument, the
- Walker.
- :return: A `Walker` object
- """
- from dulwich.walk import Walker
- if include is None:
- include = [self.head()]
- if isinstance(include, str):
- include = [include]
- kwargs['get_parents'] = lambda commit: self.get_parents(commit.id, commit)
- return Walker(self.object_store, include, *args, **kwargs)
- def __getitem__(self, name):
- """Retrieve a Git object by SHA1 or ref.
- :param name: A Git object SHA1 or a ref name
- :return: A `ShaFile` object, such as a Commit or Blob
- :raise KeyError: when the specified ref or object does not exist
- """
- if not isinstance(name, bytes):
- raise TypeError("'name' must be bytestring, not %.80s" %
- type(name).__name__)
- if len(name) in (20, 40):
- try:
- return self.object_store[name]
- except (KeyError, ValueError):
- pass
- try:
- return self.object_store[self.refs[name]]
- except RefFormatError:
- raise KeyError(name)
- def __contains__(self, name):
- """Check if a specific Git object or ref is present.
- :param name: Git object SHA1 or ref name
- """
- if len(name) in (20, 40):
- return name in self.object_store or name in self.refs
- else:
- return name in self.refs
- def __setitem__(self, name, value):
- """Set a ref.
- :param name: ref name
- :param value: Ref value - either a ShaFile object, or a hex sha
- """
- if name.startswith(b"refs/") or name == b'HEAD':
- if isinstance(value, ShaFile):
- self.refs[name] = value.id
- elif isinstance(value, bytes):
- self.refs[name] = value
- else:
- raise TypeError(value)
- else:
- raise ValueError(name)
- def __delitem__(self, name):
- """Remove a ref.
- :param name: Name of the ref to remove
- """
- if name.startswith(b"refs/") or name == b"HEAD":
- del self.refs[name]
- else:
- raise ValueError(name)
- def _get_user_identity(self):
- """Determine the identity to use for new commits.
- """
- config = self.get_config_stack()
- return (config.get((b"user", ), b"name") + b" <" +
- config.get((b"user", ), b"email") + b">")
- def _add_graftpoints(self, updated_graftpoints):
- """Add or modify graftpoints
- :param updated_graftpoints: Dict of commit shas to list of parent shas
- """
- # Simple validation
- for commit, parents in updated_graftpoints.items():
- for sha in [commit] + parents:
- check_hexsha(sha, 'Invalid graftpoint')
- self._graftpoints.update(updated_graftpoints)
- def _remove_graftpoints(self, to_remove=[]):
- """Remove graftpoints
- :param to_remove: List of commit shas
- """
- for sha in to_remove:
- del self._graftpoints[sha]
- def do_commit(self, message=None, committer=None,
- author=None, commit_timestamp=None,
- commit_timezone=None, author_timestamp=None,
- author_timezone=None, tree=None, encoding=None,
- ref=b'HEAD', merge_heads=None):
- """Create a new commit.
- :param message: Commit message
- :param committer: Committer fullname
- :param author: Author fullname (defaults to committer)
- :param commit_timestamp: Commit timestamp (defaults to now)
- :param commit_timezone: Commit timestamp timezone (defaults to GMT)
- :param author_timestamp: Author timestamp (defaults to commit timestamp)
- :param author_timezone: Author timestamp timezone
- (defaults to commit timestamp timezone)
- :param tree: SHA1 of the tree root to use (if not specified the
- current index will be committed).
- :param encoding: Encoding
- :param ref: Optional ref to commit to (defaults to current branch)
- :param merge_heads: Merge heads (defaults to .git/MERGE_HEADS)
- :return: New commit SHA1
- """
- import time
- c = Commit()
- if tree is None:
- index = self.open_index()
- c.tree = index.commit(self.object_store)
- else:
- if len(tree) != 40:
- raise ValueError("tree must be a 40-byte hex sha string")
- c.tree = tree
- try:
- self.hooks['pre-commit'].execute()
- except HookError as e:
- raise CommitError(e)
- except KeyError: # no hook defined, silent fallthrough
- pass
- if merge_heads is None:
- # FIXME: Read merge heads from .git/MERGE_HEADS
- merge_heads = []
- if committer is None:
- # FIXME: Support GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL environment
- # variables
- committer = self._get_user_identity()
- c.committer = committer
- if commit_timestamp is None:
- # FIXME: Support GIT_COMMITTER_DATE environment variable
- commit_timestamp = time.time()
- c.commit_time = int(commit_timestamp)
- if commit_timezone is None:
- # FIXME: Use current user timezone rather than UTC
- commit_timezone = 0
- c.commit_timezone = commit_timezone
- if author is None:
- # FIXME: Support GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL environment
- # variables
- author = committer
- c.author = author
- if author_timestamp is None:
- # FIXME: Support GIT_AUTHOR_DATE environment variable
- author_timestamp = commit_timestamp
- c.author_time = int(author_timestamp)
- if author_timezone is None:
- author_timezone = commit_timezone
- c.author_timezone = author_timezone
- if encoding is not None:
- c.encoding = encoding
- if message is None:
- # FIXME: Try to read commit message from .git/MERGE_MSG
- raise ValueError("No commit message specified")
- try:
- c.message = self.hooks['commit-msg'].execute(message)
- if c.message is None:
- c.message = message
- except HookError as e:
- raise CommitError(e)
- except KeyError: # no hook defined, message not modified
- c.message = message
- if ref is None:
- # Create a dangling commit
- c.parents = merge_heads
- self.object_store.add_object(c)
- else:
- try:
- old_head = self.refs[ref]
- c.parents = [old_head] + merge_heads
- self.object_store.add_object(c)
- ok = self.refs.set_if_equals(ref, old_head, c.id)
- except KeyError:
- c.parents = merge_heads
- self.object_store.add_object(c)
- ok = self.refs.add_if_new(ref, c.id)
- if not ok:
- # Fail if the atomic compare-and-swap failed, leaving the commit and
- # all its objects as garbage.
- raise CommitError("%s changed during commit" % (ref,))
- try:
- self.hooks['post-commit'].execute()
- except HookError as e: # silent failure
- warnings.warn("post-commit hook failed: %s" % e, UserWarning)
- except KeyError: # no hook defined, silent fallthrough
- pass
- return c.id
- path_sep_bytes = os.path.sep.encode(sys.getfilesystemencoding())
- class Repo(BaseRepo):
- """A git repository backed by local disk.
- To open an existing repository, call the contructor with
- the path of the repository.
- To create a new repository, use the Repo.init class method.
- """
- def __init__(self, path):
- self.path = path
- if not isinstance(path, bytes):
- self._path_bytes = path.encode(sys.getfilesystemencoding())
- else:
- self._path_bytes = path
- if os.path.isdir(os.path.join(self._path_bytes, b'.git', OBJECTDIR)):
- self.bare = False
- self._controldir = os.path.join(self._path_bytes, b'.git')
- elif (os.path.isdir(os.path.join(self._path_bytes, OBJECTDIR)) and
- os.path.isdir(os.path.join(self._path_bytes, REFSDIR))):
- self.bare = True
- self._controldir = self._path_bytes
- elif (os.path.isfile(os.path.join(self._path_bytes, b'.git'))):
- import re
- with open(os.path.join(self._path_bytes, b'.git'), 'rb') as f:
- _, gitdir = re.match(b'(gitdir: )(.+$)', f.read()).groups()
- self.bare = False
- self._controldir = os.path.join(self._path_bytes, gitdir)
- else:
- raise NotGitRepository(
- "No git repository was found at %(path)s" % dict(path=path)
- )
- object_store = DiskObjectStore(os.path.join(self.controldir(),
- OBJECTDIR))
- refs = DiskRefsContainer(self.controldir())
- BaseRepo.__init__(self, object_store, refs)
- self._graftpoints = {}
- graft_file = self.get_named_file(os.path.join(b'info', b'grafts'))
- if graft_file:
- with graft_file:
- self._graftpoints.update(parse_graftpoints(graft_file))
- graft_file = self.get_named_file(b'shallow')
- if graft_file:
- with graft_file:
- self._graftpoints.update(parse_graftpoints(graft_file))
- self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
- self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
- self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
- def controldir(self):
- """Return the path of the control directory."""
- return self._controldir
- def _put_named_file(self, path, contents):
- """Write a file to the control dir with the given name and contents.
- :param path: The path to the file, relative to the control dir.
- :param contents: A string to write to the file.
- """
- path = path.lstrip(path_sep_bytes)
- with GitFile(os.path.join(self.controldir(), path), 'wb') as f:
- f.write(contents)
- def get_named_file(self, path):
- """Get a file from the control dir with a specific name.
- Although the filename should be interpreted as a filename relative to
- the control dir in a disk-based Repo, the object returned need not be
- pointing to a file in that location.
- :param path: The path to the file, relative to the control dir.
- :return: An open file object, or None if the file does not exist.
- """
- # TODO(dborowitz): sanitize filenames, since this is used directly by
- # the dumb web serving code.
- path = path.lstrip(path_sep_bytes)
- try:
- return open(os.path.join(self.controldir(), path), 'rb')
- except (IOError, OSError) as e:
- if e.errno == errno.ENOENT:
- return None
- raise
- def index_path(self):
- """Return path to the index file."""
- return os.path.join(self.controldir(), INDEX_FILENAME)
- def open_index(self):
- """Open the index for this repository.
- :raise NoIndexPresent: If no index is present
- :return: The matching `Index`
- """
- from dulwich.index import Index
- if not self.has_index():
- raise NoIndexPresent()
- return Index(self.index_path())
- def has_index(self):
- """Check if an index is present."""
- # Bare repos must never have index files; non-bare repos may have a
- # missing index file, which is treated as empty.
- return not self.bare
- def stage(self, paths, fsencoding=sys.getfilesystemencoding()):
- """Stage a set of paths.
- :param paths: List of paths, relative to the repository path
- """
- if not isinstance(paths, list):
- paths = [paths]
- from dulwich.index import (
- blob_from_path_and_stat,
- index_entry_from_stat,
- )
- index = self.open_index()
- for path in paths:
- if not isinstance(path, bytes):
- disk_path_bytes = path.encode(sys.getfilesystemencoding())
- repo_path_bytes = path.encode(fsencoding)
- else:
- disk_path_bytes, repo_path_bytes = path, path
- full_path = os.path.join(self._path_bytes, disk_path_bytes)
- try:
- st = os.lstat(full_path)
- except OSError:
- # File no longer exists
- try:
- del index[repo_path_bytes]
- except KeyError:
- pass # already removed
- else:
- blob = blob_from_path_and_stat(full_path, st)
- self.object_store.add_object(blob)
- index[repo_path_bytes] = index_entry_from_stat(st, blob.id, 0)
- index.write()
- def clone(self, target_path, mkdir=True, bare=False,
- origin=b"origin"):
- """Clone this repository.
- :param target_path: Target path
- :param mkdir: Create the target directory
- :param bare: Whether to create a bare repository
- :param origin: Base name for refs in target repository
- cloned from this repository
- :return: Created repository as `Repo`
- """
- if not bare:
- target = self.init(target_path, mkdir=mkdir)
- else:
- target = self.init_bare(target_path)
- self.fetch(target)
- target.refs.import_refs(
- b'refs/remotes/' + origin, self.refs.as_dict(b'refs/heads'))
- target.refs.import_refs(
- b'refs/tags', self.refs.as_dict(b'refs/tags'))
- try:
- target.refs.add_if_new(
- b'refs/heads/master',
- self.refs[b'refs/heads/master'])
- except KeyError:
- pass
- # Update target head
- head, head_sha = self.refs._follow(b'HEAD')
- if head is not None and head_sha is not None:
- target.refs.set_symbolic_ref(b'HEAD', head)
- target[b'HEAD'] = head_sha
- if not bare:
- # Checkout HEAD to target dir
- target.reset_index()
- return target
- def reset_index(self, tree=None):
- """Reset the index back to a specific tree.
- :param tree: Tree SHA to reset to, None for current HEAD tree.
- """
- from dulwich.index import (
- build_index_from_tree,
- validate_path_element_default,
- validate_path_element_ntfs,
- )
- if tree is None:
- tree = self[b'HEAD'].tree
- config = self.get_config()
- honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt")
- if config.get_boolean('core', 'core.protectNTFS', os.name == "nt"):
- validate_path_element = validate_path_element_ntfs
- else:
- validate_path_element = validate_path_element_default
- return build_index_from_tree(self._path_bytes, self.index_path(),
- self.object_store, tree, honor_filemode=honor_filemode,
- validate_path_element=validate_path_element)
- def get_config(self):
- """Retrieve the config object.
- :return: `ConfigFile` object for the ``.git/config`` file.
- """
- from dulwich.config import ConfigFile
- path = os.path.join(self._controldir, b'config')
- try:
- return ConfigFile.from_path(path)
- except (IOError, OSError) as e:
- if e.errno != errno.ENOENT:
- raise
- ret = ConfigFile()
- ret.path = path
- return ret
- def get_description(self):
- """Retrieve the description of this repository.
- :return: A string describing the repository or None.
- """
- path = os.path.join(self._controldir, b'description')
- try:
- with GitFile(path, 'rb') as f:
- return f.read()
- except (IOError, OSError) as e:
- if e.errno != errno.ENOENT:
- raise
- return None
- def __repr__(self):
- return "<Repo at %r>" % self.path
- def set_description(self, description):
- """Set the description for this repository.
- :param description: Text to set as description for this repository.
- """
- self._put_named_file(b'description', description)
- @classmethod
- def _init_maybe_bare(cls, path, bare):
- if not isinstance(path, bytes):
- path_bytes = path.encode(sys.getfilesystemencoding())
- else:
- path_bytes = path
- for d in BASE_DIRECTORIES:
- os.mkdir(os.path.join(path_bytes, *d))
- DiskObjectStore.init(os.path.join(path_bytes, OBJECTDIR))
- ret = cls(path)
- ret.refs.set_symbolic_ref(b'HEAD', b"refs/heads/master")
- ret._init_files(bare)
- return ret
- @classmethod
- def init(cls, path, mkdir=False):
- """Create a new repository.
- :param path: Path in which to create the repository
- :param mkdir: Whether to create the directory
- :return: `Repo` instance
- """
- if not isinstance(path, bytes):
- path_bytes = path.encode(sys.getfilesystemencoding())
- else:
- path_bytes = path
- if mkdir:
- os.mkdir(path_bytes)
- controldir = os.path.join(path_bytes, b'.git')
- os.mkdir(controldir)
- cls._init_maybe_bare(controldir, False)
- return cls(path)
- @classmethod
- def init_bare(cls, path):
- """Create a new bare repository.
- ``path`` should already exist and be an emty directory.
- :param path: Path to create bare repository in
- :return: a `Repo` instance
- """
- return cls._init_maybe_bare(path, True)
- create = init_bare
- def close(self):
- self.object_store.close()
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.close()
- class MemoryRepo(BaseRepo):
- """Repo that stores refs, objects, and named files in memory.
- MemoryRepos are always bare: they have no working tree and no index, since
- those have a stronger dependency on the filesystem.
- """
- def __init__(self):
- from dulwich.config import ConfigFile
- BaseRepo.__init__(self, MemoryObjectStore(), DictRefsContainer({}))
- self._named_files = {}
- self.bare = True
- self._config = ConfigFile()
- def _put_named_file(self, path, contents):
- """Write a file to the control dir with the given name and contents.
- :param path: The path to the file, relative to the control dir.
- :param contents: A string to write to the file.
- """
- self._named_files[path] = contents
- def get_named_file(self, path):
- """Get a file from the control dir with a specific name.
- Although the filename should be interpreted as a filename relative to
- the control dir in a disk-baked Repo, the object returned need not be
- pointing to a file in that location.
- :param path: The path to the file, relative to the control dir.
- :return: An open file object, or None if the file does not exist.
- """
- contents = self._named_files.get(path, None)
- if contents is None:
- return None
- return BytesIO(contents)
- def open_index(self):
- """Fail to open index for this repo, since it is bare.
- :raise NoIndexPresent: Raised when no index is present
- """
- raise NoIndexPresent()
- def get_config(self):
- """Retrieve the config object.
- :return: `ConfigFile` object.
- """
- return self._config
- def get_description(self):
- """Retrieve the repository description.
- This defaults to None, for no description.
- """
- return None
- @classmethod
- def init_bare(cls, objects, refs):
- """Create a new bare repository in memory.
- :param objects: Objects for the new repository,
- as iterable
- :param refs: Refs as dictionary, mapping names
- to object SHA1s
- """
- ret = cls()
- for obj in objects:
- ret.object_store.add_object(obj)
- for refname, sha in refs.items():
- ret.refs[refname] = sha
- ret._init_files(bare=True)
- return ret
|