diff options
Diffstat (limited to 'lib/git')
-rw-r--r-- | lib/git/actor.py | 9 | ||||
-rw-r--r-- | lib/git/cmd.py | 26 | ||||
-rw-r--r-- | lib/git/diff.py | 3 | ||||
-rw-r--r-- | lib/git/index.py | 694 | ||||
-rw-r--r-- | lib/git/objects/base.py | 1 | ||||
-rw-r--r-- | lib/git/objects/commit.py | 40 | ||||
-rw-r--r-- | lib/git/objects/tree.py | 17 | ||||
-rw-r--r-- | lib/git/refs.py | 216 | ||||
-rw-r--r-- | lib/git/repo.py | 9 |
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): |