diff options
author | Sebastian Thiel <byronimo@gmail.com> | 2010-11-17 15:24:48 +0100 |
---|---|---|
committer | Sebastian Thiel <byronimo@gmail.com> | 2010-11-17 15:24:48 +0100 |
commit | a1e6234c27abf041e4c8cd1a799950e7cd9104f6 (patch) | |
tree | 3e39fa29ea1478f9a31f378a25bd9c58bb71ed91 | |
parent | b03933057df80ea9f860cc616eb7733f140f866e (diff) | |
download | gitpython-a1e6234c27abf041e4c8cd1a799950e7cd9104f6.tar.gz |
Inital implementation of Submodule.move including a very simple and to-be-improved test
-rw-r--r-- | lib/git/config.py | 5 | ||||
-rw-r--r-- | lib/git/index/fun.py | 3 | ||||
-rw-r--r-- | lib/git/objects/submodule.py | 145 | ||||
-rw-r--r-- | test/git/test_repo.py | 9 | ||||
-rw-r--r-- | test/git/test_submodule.py | 54 |
5 files changed, 162 insertions, 54 deletions
diff --git a/lib/git/config.py b/lib/git/config.py index 0528f318..f1a8832e 100644 --- a/lib/git/config.py +++ b/lib/git/config.py @@ -91,6 +91,11 @@ class SectionConstraint(object): as first argument""" return getattr(self._config, method)(self._section_name, *args, **kwargs) + @property + def config(self): + """return: Configparser instance we constrain""" + return self._config + class GitConfigParser(cp.RawConfigParser, object): """Implements specifics required to read git style configuration files. diff --git a/lib/git/index/fun.py b/lib/git/index/fun.py index b05344a8..87fdf1a9 100644 --- a/lib/git/index/fun.py +++ b/lib/git/index/fun.py @@ -30,6 +30,7 @@ from typ import ( CE_NAMEMASK, CE_STAGESHIFT ) +CE_NAMEMASK_INV = ~CE_NAMEMASK from util import ( pack, @@ -84,7 +85,7 @@ def write_cache(entries, stream, extension_data=None, ShaStreamCls=IndexFileSHA1 path = entry[3] plen = len(path) & CE_NAMEMASK # path length assert plen == len(path), "Path %s too long to fit into index" % entry[3] - flags = plen | entry[2] + flags = plen | (entry[2] & CE_NAMEMASK_INV) # clear possible previous values write(pack(">LLLLLL20sH", entry[6], entry[7], entry[0], entry[8], entry[9], entry[10], entry[1], flags)) write(path) diff --git a/lib/git/objects/submodule.py b/lib/git/objects/submodule.py index e07117a6..8a1ab6af 100644 --- a/lib/git/objects/submodule.py +++ b/lib/git/objects/submodule.py @@ -28,6 +28,19 @@ def sm_name(section): def mkhead(repo, path): """:return: New branch/head instance""" return git.Head(repo, git.Head.to_full_path(path)) + +def unbare_repo(func): + """Methods with this decorator raise InvalidGitRepositoryError if they + encounter a bare repository""" + def wrapper(self, *args, **kwargs): + if self.repo.bare: + raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__) + #END bare method + return func(self, *args, **kwargs) + # END wrapper + wrapper.__name__ = func.__name__ + return wrapper + #} END utilities @@ -39,10 +52,14 @@ class SubmoduleConfigParser(GitConfigParser): with the new data, if we have written into a stream. Otherwise it will add the local file to the index to make it correspond with the working tree. Additionally, the cache must be cleared + + Please note that no mutating method will work in bare mode """ def __init__(self, *args, **kwargs): self._smref = None + self._index = None + self._auto_write = True super(SubmoduleConfigParser, self).__init__(*args, **kwargs) #{ Interface @@ -59,7 +76,11 @@ class SubmoduleConfigParser(GitConfigParser): sm = self._smref() if sm is not None: - sm.repo.index.add([sm.k_modules_file]) + index = self._index + if index is None: + index = sm.repo.index + # END handle index + index.add([sm.k_modules_file], write=self._auto_write) sm._clear_cache() # END handle weakref @@ -102,6 +123,7 @@ class Submodule(base.IndexObject, Iterable, Traversable): :param url: The url to the remote repository which is the submodule :param branch: Head instance to checkout when cloning the remote repository""" super(Submodule, self).__init__(repo, binsha, mode, path) + self.size = 0 if parent_commit is not None: self._parent_commit = parent_commit if url is not None: @@ -113,9 +135,7 @@ class Submodule(base.IndexObject, Iterable, Traversable): self._name = name def _set_cache_(self, attr): - if attr == 'size': - raise ValueError("Submodules do not have a size as they do not refer to anything in this repository") - elif attr == '_parent_commit': + if attr == '_parent_commit': # set a default value, which is the root tree of the current head self._parent_commit = self.repo.commit() elif attr in ('path', '_url', '_branch'): @@ -235,8 +255,8 @@ class Submodule(base.IndexObject, Iterable, Traversable): :note: works atomically, such that no change will be done if the repository update fails for instance""" if repo.bare: - raise InvalidGitRepositoryError("Cannot add a submodule to bare repositories") - #END handle bare mode + raise InvalidGitRepositoryError("Cannot add submodules to bare repositories") + # END handle bare repos path = to_native_path_linux(path) if path.endswith('/'): @@ -280,7 +300,8 @@ class Submodule(base.IndexObject, Iterable, Traversable): # END verify url # update configuration and index - writer = sm.config_writer() + index = sm.repo.index + writer = sm.config_writer(index=index, write=False) writer.set_value('url', url) writer.set_value('path', path) @@ -302,11 +323,10 @@ class Submodule(base.IndexObject, Iterable, Traversable): pcommit = repo.head.commit sm._parent_commit = pcommit sm.binsha = mrepo.head.commit.binsha - repo.index.add([sm], write=True) + index.add([sm], write=True) return sm - def update(self, recursive=False, init=True, to_latest_revision=False): """Update the repository of this submodule to point to the checkout we point at with the binsha of this instance. @@ -426,6 +446,85 @@ class Submodule(base.IndexObject, Iterable, Traversable): return self + @unbare_repo + def move(self, module_path): + """Move the submodule to a another module path. This involves physically moving + the repository at our current path, changing the configuration, as well as + adjusting our index entry accordingly. + :param module_path: the path to which to move our module, given as + repository-relative path. Intermediate directories will be created + accordingly. If the path already exists, it must be empty. + Trailling (back)slashes are removed automatically + :return: self + :raise ValueError: if the module path existed and was not empty, or was a file + :note: Currently the method is not atomic, and it could leave the repository + in an inconsistent state if a sub-step fails for some reason + """ + module_path = to_native_path_linux(module_path) + if module_path.endswith('/'): + module_path = module_path[:-1] + # END handle trailing slash + + # VERIFY DESTINATION + if module_path == self.path: + return self + #END handle no change + + dest_path = join_path_native(self.repo.working_tree_dir, module_path) + if os.path.isfile(dest_path): + raise ValueError("Cannot move repository onto a file: %s" % dest_path) + # END handle target files + + # remove existing destination + if os.path.exists(dest_path): + if len(os.listdir(dest_path)): + raise ValueError("Destination module directory was not empty") + #END handle non-emptyness + + if os.path.islink(dest_path): + os.remove(dest_path) + else: + os.rmdir(dest_path) + #END handle link + else: + # recreate parent directories + # NOTE: renames() does that now + pass + #END handle existance + + # move the module into place if possible + cur_path = self.module_path() + if os.path.exists(cur_path): + os.renames(cur_path, dest_path) + #END move physical module + + # NOTE: from now on, we would have to undo the rename ! + + # rename the index entry - have to manipulate the index directly as + # git-mv cannot be used on submodules ... yeah + index = self.repo.index + try: + ekey = index.entry_key(self.path, 0) + entry = index.entries[ekey] + del(index.entries[ekey]) + nentry = git.IndexEntry(entry[:3]+(module_path,)+entry[4:]) + ekey = index.entry_key(module_path, 0) + index.entries[ekey] = nentry + except KeyError: + raise ValueError("Submodule's entry at %r did not exist" % (self.path)) + #END handle submodule doesn't exist + + # update configuration + writer = self.config_writer(index=index) # auto-write + writer.set_value('path', module_path) + self.path = module_path + del(writer) + + return self + + + + @unbare_repo def remove(self, module=True, force=False, configuration=True, dry_run=False): """Remove this submodule from the repository. This will remove our entry from the .gitmodules file and the entry in the .git/config file. @@ -449,10 +548,6 @@ class Submodule(base.IndexObject, Iterable, Traversable): :note: doesn't work in bare repositories :raise InvalidGitRepositoryError: thrown if the repository cannot be deleted :raise OSError: if directories or files could not be removed""" - if self.repo.bare: - raise InvalidGitRepositoryError("Cannot delete a submodule in bare repository") - # END handle bare mode - if not (module + configuration): raise ValueError("Need to specify to delete at least the module, or the configuration") # END handle params @@ -565,31 +660,37 @@ class Submodule(base.IndexObject, Iterable, Traversable): return self - def config_writer(self): + @unbare_repo + def config_writer(self, index=None, write=True): """:return: a config writer instance allowing you to read and write the data belonging to this submodule into the .gitmodules file. + :param index: if not None, an IndexFile instance which should be written. + defaults to the index of the Submodule's parent repository. + :param write: if True, the index will be written each time a configuration + value changes. + :note: the parameters allow for a more efficient writing of the index, + as you can pass in a modified index on your own, prevent automatic writing, + and write yourself once the whole operation is complete :raise ValueError: if trying to get a writer on a parent_commit which does not match the current head commit :raise IOError: If the .gitmodules file/blob could not be read""" - if self.repo.bare: - raise InvalidGitRepositoryError("Cannot change submodule configuration in a bare repository") - return self._config_parser_constrained(read_only=False) + writer = self._config_parser_constrained(read_only=False) + if index is not None: + writer.config._index = index + writer.config._auto_write = write + return writer #} END edit interface #{ Query Interface + @unbare_repo def module(self): """:return: Repo instance initialized from the repository at our submodule path :raise InvalidGitRepositoryError: if a repository was not available. This could also mean that it was not yet initialized""" # late import to workaround circular dependencies - - if self.repo.bare: - raise InvalidGitRepositoryError("Cannot retrieve module repository in bare parent repositories") - # END handle bare mode - module_path = self.module_path() try: repo = git.Repo(module_path) diff --git a/test/git/test_repo.py b/test/git/test_repo.py index 3a59f05e..2acccced 100644 --- a/test/git/test_repo.py +++ b/test/git/test_repo.py @@ -562,3 +562,12 @@ class TestRepo(TestBase): assert isinstance(self.rorepo.submodule("lib/git/ext/gitdb"), Submodule) self.failUnlessRaises(ValueError, self.rorepo.submodule, "doesn't exist") + + @with_rw_repo('HEAD', bare=False) + def test_submodule_update(self, rwrepo): + # fails in bare mode + rwrepo._bare = True + self.failUnlessRaises(InvalidGitRepositoryError, rwrepo.submodule_update) + rwrepo._bare = False + + diff --git a/test/git/test_submodule.py b/test/git/test_submodule.py index 6432fcaf..20826f70 100644 --- a/test/git/test_submodule.py +++ b/test/git/test_submodule.py @@ -36,8 +36,8 @@ class TestSubmodule(TestBase): assert sm.url == 'git://gitorious.org/git-python/gitdb.git' assert sm.branch.name == 'master' # its unset in this case assert sm.parent_commit == rwrepo.head.commit - # size is invalid - self.failUnlessRaises(ValueError, getattr, sm, 'size') + # size is always 0 + assert sm.size == 0 # some commits earlier we still have a submodule, but its at a different commit smold = Submodule.iter_items(rwrepo, self.k_subm_changed).next() @@ -240,7 +240,7 @@ class TestSubmodule(TestBase): # add a simple remote repo - trailing slashes are no problem smid = "newsub" osmid = "othersub" - nsm = Submodule.add(rwrepo, smid, sm_repopath, new_smclone_path, None, no_checkout=True) + nsm = Submodule.add(rwrepo, smid, sm_repopath, new_smclone_path+"/", None, no_checkout=True) assert nsm.name == smid assert nsm.module_exists() assert nsm.exists() @@ -261,11 +261,29 @@ class TestSubmodule(TestBase): rwrepo.index.commit("my submod commit") assert len(rwrepo.submodules) == 2 - # if a submodule's repo has no remotes, it can't be added without an explicit url - osmod = osm.module() # needs update as the head changed, it thinks its in the history # of the repo otherwise + nsm._parent_commit = rwrepo.head.commit osm._parent_commit = rwrepo.head.commit + + # MOVE MODULE + ############# + # renaming to the same path does nothing + assert nsm.move(sm.path) is nsm + + # rename a module + nmp = join_path_native("new", "module", "dir") + "/" # new module path + assert nsm.move(nmp) is nsm + nmp = nmp[:-1] # cut last / + assert nsm.path == nmp + assert rwrepo.submodules[0].path == nmp + + + # REMOVE 'EM ALL + ################ + # if a submodule's repo has no remotes, it can't be added without an explicit url + osmod = osm.module() + osm.remove(module=False) for remote in osmod.remotes: remote.remove(osmod, remote.name) @@ -273,35 +291,9 @@ class TestSubmodule(TestBase): self.failUnlessRaises(ValueError, Submodule.add, rwrepo, osmid, csm_repopath, url=None) # END handle bare mode - # Error if there is no submodule file here self.failUnlessRaises(IOError, Submodule._config_parser, rwrepo, rwrepo.commit(self.k_no_subm_tag), True) - # TODO: Handle bare/unbare - # latest submodules write changes into the .gitmodules files - - # uncached path/url - retrieves information from .gitmodules file - - # index stays up-to-date with the working tree .gitmodules file - - # changing the root_tree yields new values when querying them (i.e. cache is cleared) - - - - - # set_parent_commit fails if tree has no gitmodule file - - - - if rwrepo.bare: - # module fails - pass - else: - # get the module repository - pass - # END bare handling - - # Writing of historical submodule configurations must not work @with_rw_repo(k_subm_current) def test_base_rw(self, rwrepo): |