summaryrefslogtreecommitdiff
path: root/lib/git
diff options
context:
space:
mode:
authorSebastian Thiel <byronimo@gmail.com>2009-10-26 23:24:56 +0100
committerSebastian Thiel <byronimo@gmail.com>2009-10-26 23:24:56 +0100
commit2792e534dd55fe03bca302f87a3ea638a7278bf1 (patch)
tree28d8f1cc81d8d121a9976204ee10be22996f6e2d /lib/git
parent1b89f39432cdb395f5fbb9553b56595d29e2b773 (diff)
parent0ef1f89abe5b2334705ee8f1a6da231b0b6c9a50 (diff)
downloadgitpython-2792e534dd55fe03bca302f87a3ea638a7278bf1.tar.gz
Merge branch 'index' into improvements
* index: index.add: Finished implemenation including through tests When parsing trees, we now store the originan type bits as well, previously we dropped it cmd.wait: AutoKill wrapped process will automatically raise on errors to unify error handling amongst clients using the process directly. It might be needed to add a flag allowing to easily override that added head kwarg to reset and commit method, allowing to automatically change the head to the given commit, which makes the methods more versatile refs.SymoblicRef: implemented direcft setting of the symbolic references commit, which possibly dereferences to the respective head index.commit: implemented initial version, but in fact some more changes are required to have a nice API. Tests are not yet fully done either actor: added __eq__, __ne__ and __hash__ methods including simple test index.remove implemented including throrough test Implemented index.reset method including test IndexEntry is now based on a 'minimal' version that is suitable to be fed into UpdateIndex. The Inode and device information is only needed to quickly compare the index against the working tree for changes, hence it should not be that dominant in the API either. More changes to come Added notes about git-update-ref Refs can now set the reference they are pointing to in a controlled fashion by writing their ref file directly Added TagRefernce creation and deletion including tests Implemented head methods: create, delete, rename, including tests refs: added create, delete and rename methods where appropriate. Tests are marked, implementation is needed for most of them Added frame for IndexFile add/remove/commit methods and respective test markers Added repo.index property including simple test, and additional ideas in the TODO list Renamed Index to IndexFile, adjusted tests, it will only operate on physical files, not on streams, as Indices are not streamed by any git command ( at least not in raw format )
Diffstat (limited to 'lib/git')
-rw-r--r--lib/git/actor.py9
-rw-r--r--lib/git/cmd.py26
-rw-r--r--lib/git/diff.py3
-rw-r--r--lib/git/index.py694
-rw-r--r--lib/git/objects/base.py1
-rw-r--r--lib/git/objects/commit.py40
-rw-r--r--lib/git/objects/tree.py17
-rw-r--r--lib/git/refs.py216
-rw-r--r--lib/git/repo.py9
9 files changed, 813 insertions, 202 deletions
diff --git a/lib/git/actor.py b/lib/git/actor.py
index fe4a47e5..04872f1c 100644
--- a/lib/git/actor.py
+++ b/lib/git/actor.py
@@ -18,6 +18,15 @@ class Actor(object):
self.name = name
self.email = email
+ def __eq__(self, other):
+ return self.name == other.name and self.email == other.email
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __hash__(self):
+ return hash((self.name, self.email))
+
def __str__(self):
return self.name
diff --git a/lib/git/cmd.py b/lib/git/cmd.py
index d674224c..4b4b84af 100644
--- a/lib/git/cmd.py
+++ b/lib/git/cmd.py
@@ -42,12 +42,16 @@ class Git(object):
"""
Kill/Interrupt the stored process instance once this instance goes out of scope. It is
used to prevent processes piling up in case iterators stop reading.
- Besides all attributes are wired through to the contained process object
+ Besides all attributes are wired through to the contained process object.
+
+ The wait method was overridden to perform automatic status code checking
+ and possibly raise.
"""
- __slots__= "proc"
+ __slots__= ("proc", "args")
- def __init__(self, proc ):
+ def __init__(self, proc, args ):
self.proc = proc
+ self.args = args
def __del__(self):
# did the process finish already so we have a return code ?
@@ -64,6 +68,20 @@ class Git(object):
def __getattr__(self, attr):
return getattr(self.proc, attr)
+
+ def wait(self):
+ """
+ Wait for the process and return its status code.
+
+ Raise
+ GitCommandError if the return status is not 0
+ """
+ status = self.proc.wait()
+ if status != 0:
+ raise GitCommandError(self.args, status, self.proc.stderr.read())
+ # END status handling
+ return status
+
def __init__(self, git_dir=None):
@@ -184,7 +202,7 @@ class Git(object):
**extra
)
if as_process:
- return self.AutoInterrupt(proc)
+ return self.AutoInterrupt(proc, command)
# Wait for the process to return
status = 0
diff --git a/lib/git/diff.py b/lib/git/diff.py
index 03e6709c..b0e0898a 100644
--- a/lib/git/diff.py
+++ b/lib/git/diff.py
@@ -100,9 +100,6 @@ class Diffable(object):
index = diff_method(self.repo, proc.stdout)
status = proc.wait()
- if status != 0:
- raise GitCommandError(("git diff",)+tuple(args), status, proc.stderr.read())
-
return index
diff --git a/lib/git/index.py b/lib/git/index.py
index 4217c9a2..cc3f3a4e 100644
--- a/lib/git/index.py
+++ b/lib/git/index.py
@@ -13,10 +13,12 @@ import mmap
import objects
import tempfile
import os
+import sys
import stat
+import subprocess
import git.diff as diff
-from git.objects import Blob, Tree, Object
+from git.objects import Blob, Tree, Object, Commit
from git.utils import SHA1Writer, LazyMixin, ConcurrentWriteOperation
@@ -34,10 +36,66 @@ class _TemporaryFileSwap(object):
def __del__(self):
if os.path.isfile(self.tmp_file_path):
+ if sys.platform == "win32" and os.path.exists(self.file_path):
+ os.remove(self.file_path)
os.rename(self.tmp_file_path, self.file_path)
+ # END temp file exists
-class IndexEntry(tuple):
+class BaseIndexEntry(tuple):
+ """
+ Small Brother of an index entry which can be created to describe changes
+ done to the index in which case plenty of additional information is not requried.
+
+ As the first 4 data members match exactly to the IndexEntry type, methods
+ expecting a BaseIndexEntry can also handle full IndexEntries even if they
+ use numeric indices for performance reasons.
+ """
+
+ def __str__(self):
+ return "%o %s %i\t%s\n" % (self.mode, self.sha, self.stage, self.path)
+
+ @property
+ def mode(self):
+ """
+ File Mode, compatible to stat module constants
+ """
+ return self[0]
+
+ @property
+ def sha(self):
+ """
+ hex sha of the blob
+ """
+ return self[1]
+
+ @property
+ def stage(self):
+ """
+ Stage of the entry, either:
+ 0 = default stage
+ 1 = stage before a merge or common ancestor entry in case of a 3 way merge
+ 2 = stage of entries from the 'left' side of the merge
+ 3 = stage of entries from the right side of the merge
+ Note:
+ For more information, see http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
+ """
+ return self[2]
+
+ @property
+ def path(self):
+ return self[3]
+
+ @classmethod
+ def from_blob(cls, blob, stage = 0):
+ """
+ Returns
+ Fully equipped BaseIndexEntry at the given stage
+ """
+ return cls((blob.mode, blob.id, stage, blob.path))
+
+
+class IndexEntry(BaseIndexEntry):
"""
Allows convenient access to IndexEntry data without completely unpacking it.
@@ -53,49 +111,42 @@ class IndexEntry(tuple):
Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
file's creation time
"""
- return struct.unpack(">LL", self[0])
+ return struct.unpack(">LL", self[4])
@property
def mtime(self):
"""
See ctime property, but returns modification time
"""
- return struct.unpack(">LL", self[1])
+ return struct.unpack(">LL", self[5])
@property
def dev(self):
"""
Device ID
"""
- return self[2]
+ return self[6]
@property
def inode(self):
"""
Inode ID
"""
- return self[3]
-
- @property
- def mode(self):
- """
- File Mode, compatible to stat module constants
- """
- return self[4]
-
+ return self[7]
+
@property
def uid(self):
"""
User ID
"""
- return self[5]
+ return self[8]
@property
def gid(self):
"""
Group ID
"""
- return self[6]
+ return self[9]
@property
def size(self):
@@ -105,33 +156,8 @@ class IndexEntry(tuple):
Note
Will be 0 if the stage is not 0 ( hence it is an unmerged entry )
"""
- return self[7]
-
- @property
- def sha(self):
- """
- hex sha of the blob
- """
- return self[8]
-
- @property
- def stage(self):
- """
- Stage of the entry, either:
- 0 = default stage
- 1 = stage before a merge or common ancestor entry in case of a 3 way merge
- 2 = stage of entries from the 'left' side of the merge
- 3 = stage of entries from the right side of the merge
- Note:
- For more information, see http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
- """
- return self[9]
-
- @property
- def path(self):
return self[10]
-
@classmethod
def from_blob(cls, blob):
"""
@@ -139,10 +165,46 @@ class IndexEntry(tuple):
Minimal entry resembling the given blob objecft
"""
time = struct.pack(">LL", 0, 0)
- return IndexEntry((time, time, 0, 0, blob.mode, 0, 0, blob.size, blob.id, 0, blob.path))
+ return IndexEntry((blob.mode, blob.id, 0, blob.path, time, time, 0, 0, 0, 0, blob.size))
-class Index(LazyMixin, diff.Diffable):
+def clear_cache(func):
+ """
+ Decorator for functions that alter the index using the git command. This would
+ invalidate our possibly existing entries dictionary which is why it must be
+ deleted to allow it to be lazily reread later.
+
+ Note
+ This decorator will not be required once all functions are implemented
+ natively which in fact is possible, but probably not feasible performance wise.
+ """
+ def clear_cache_if_not_raised(self, *args, **kwargs):
+ rval = func(self, *args, **kwargs)
+ del(self.entries)
+ return rval
+
+ # END wrapper method
+ clear_cache_if_not_raised.__name__ = func.__name__
+ return clear_cache_if_not_raised
+
+
+def default_index(func):
+ """
+ Decorator assuring the wrapped method may only run if we are the default
+ repository index. This is as we rely on git commands that operate
+ on that index only.
+ """
+ def check_default_index(self, *args, **kwargs):
+ if self._file_path != self._index_path():
+ raise AssertionError( "Cannot call %r on indices that do not represent the default git index" % func.__name__ )
+ return func(self, *args, **kwargs)
+ # END wrpaper method
+
+ check_default_index.__name__ = func.__name__
+ return check_default_index
+
+
+class IndexFile(LazyMixin, diff.Diffable):
"""
Implements an Index that can be manipulated using a native implementation in
order to save git command function calls wherever possible.
@@ -155,14 +217,18 @@ class Index(LazyMixin, diff.Diffable):
``Entries``
The index contains an entries dict whose keys are tuples of type IndexEntry
to facilitate access.
+
+ You may only read the entries dict or manipulate it through designated methods.
+ Otherwise changes to it will be lost when changing the index using its methods.
"""
- __slots__ = ( "repo", "version", "entries", "_extension_data", "_is_default_index" )
+ __slots__ = ( "repo", "version", "entries", "_extension_data", "_file_path" )
_VERSION = 2 # latest version we support
S_IFGITLINK = 0160000
- def __init__(self, repo, stream = None):
+ def __init__(self, repo, file_path=None):
"""
- Initialize this Index instance, optionally from the given ``stream``
+ Initialize this Index instance, optionally from the given ``file_path``.
+ If no file_path is given, we will be created from the current index file.
If a stream is not given, the stream will be initialized from the current
repository's index on demand.
@@ -170,24 +236,27 @@ class Index(LazyMixin, diff.Diffable):
self.repo = repo
self.version = self._VERSION
self._extension_data = ''
- self._is_default_index = True
- if stream is not None:
- self._is_default_index = False
- self._read_from_stream(stream)
- # END read from stream immediatly
-
+ self._file_path = file_path or self._index_path()
def _set_cache_(self, attr):
if attr == "entries":
# read the current index
- fp = open(self._index_path(), "r")
+ # try memory map for speed
+ fp = open(self._file_path, "r")
+ stream = fp
+ try:
+ stream = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
+ except Exception:
+ pass
+ # END memory mapping
+
try:
- self._read_from_stream(fp)
+ self._read_from_stream(stream)
finally:
fp.close()
# END read from default index on demand
else:
- super(Index, self)._set_cache_(attr)
+ super(IndexFile, self)._set_cache_(attr)
def _index_path(self):
return os.path.join(self.repo.path, "index")
@@ -197,12 +266,9 @@ class Index(LazyMixin, diff.Diffable):
def path(self):
"""
Returns
- Path to the index file we are representing or None if we are
- a loose index that was read from a stream.
+ Path to the index file we are representing
"""
- if self._is_default_index:
- return self._index_path()
- return None
+ return self._file_path
@classmethod
def _read_entry(cls, stream):
@@ -217,8 +283,7 @@ class Index(LazyMixin, diff.Diffable):
real_size = ((stream.tell() - beginoffset + 8) & ~7)
data = stream.read((beginoffset + real_size) - stream.tell())
- return IndexEntry((ctime, mtime, dev, ino, mode, uid, gid, size,
- binascii.hexlify(sha), flags >> 12, path))
+ return IndexEntry((mode, binascii.hexlify(sha), flags >> 12, path, ctime, mtime, dev, ino, uid, gid, size))
@classmethod
def _read_header(cls, stream):
@@ -252,88 +317,35 @@ class Index(LazyMixin, diff.Diffable):
# truncate the sha in the end as we will dynamically create it anyway
self._extension_data = self._extension_data[:-20]
-
- @classmethod
- def from_file(cls, repo, file_path):
- """
- Returns
- Index instance as recreated from the given stream.
-
- ``repo``
- Repository the index is related to
-
- ``file_pa ``
- File path pointing to git index file
-
- Note
- Reading is based on the dulwich project.
- """
- fp = open(file_path, "r")
-
- # try memory map for speed
- stream = fp
- try:
- stream = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
- except Exception:
- pass
- # END memory mapping
-
- try:
- return cls(repo, stream)
- finally:
- fp.close()
-
@classmethod
- def to_file(cls, index, file_path):
- """
- Write the index data to the given file path.
-
- ``index``
- Index you wish to write.
-
- ``file_path``
- Path at which to write the index data. Please note that missing directories
- will lead to an exception to be thrown.
-
- Raise
- IOError if the file could not be written
- """
- fp = open(file_path, "w")
- try:
- return index.write(fp)
- finally:
- fp.close()
- # END exception handling
-
-
- @classmethod
def _write_cache_entry(cls, stream, entry):
"""
Write an IndexEntry to a stream
"""
beginoffset = stream.tell()
- stream.write(entry[0]) # ctime
- stream.write(entry[1]) # mtime
- path = entry[10]
+ stream.write(entry[4]) # ctime
+ stream.write(entry[5]) # mtime
+ path = entry[3]
plen = len(path) & 0x0fff # path length
- assert plen == len(path), "Path %s too long to fit into index" % entry[10]
- flags = plen | (entry[9] << 12)# stage and path length are 2 byte flags
- stream.write(struct.pack(">LLLLLL20sH", entry[2], entry[3], entry[4],
- entry[5], entry[6], entry[7], binascii.unhexlify(entry[8]), flags))
+ assert plen == len(path), "Path %s too long to fit into index" % entry[3]
+ flags = plen | (entry[2] << 12)# stage and path length are 2 byte flags
+ stream.write(struct.pack(">LLLLLL20sH", entry[6], entry[7], entry[0],
+ entry[8], entry[9], entry[10], binascii.unhexlify(entry[1]), flags))
stream.write(path)
real_size = ((stream.tell() - beginoffset + 8) & ~7)
stream.write("\0" * ((beginoffset + real_size) - stream.tell()))
- def write(self, stream=None):
+ def write(self, file_path = None):
"""
- Write the current state to the given stream or to the default repository
- index.
+ Write the current state to our file path or to the given one
- ``stream``
- File-like object or None.
- If None, the default repository index will be overwritten.
+ ``file_path``
+ If None, we will write to our stored file path from which we have
+ been initialized. Otherwise we write to the given file path.
+ Please note that this will change the file_path of this index to
+ the one you gave.
Returns
self
@@ -341,12 +353,8 @@ class Index(LazyMixin, diff.Diffable):
Note
Index writing based on the dulwich implementation
"""
- write_op = None
- if stream is None:
- write_op = ConcurrentWriteOperation(self._index_path())
- stream = write_op._begin_writing()
- # stream = open(self._index_path()
- # END stream handling
+ write_op = ConcurrentWriteOperation(file_path or self._file_path)
+ stream = write_op._begin_writing()
stream = SHA1Writer(stream)
@@ -356,7 +364,7 @@ class Index(LazyMixin, diff.Diffable):
# body
entries_sorted = self.entries.values()
- entries_sorted.sort(key=lambda e: (e[10], e[9])) # use path/stage as sort key
+ entries_sorted.sort(key=lambda e: (e[3], e[2])) # use path/stage as sort key
for entry in entries_sorted:
self._write_cache_entry(stream, entry)
# END for each entry
@@ -366,10 +374,11 @@ class Index(LazyMixin, diff.Diffable):
# write the sha over the content
stream.write_sha()
+ write_op._end_writing()
- if write_op is not None:
- write_op._end_writing()
-
+ # make sure we represent what we have written
+ if file_path is not None:
+ self._file_path = file_path
@classmethod
def from_tree(cls, repo, *treeish, **kwargs):
@@ -394,6 +403,11 @@ class Index(LazyMixin, diff.Diffable):
``**kwargs``
Additional arguments passed to git-read-tree
+ Returns
+ New IndexFile instance. It will point to a temporary index location which
+ does not exist anymore. If you intend to write such a merged Index, supply
+ an alternate file_path to its 'write' method.
+
Note:
In the three-way merge case, --aggressive will be specified to automatically
resolve more cases in a commonly correct manner. Specify trivial=True as kwarg
@@ -428,7 +442,8 @@ class Index(LazyMixin, diff.Diffable):
index_handler = _TemporaryFileSwap(os.path.join(repo.path, 'index'))
try:
repo.git.read_tree(*arg_list, **kwargs)
- index = cls.from_file(repo, tmp_index)
+ index = cls(repo, tmp_index)
+ index.entries # force it to read the file
finally:
if os.path.exists(tmp_index):
os.remove(tmp_index)
@@ -455,6 +470,12 @@ class Index(LazyMixin, diff.Diffable):
ret |= (index_mode & 0111)
return ret
+ @classmethod
+ def _tree_mode_to_index_mode(cls, tree_mode):
+ """
+ Convert a tree mode to index mode as good as possible
+ """
+
def iter_blobs(self, predicate = lambda t: True):
"""
Returns
@@ -507,6 +528,10 @@ class Index(LazyMixin, diff.Diffable):
Returns:
self
+
+ Note
+ You will have to write the index manually once you are done, i.e.
+ index.resolve_blobs(blobs).write()
"""
for blob in iter_blobs:
stage_null_key = (blob.path, 0)
@@ -527,6 +552,22 @@ class Index(LazyMixin, diff.Diffable):
# END for each blob
return self
+
+ def update(self):
+ """
+ Reread the contents of our index file, discarding all cached information
+ we might have.
+
+ Note:
+ This is a possibly dangerious operations as it will discard your changes
+ to index.entries
+
+ Returns
+ self
+ """
+ del(self.entries)
+ # allows to lazily reread on demand
+ return self
def write_tree(self):
"""
@@ -539,20 +580,11 @@ class Index(LazyMixin, diff.Diffable):
index_path = self._index_path()
tmp_index_mover = _TemporaryFileSwap(index_path)
- self.to_file(self, index_path)
+ self.write(index_path)
+ tree_sha = self.repo.git.write_tree()
- try:
- tree_sha = self.repo.git.write_tree()
- finally:
- # remove our index file so that the original index can move back into place
- # On linux it will silently overwrite, on windows it won't
- if os.path.isfile(index_path):
- os.remove(index_path)
- # END remove our own index file beforehand
- # END write tree handling
return Tree(self.repo, tree_sha, 0, '')
-
def _process_diff_args(self, args):
try:
args.pop(args.index(self))
@@ -561,6 +593,361 @@ class Index(LazyMixin, diff.Diffable):
# END remove self
return args
+
+ def _to_relative_path(self, path):
+ """
+ Return
+ Version of path relative to our git directory or raise ValueError
+ if it is not within our git direcotory
+ """
+ if not os.path.isabs(path):
+ return path
+ relative_path = path.replace(self.repo.git.git_dir+"/", "")
+ if relative_path == path:
+ raise ValueError("Absolute path %r is not in git repository at %r" % (path,self.repo.git.git_dir))
+ return relative_path
+
+ def _preprocess_add_items(self, items):
+ """
+ Split the items into two lists of path strings and BaseEntries.
+ """
+ paths = list()
+ entries = list()
+
+ for item in items:
+ if isinstance(item, basestring):
+ paths.append(self._to_relative_path(item))
+ elif isinstance(item, Blob):
+ entries.append(BaseIndexEntry.from_blob(item))
+ elif isinstance(item, BaseIndexEntry):
+ entries.append(item)
+ else:
+ raise TypeError("Invalid Type: %r" % item)
+ # END for each item
+ return (paths, entries)
+
+
+ @clear_cache
+ @default_index
+ def add(self, items, force=True, **kwargs):
+ """
+ Add files from the working tree, specific blobs or BaseIndexEntries
+ to the index. The underlying index file will be written immediately, hence
+ you should provide as many items as possible to minimize the amounts of writes
+
+ ``items``
+ Multiple types of items are supported, types can be mixed within one call.
+ Different types imply a different handling. File paths may generally be
+ relative or absolute.
+
+ - path string
+ strings denote a relative or absolute path into the repository pointing to
+ an existing file, i.e. CHANGES, lib/myfile.ext, '/home/gitrepo/lib/myfile.ext'.
+
+ Paths provided like this must exist. When added, they will be written
+ into the object database.
+
+ PathStrings may contain globs, such as 'lib/__init__*' or can be directories
+ like 'lib', the latter ones will add all the files within the dirctory and
+ subdirectories.
+
+ This equals a straight git-add.
+
+ They are added at stage 0
+
+ - Blob object
+ Blobs are added as they are assuming a valid mode is set.
+ The file they refer to may or may not exist in the file system, but
+ must be a path relative to our repository.
+
+ If their sha is null ( 40*0 ), their path must exist in the file system
+ as an object will be created from the data at the path.The handling
+ now very much equals the way string paths are processed, except that
+ the mode you have set will be kept. This allows you to create symlinks
+ by settings the mode respectively and writing the target of the symlink
+ directly into the file. This equals a default Linux-Symlink which
+ is not dereferenced automatically, except that it can be created on
+ filesystems not supporting it as well.
+
+ Please note that globs or directories are not allowed in Blob objects.
+
+ They are added at stage 0
+
+ - BaseIndexEntry or type
+ Handling equals the one of Blob objects, but the stage may be
+ explicitly set.
+
+ ``force``
+ If True, otherwise ignored or excluded files will be
+ added anyway.
+ As opposed to the git-add command, we enable this flag by default
+ as the API user usually wants the item to be added even though
+ they might be excluded.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-update-index, such
+ as index_only.
+
+ Returns
+ List(BaseIndexEntries) representing the entries just actually added.
+ """
+ # sort the entries into strings and Entries, Blobs are converted to entries
+ # automatically
+ # paths can be git-added, for everything else we use git-update-index
+ entries_added = list()
+ paths, entries = self._preprocess_add_items(items)
+
+ if paths:
+ git_add_output = self.repo.git.add(paths, v=True)
+ # force rereading our entries
+ del(self.entries)
+ for line in git_add_output.splitlines():
+ # line contains:
+ # add '<path>'
+ added_file = line[5:-1]
+ entries_added.append(self.entries[(added_file,0)])
+ # END for each line
+ # END path handling
+
+ if entries:
+ null_mode_entries = [ e for e in entries if e.mode == 0 ]
+ if null_mode_entries:
+ raise ValueError("At least one Entry has a null-mode - please use index.remove to remove files for clarity")
+ # END null mode should be remove
+
+ # create objects if required, otherwise go with the existing shas
+ null_entries_indices = [ i for i,e in enumerate(entries) if e.sha == Object.NULL_HEX_SHA ]
+ if null_entries_indices:
+ hash_proc = self.repo.git.hash_object(w=True, stdin_paths=True, istream=subprocess.PIPE, as_process=True)
+ hash_proc.stdin.write('\n'.join(entries[i].path for i in null_entries_indices))
+ obj_ids = self._flush_stdin_and_wait(hash_proc).splitlines()
+ assert len(obj_ids) == len(null_entries_indices), "git-hash-object did not produce all requested objects: want %i, got %i" % ( len(null_entries_indices), len(obj_ids) )
+
+ # update IndexEntries with new object id
+ for i,new_sha in zip(null_entries_indices, obj_ids):
+ e = entries[i]
+ new_entry = BaseIndexEntry((e.mode, new_sha, e.stage, e.path))
+ entries[i] = new_entry
+ # END for each index
+ # END null_entry handling
+
+ # feed all the data to stdin
+ update_index_proc = self.repo.git.update_index(index_info=True, istream=subprocess.PIPE, as_process=True, **kwargs)
+ update_index_proc.stdin.write('\n'.join(str(e) for e in entries))
+ entries_added.extend(entries)
+ self._flush_stdin_and_wait(update_index_proc)
+ # END if there are base entries
+
+ return entries_added
+
+ @clear_cache
+ @default_index
+ def remove(self, items, working_tree=False, **kwargs):
+ """
+ Remove the given items from the index and optionally from
+ the working tree as well.
+
+ ``items``
+ Multiple types of items are supported which may be be freely mixed.
+
+ - path string
+ Remove the given path at all stages. If it is a directory, you must
+ specify the r=True keyword argument to remove all file entries
+ below it. If absolute paths are given, they will be converted
+ to a path relative to the git repository directory containing
+ the working tree
+
+ The path string may include globs, such as *.c.
+
+ - Blob object
+ Only the path portion is used in this case.
+
+ - BaseIndexEntry or compatible type
+ The only relevant information here Yis the path. The stage is ignored.
+
+ ``working_tree``
+ If True, the entry will also be removed from the working tree, physically
+ removing the respective file. This may fail if there are uncommited changes
+ in it.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-rm, such
+ as 'r' to allow recurive removal of
+
+ Returns
+ List(path_string, ...) list of paths that have been removed effectively.
+ This is interesting to know in case you have provided a directory or
+ globs. Paths are relative to the
+ """
+ args = list()
+ if not working_tree:
+ args.append("--cached")
+ args.append("--")
+
+ # preprocess paths
+ paths = list()
+ for item in items:
+ if isinstance(item, (BaseIndexEntry,Blob)):
+ paths.append(self._to_relative_path(item.path))
+ elif isinstance(item, basestring):
+ paths.append(self._to_relative_path(item))
+ else:
+ raise TypeError("Invalid item type: %r" % item)
+ # END for each item
+
+ removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
+
+ # process output to gain proper paths
+ # rm 'path'
+ return [ p[4:-1] for p in removed_paths ]
+
+ @default_index
+ def commit(self, message, parent_commits=None, head=True):
+ """
+ Commit the current index, creating a commit object.
+
+ ``message``
+ Commit message. It may be an empty string if no message is provided.
+ It will be converted to a string in any case.
+
+ ``parent_commits``
+ Optional Commit objects to use as parents for the new commit.
+ If empty list, the commit will have no parents at all and become
+ a root commit.
+ If None , the current head commit will be the parent of the
+ new commit object
+
+ ``head``
+ If True, the HEAD will be advanced to the new commit automatically.
+ Else the HEAD will remain pointing on the previous commit. This could
+ lead to undesired results when diffing files.
+
+ Returns
+ Commit object representing the new commit
+
+ Note:
+ Additional information about hte committer and Author are taken from the
+ environment or from the git configuration, see git-commit-tree for
+ more information
+ """
+ parents = parent_commits
+ if parent_commits is None:
+ parent_commits = [ self.repo.head.commit ]
+
+ parent_args = [ ("-p", str(commit)) for commit in parent_commits ]
+
+ # create message stream
+ tmp_file_path = tempfile.mktemp()
+ fp = open(tmp_file_path,"w")
+ fp.write(str(message))
+ fp.close()
+ fp = open(tmp_file_path,"r")
+ fp.seek(0)
+
+ try:
+ # write the current index as tree
+ tree_sha = self.repo.git.write_tree()
+ commit_sha = self.repo.git.commit_tree(tree_sha, parent_args, istream=fp)
+ new_commit = Commit(self.repo, commit_sha)
+
+ if head:
+ self.repo.head.commit = new_commit
+ # END advance head handling
+
+ return new_commit
+ finally:
+ fp.close()
+ os.remove(tmp_file_path)
+
+ @classmethod
+ def _flush_stdin_and_wait(cls, proc):
+ proc.stdin.flush()
+ proc.stdin.close()
+ stdout = proc.stdout.read()
+ proc.wait()
+ return stdout
+
+ @default_index
+ def checkout(self, paths=None, force=False, **kwargs):
+ """
+ Checkout the given paths or all files from the version in the index.
+
+ ``paths``
+ If None, all paths in the index will be checked out. Otherwise an iterable
+ or single path of relative or absolute paths pointing to files is expected.
+ The command will ignore paths that do not exist.
+
+ ``force``
+ If True, existing files will be overwritten. If False, these will
+ be skipped.
+
+ ``**kwargs``
+ Additional arguments to be pasesd to git-checkout-index
+
+ Returns
+ self
+ """
+ args = ["--index"]
+ if force:
+ args.append("--force")
+
+ if paths is None:
+ args.append("--all")
+ self.repo.git.checkout_index(*args, **kwargs)
+ else:
+ if not isinstance(paths, (tuple,list)):
+ paths = [paths]
+
+ args.append("--stdin")
+ paths = [self._to_relative_path(p) for p in paths]
+ co_proc = self.repo.git.checkout_index(args, as_process=True, istream=subprocess.PIPE, **kwargs)
+ co_proc.stdin.write('\n'.join(paths))
+ self._flush_stdin_and_wait(co_proc)
+ # END paths handling
+ return self
+
+ @clear_cache
+ @default_index
+ def reset(self, commit='HEAD', working_tree=False, paths=None, head=False, **kwargs):
+ """
+ Reset the index to reflect the tree at the given commit. This will not
+ adjust our HEAD reference as opposed to HEAD.reset by default.
+
+ ``commit``
+ Revision, Reference or Commit specifying the commit we should represent.
+ If you want to specify a tree only, use IndexFile.from_tree and overwrite
+ the default index.
+
+ ``working_tree``
+ If True, the files in the working tree will reflect the changed index.
+ If False, the working tree will not be touched
+ Please note that changes to the working copy will be discarded without
+ warning !
+
+ ``head``
+ If True, the head will be set to the given commit. This is False by default,
+ but if True, this method behaves like HEAD.reset.
+
+ ``**kwargs``
+ Additional keyword arguments passed to git-reset
+
+ Returns
+ self
+ """
+ cur_head = self.repo.head
+ prev_commit = cur_head.commit
+
+ # reset to get the tree/working copy
+ cur_head.reset(commit, index=True, working_tree=working_tree, paths=paths, **kwargs)
+
+ # put the head back, possibly
+ if not head:
+ cur_head.reset(prev_commit, index=False, working_tree=False)
+ # END reset head
+
+ return self
+
+ @default_index
def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs):
"""
Diff this index against the working copy or a Tree or Commit object
@@ -572,9 +959,6 @@ class Index(LazyMixin, diff.Diffable):
Will only work with indices that represent the default git index as
they have not been initialized with a stream.
"""
- if not self._is_default_index:
- raise AssertionError( "Cannot diff custom indices as they do not represent the default git index" )
-
# index against index is always empty
if other is self.Index:
return diff.DiffIndex()
@@ -598,5 +982,5 @@ class Index(LazyMixin, diff.Diffable):
raise ValueError( "other must be None, Diffable.Index, a Tree or Commit, was %r" % other )
# diff against working copy - can be handled by superclass natively
- return super(Index, self).diff(other, paths, create_patch, **kwargs)
+ return super(IndexFile, self).diff(other, paths, create_patch, **kwargs)
diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py
index 0dfd1a23..0bece6f1 100644
--- a/lib/git/objects/base.py
+++ b/lib/git/objects/base.py
@@ -20,6 +20,7 @@ class Object(LazyMixin):
inst.size # objects uncompressed data size
inst.data # byte string containing the whole data of the object
"""
+ NULL_HEX_SHA = '0'*40
TYPES = ("blob", "tree", "commit", "tag")
__slots__ = ("repo", "id", "size", "data" )
type = None # to be set by subclass
diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py
index 0f8ed7f8..d9f87116 100644
--- a/lib/git/objects/commit.py
+++ b/lib/git/objects/commit.py
@@ -83,7 +83,7 @@ class Commit(base.Object, Iterable, diff.Diffable):
# prepare our data lines to match rev-list
data_lines = self.data.splitlines()
data_lines.insert(0, "commit %s" % self.id)
- temp = self._iter_from_process_or_stream(self.repo, iter(data_lines)).next()
+ temp = self._iter_from_process_or_stream(self.repo, iter(data_lines), False).next()
self.parents = temp.parents
self.tree = temp.tree
self.author = temp.author
@@ -111,7 +111,8 @@ class Commit(base.Object, Iterable, diff.Diffable):
to commits actually containing the paths
``kwargs``
- Additional options to be passed to git-rev-list
+ Additional options to be passed to git-rev-list. They must not alter
+ the ouput style of the command, or parsing will yield incorrect results
Returns
int
"""
@@ -153,9 +154,8 @@ class Commit(base.Object, Iterable, diff.Diffable):
options = {'pretty': 'raw', 'as_process' : True }
options.update(kwargs)
- # the test system might confront us with string values -
proc = repo.git.rev_list(rev, '--', paths, **options)
- return cls._iter_from_process_or_stream(repo, proc)
+ return cls._iter_from_process_or_stream(repo, proc, True)
def iter_parents(self, paths='', **kwargs):
"""
@@ -200,7 +200,7 @@ class Commit(base.Object, Iterable, diff.Diffable):
return stats.Stats._list_from_string(self.repo, text)
@classmethod
- def _iter_from_process_or_stream(cls, repo, proc_or_stream):
+ def _iter_from_process_or_stream(cls, repo, proc_or_stream, from_rev_list):
"""
Parse out commit information into a list of Commit objects
@@ -210,6 +210,9 @@ class Commit(base.Object, Iterable, diff.Diffable):
``proc``
git-rev-list process instance (raw format)
+ ``from_rev_list``
+ If True, the stream was created by rev-list in which case we parse
+ the message differently
Returns
iterator returning Commit objects
"""
@@ -217,10 +220,10 @@ class Commit(base.Object, Iterable, diff.Diffable):
if not hasattr(stream,'next'):
stream = proc_or_stream.stdout
-
for line in stream:
- id = line.split()[1]
- assert line.split()[0] == "commit"
+ commit_tokens = line.split()
+ id = commit_tokens[1]
+ assert commit_tokens[0] == "commit"
tree = stream.next().split()[1]
parents = []
@@ -240,13 +243,20 @@ class Commit(base.Object, Iterable, diff.Diffable):
stream.next()
message_lines = []
- next_line = None
- for msg_line in stream:
- if not msg_line.startswith(' '):
- break
- # END abort message reading
- message_lines.append(msg_line.strip())
- # END while there are message lines
+ if from_rev_list:
+ for msg_line in stream:
+ if not msg_line.startswith(' '):
+ # and forget about this empty marker
+ break
+ # END abort message reading
+ # strip leading 4 spaces
+ message_lines.append(msg_line[4:])
+ # END while there are message lines
+ else:
+ # a stream from our data simply gives us the plain message
+ for msg_line in stream:
+ message_lines.append(msg_line)
+ # END message parsing
message = '\n'.join(message_lines)
yield Commit(repo, id=id, parents=tuple(parents), tree=tree, author=author, authored_date=authored_date,
diff --git a/lib/git/objects/tree.py b/lib/git/objects/tree.py
index 92aae881..371c0dd3 100644
--- a/lib/git/objects/tree.py
+++ b/lib/git/objects/tree.py
@@ -38,9 +38,9 @@ class Tree(base.IndexObject, diff.Diffable):
__slots__ = "_cache"
# using ascii codes for comparison
- ascii_commit_id = (0x31 << 4) + 0x36
- ascii_blob_id = (0x31 << 4) + 0x30
- ascii_tree_id = (0x34 << 4) + 0x30
+ commit_id = 016
+ blob_id = 010
+ tree_id = 040
def __init__(self, repo, id, mode=0, path=None):
@@ -88,8 +88,8 @@ class Tree(base.IndexObject, diff.Diffable):
mode = 0
mode_boundary = i + 6
- # keep it ascii - we compare against the respective values
- type_id = (ord(data[i])<<4) + ord(data[i+1])
+ # read type
+ type_id = ((ord(data[i])-ord_zero)<<3) + (ord(data[i+1])-ord_zero)
i += 2
while data[i] != ' ':
@@ -115,12 +115,13 @@ class Tree(base.IndexObject, diff.Diffable):
sha = data[i:i+20]
i = i + 20
+ mode |= type_id<<12
hexsha = sha_to_hex(sha)
- if type_id == self.ascii_blob_id:
+ if type_id == self.blob_id:
yield blob.Blob(self.repo, hexsha, mode, name)
- elif type_id == self.ascii_tree_id:
+ elif type_id == self.tree_id:
yield Tree(self.repo, hexsha, mode, name)
- elif type_id == self.ascii_commit_id:
+ elif type_id == self.commit_id:
# todo
yield None
else:
diff --git a/lib/git/refs.py b/lib/git/refs.py
index a83628ce..26e7c09e 100644
--- a/lib/git/refs.py
+++ b/lib/git/refs.py
@@ -19,7 +19,7 @@ class Reference(LazyMixin, Iterable):
_common_path_default = "refs"
_id_attribute_ = "name"
- def __init__(self, repo, path, object = None):
+ def __init__(self, repo, path):
"""
Initialize this instance
``repo``
@@ -29,16 +29,12 @@ class Reference(LazyMixin, Iterable):
Path relative to the .git/ directory pointing to the ref in question, i.e.
refs/heads/master
- ``object``
- Object instance, will be retrieved on demand if None
"""
if not path.startswith(self._common_path_default):
raise ValueError("Cannot instantiate %s Reference from path %s" % ( self.__class__.__name__, path ))
self.repo = repo
self.path = path
- if object is not None:
- self.object = object
def __str__(self):
return self.name
@@ -69,8 +65,7 @@ class Reference(LazyMixin, Iterable):
return '/'.join(tokens[2:])
- @property
- def object(self):
+ def _get_object(self):
"""
Returns
The object our ref currently refers to. Refs can be cached, they will
@@ -80,17 +75,54 @@ class Reference(LazyMixin, Iterable):
# Our path will be resolved to the hexsha which will be used accordingly
return Object.new(self.repo, self.path)
- @property
- def commit(self):
+ def _set_object(self, ref, type=None):
+ """
+ Set our reference to point to the given ref. It will be converted
+ to a specific hexsha.
+
+ ``type``
+ If not None, string type of that the object must have, other we raise
+ a type error. Only used internally
+
+ Returns
+ Object we have set. This is used internally only to reduce the amount
+ of calls to the git command
+ """
+ obj = Object.new(self.repo, ref)
+ if type is not None and obj.type != type:
+ raise TypeError("Reference %r cannot point to object of type %r" % (self,obj.type))
+
+ full_ref_path = os.path.join(self.repo.path, self.path)
+ fp = open(full_ref_path, "w")
+ try:
+ fp.write(str(obj))
+ finally:
+ fp.close()
+ return obj
+
+ object = property(_get_object, _set_object, doc="Return the object our ref currently refers to")
+
+ def _set_commit(self, commit):
+ """
+ Set ourselves to point to the given commit.
+
+ Raise
+ ValueError if commit does not actually point to a commit
+ """
+ self._set_object(commit, type="commit")
+
+ def _get_commit(self):
"""
Returns
- Commit object the head points to
+ Commit object the reference points to
"""
commit = self.object
if commit.type != "commit":
- raise TypeError("Object of reference %s did not point to a commit" % self)
+ raise TypeError("Object of reference %s did not point to a commit, but to %r" % (self, commit))
return commit
+ commit = property(_get_commit, _set_commit, doc="Return Commit object the reference points to")
+
@classmethod
def iter_items(cls, repo, common_path = None, **kwargs):
"""
@@ -182,6 +214,7 @@ class Reference(LazyMixin, Iterable):
# obj.size = object_size
# return cls(repo, full_path, obj)
+
class SymbolicReference(object):
"""
@@ -217,8 +250,7 @@ class SymbolicReference(object):
def _get_path(self):
return os.path.join(self.repo.path, self.name)
- @property
- def commit(self):
+ def _get_commit(self):
"""
Returns:
Commit object we point to, works for detached and non-detached
@@ -238,6 +270,18 @@ class SymbolicReference(object):
# Otherwise it would have detached it
return Head(self.repo, tokens[1]).commit
+ def _set_commit(self, commit):
+ """
+ Set our commit, possibly dereference our symbolic reference first.
+ """
+ if self.is_detached:
+ return self._set_reference(commit)
+
+ # set the commit on our reference
+ self._get_reference().commit = commit
+
+ commit = property(_get_commit, _set_commit, doc="Query or set commits directly")
+
def _get_reference(self):
"""
Returns
@@ -247,7 +291,7 @@ class SymbolicReference(object):
try:
tokens = fp.readline().rstrip().split(' ')
if tokens[0] != 'ref:':
- raise TypeError("%s is a detached symbolic reference as it points to %r" % tokens[0])
+ raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, tokens[0]))
return Reference.from_path(self.repo, tokens[1])
finally:
fp.close()
@@ -322,7 +366,8 @@ class HEAD(SymbolicReference):
paths=None, **kwargs):
"""
Reset our HEAD to the given commit optionally synchronizing
- the index and working tree.
+ the index and working tree. The reference we refer to will be set to
+ commit as well.
``commit``
Commit object, Reference Object or string identifying a revision we
@@ -383,12 +428,92 @@ class Head(Reference):
"""
_common_path_default = "refs/heads"
+ @classmethod
+ def create(cls, repo, path, commit='HEAD', force=False, **kwargs ):
+ """
+ Create a new head.
+ ``repo``
+ Repository to create the head in
+
+ ``path``
+ The name or path of the head, i.e. 'new_branch' or
+ feature/feature1. The prefix refs/heads is implied.
+
+ ``commit``
+ Commit to which the new head should point, defaults to the
+ current HEAD
+
+ ``force``
+ if True, force creation even if branch with that name already exists.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-branch, i.e.
+ track, no-track, l
+
+ Returns
+ Newly created Head
+
+ Note
+ This does not alter the current HEAD, index or Working Tree
+ """
+ if cls is not Head:
+ raise TypeError("Only Heads can be created explicitly, not objects of type %s" % cls.__name__)
+
+ args = ( path, commit )
+ if force:
+ kwargs['f'] = True
+
+ repo.git.branch(*args, **kwargs)
+ return cls(repo, "%s/%s" % ( cls._common_path_default, path))
+
+
+ @classmethod
+ def delete(cls, repo, *heads, **kwargs):
+ """
+ Delete the given heads
+
+ ``force``
+ If True, the heads will be deleted even if they are not yet merged into
+ the main development stream.
+ Default False
+ """
+ force = kwargs.get("force", False)
+ flag = "-d"
+ if force:
+ flag = "-D"
+ repo.git.branch(flag, *heads)
+
+
+ def rename(self, new_path, force=False):
+ """
+ Rename self to a new path
+
+ ``new_path``
+ Either a simple name or a path, i.e. new_name or features/new_name.
+ The prefix refs/heads is implied
+
+ ``force``
+ If True, the rename will succeed even if a head with the target name
+ already exists.
+
+ Returns
+ self
+ """
+ flag = "-m"
+ if force:
+ flag = "-M"
+
+ self.repo.git.branch(flag, self, new_path)
+ self.path = "%s/%s" % (self._common_path_default, new_path)
+ return self
+
+
class TagReference(Reference):
"""
Class representing a lightweight tag reference which either points to a commit
- or to a tag object. In the latter case additional information, like the signature
- or the tag-creator, is available.
+ ,a tag object or any other object. In the latter case additional information,
+ like the signature or the tag-creator, is available.
This tag object will always point to a commit object, but may carray additional
information in a tag object::
@@ -426,6 +551,52 @@ class TagReference(Reference):
if self.object.type == "tag":
return self.object
return None
+
+ @classmethod
+ def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs):
+ """
+ Create a new tag object.
+
+ ``path``
+ The name of the tag, i.e. 1.0 or releases/1.0.
+ The prefix refs/tags is implied
+
+ ``ref``
+ A reference to the object you want to tag. It can be a commit, tree or
+ blob.
+
+ ``message``
+ If not None, the message will be used in your tag object. This will also
+ create an additional tag object that allows to obtain that information, i.e.::
+ tagref.tag.message
+
+ ``force``
+ If True, to force creation of a tag even though that tag already exists.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-tag
+
+ Returns
+ A new TagReference
+ """
+ args = ( path, ref )
+ if message:
+ kwargs['m'] = message
+ if force:
+ kwargs['f'] = True
+
+ repo.git.tag(*args, **kwargs)
+ return TagReference(repo, "%s/%s" % (cls._common_path_default, path))
+
+ @classmethod
+ def delete(cls, repo, *tags):
+ """
+ Delete the given existing tag or tags
+ """
+ repo.git.tag("-d", *tags)
+
+
+
# provide an alias
@@ -459,3 +630,14 @@ class RemoteReference(Head):
"""
tokens = self.path.split('/')
return '/'.join(tokens[3:])
+
+ @classmethod
+ def delete(cls, repo, *remotes, **kwargs):
+ """
+ Delete the given remote references.
+
+ Note
+ kwargs are given for compatability with the base class method as we
+ should not narrow the signature.
+ """
+ repo.git.branch("-d", "-r", *remotes)
diff --git a/lib/git/repo.py b/lib/git/repo.py
index 94555a31..569d6f1b 100644
--- a/lib/git/repo.py
+++ b/lib/git/repo.py
@@ -14,6 +14,7 @@ from errors import InvalidGitRepositoryError, NoSuchPathError
from cmd import Git
from actor import Actor
from refs import *
+from index import IndexFile
from objects import *
from config import GitConfigParser
from remote import Remote
@@ -145,6 +146,14 @@ class Repo(object):
# alias heads
branches = heads
+
+ @property
+ def index(self):
+ """
+ Returns
+ IndexFile representing this repository's index.
+ """
+ return IndexFile(self)
@property
def head(self):