diff options
Diffstat (limited to 'lib/git/index.py')
-rw-r--r-- | lib/git/index.py | 333 |
1 files changed, 249 insertions, 84 deletions
diff --git a/lib/git/index.py b/lib/git/index.py index 9fd1fad2..86bdfd39 100644 --- a/lib/git/index.py +++ b/lib/git/index.py @@ -17,7 +17,7 @@ import sys import stat 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 @@ -41,7 +41,57 @@ class _TemporaryFileSwap(object): # 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. + """ + + @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. @@ -57,49 +107,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): @@ -109,33 +152,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): """ @@ -143,8 +161,28 @@ 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)) + +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): """ @@ -176,10 +214,8 @@ class IndexFile(LazyMixin, diff.Diffable): The index contains an entries dict whose keys are tuples of type IndexEntry to facilitate access. - As opposed to the Index type, the IndexFile represents the index on file level. - This can be considered an alternate, file-based implementation of the Index class - with less support for common functions. Use it for very special and custom index - handling. + 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", "_file_path" ) _VERSION = 2 # latest version we support @@ -243,8 +279,7 @@ class IndexFile(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): @@ -286,14 +321,14 @@ class IndexFile(LazyMixin, diff.Diffable): 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())) @@ -325,7 +360,7 @@ class IndexFile(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 @@ -483,6 +518,10 @@ class IndexFile(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) @@ -511,13 +550,13 @@ class IndexFile(LazyMixin, diff.Diffable): Note: This is a possibly dangerious operations as it will discard your changes - to index.endtries + to index.entries Returns self """ del(self.entries) - self.entries + # allows to lazily reread on demand return self def write_tree(self): @@ -544,69 +583,183 @@ class IndexFile(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 + + @clear_cache @default_index def add(self, items, **kwargs): """ - Add files from the working copy, specific blobs or IndexEntries - to the index. - - TODO: Its important to specify a way to add symlinks directly, even - on systems that do not support it, like ... erm ... windows. + 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. + + 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 + + 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. + + They are added at stage 0 + + - BaseIndexEntry or type + Handling equals the one of Blob objects, but the stage may be + explicitly set. + ``**kwargs`` - Additional keyword arguments to be passed to git-update-index + Additional keyword arguments to be passed to git-update-index, such + as index_only. Returns - List(IndexEntries) representing the entries just added + List(BaseIndexEntries) representing the entries just actually added. """ raise NotImplementedError("todo") + @clear_cache @default_index - def remove(self, items, affect_working_tree=False, **kwargs): + def remove(self, items, working_tree=False, **kwargs): """ - Remove the given file_paths or blobs from the index and optionally from + Remove the given items from the index and optionally from the working tree as well. ``items`` - TODO + 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. - ``affect_working_tree`` + ``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-update-index + Additional keyword arguments to be passed to git-rm, such + as 'r' to allow recurive removal of Returns - self - """ - raise NotImplementedError("todo") - return self + 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=None, parent_commits=None, **kwargs): + def commit(self, message, parent_commits=None): """ Commit the current index, creating a commit object. ``message`` - Commit 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 None or empty, the current head commit will be the parent of the + 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 - ``**kwargs`` - Additional keyword arguments passed to git-commit - 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 """ - raise NotImplementedError("todo") + 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) + return Commit(self.repo, commit_sha) + finally: + fp.close() + os.remove(tmp_file_path) + + @clear_cache @default_index - def reset(self, commit='HEAD', working_tree=False, **kwargs): + def reset(self, commit='HEAD', working_tree=False, paths=None, **kwargs): """ Reset the index to reflect the tree at the given commit. This will not adjust our HEAD reference as opposed to HEAD.reset. @@ -619,11 +772,23 @@ class IndexFile(LazyMixin, diff.Diffable): ``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 ! ``**kwargs`` Additional keyword arguments passed to git-reset + + Returns + self """ - raise NotImplementedError("todo") + head = self.repo.head + prev_commit = head.commit + + # reset to get the tree/working copy + head.reset(commit, index=True, working_tree=working_tree, paths=paths, **kwargs) + # put the head back + head.reset(prev_commit, index=False, working_tree=False) + return self @default_index def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs): |