Skip to content

Commit 20863cf

Browse files
committed
Implemented Submodule.rename()
A test verifies it's truly working. Related to #238
1 parent a223c7b commit 20863cf

File tree

7 files changed

+147
-6
lines changed

7 files changed

+147
-6
lines changed

‎doc/source/changes.rst

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Changelog
66
================
77
* Added `Repo.merge_base()` implementation. See the `respective issue on github <https://github.com/gitpython-developers/GitPython/issues/169>`_
88
* `[include]` sections in git configuration files are now respected
9+
* Added `GitConfigParser.rename_section()`
10+
* Added `Submodule.rename()`
911
* A list of all issues can be found here: https://github.com/gitpython-developers/GitPython/issues?q=milestone%3A%22v0.3.6+-+Features%22+
1012

1113
0.3.5 - Bugfixes

‎git/config.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -478,8 +478,6 @@ def _assure_writable(self, method_name):
478478
if self.read_only:
479479
raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
480480

481-
@needs_values
482-
@set_dirty_and_flush_changes
483481
def add_section(self, section):
484482
"""Assures added options will stay in order"""
485483
return super(GitConfigParser, self).add_section(section)
@@ -546,3 +544,23 @@ def set_value(self, section, option, value):
546544
if not self.has_section(section):
547545
self.add_section(section)
548546
self.set(section, option, str(value))
547+
548+
def rename_section(self, section, new_name):
549+
"""rename the given section to new_name
550+
:raise ValueError: if section doesn't exit
551+
:raise ValueError: if a section with new_name does already exist
552+
:return: this instance
553+
"""
554+
if not self.has_section(section):
555+
raise ValueError("Source section '%s' doesn't exist" % section)
556+
if self.has_section(new_name):
557+
raise ValueError("Destination section '%s' already exists" % new_name)
558+
559+
super(GitConfigParser, self).add_section(new_name)
560+
for k, v in self.items(section):
561+
self.set(new_name, k, str(v))
562+
# end for each value to copy
563+
564+
# This call writes back the changes, which is why we don't have the respective decorator
565+
self.remove_section(section)
566+
return self

‎git/objects/submodule/base.py

+78-4
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@ def __init__(self, repo, binsha, mode=None, path=None, name=None, parent_commit=
109109
def _set_cache_(self, attr):
110110
if attr == '_parent_commit':
111111
# set a default value, which is the root tree of the current head
112-
self._parent_commit = self.repo.commit()
112+
try:
113+
self._parent_commit = self.repo.commit()
114+
except ValueError:
115+
# This fails in an empty repository.
116+
self._parent_commit = None
117+
# end exception handling
113118
elif attr in ('path', '_url', '_branch_path'):
114119
reader = self.config_reader()
115120
# default submodule values
@@ -163,7 +168,13 @@ def _config_parser(cls, repo, parent_commit, read_only):
163168
:raise IOError: If the .gitmodules file cannot be found, either locally or in the repository
164169
at the given parent commit. Otherwise the exception would be delayed until the first
165170
access of the config parser"""
166-
parent_matches_head = repo.head.commit == parent_commit
171+
try:
172+
parent_matches_head = repo.head.commit == parent_commit
173+
except ValueError:
174+
# We are most likely in an empty repository, so the HEAD doesn't point to a valid ref
175+
parent_matches_head = True
176+
# end
177+
167178
if not repo.bare and parent_matches_head:
168179
fp_module = os.path.join(repo.working_tree_dir, cls.k_modules_file)
169180
else:
@@ -370,6 +381,14 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False):
370381
mrepo = cls._clone_repo(repo, url, path, name, **kwargs)
371382
# END verify url
372383

384+
# It's important to add the URL to the parent config, to let `git submodule` know.
385+
# otherwise there is a '-' character in front of the submodule listing
386+
# a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
387+
# -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
388+
writer = sm.repo.config_writer()
389+
writer.set_value(sm_section(name), 'url', url)
390+
writer.release()
391+
373392
# update configuration and index
374393
index = sm.repo.index
375394
writer = sm.config_writer(index=index, write=False)
@@ -386,11 +405,23 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False):
386405
del(writer)
387406

388407
# we deliberatly assume that our head matches our index !
389-
pcommit = repo.head.commit
390-
sm._parent_commit = pcommit
408+
parent_repo_is_empty = False
409+
try:
410+
sm._parent_commit = repo.head.commit
411+
except ValueError:
412+
parent_repo_is_empty = True
413+
# Can't set this yet, if the parent repo is empty.
414+
# end
391415
sm.binsha = mrepo.head.commit.binsha
392416
index.add([sm], write=True)
393417

418+
if parent_repo_is_empty:
419+
# The user is expected to make a commit, and this submodule will initialize itself when
420+
# _parent_commit is required
421+
del sm._parent_commit
422+
log.debug("Will not set _parent_commit now as the parent repository has no commit yet.")
423+
# end
424+
394425
return sm
395426

396427
def update(self, recursive=False, init=True, to_latest_revision=False, progress=None,
@@ -875,6 +906,49 @@ def config_writer(self, index=None, write=True):
875906
writer.config._auto_write = write
876907
return writer
877908

909+
@unbare_repo
910+
def rename(self, new_name):
911+
"""Rename this submodule
912+
:note: This method takes care of renaming the submodule in various places, such as
913+
914+
* $parent_git_dir/config
915+
* $working_tree_dir/.gitmodules
916+
* (git >=v1.8.0: move submodule repository to new name)
917+
918+
As .gitmodules will be changed, you would need to make a commit afterwards. The changed .gitmodules file
919+
will already be added to the index
920+
921+
:return: this submodule instance
922+
"""
923+
if self.name == new_name:
924+
return self
925+
926+
# .git/config
927+
pw = self.repo.config_writer()
928+
# As we ourselves didn't write anything about submodules into the parent .git/config, we will not require
929+
# it to exist, and just ignore missing entries
930+
if pw.has_section(sm_section(self.name)):
931+
pw.rename_section(sm_section(self.name), sm_section(new_name))
932+
# end
933+
pw.release()
934+
935+
# .gitmodules
936+
cw = self.config_writer().config
937+
cw.rename_section(sm_section(self.name), sm_section(new_name))
938+
cw.release()
939+
940+
self._name = new_name
941+
942+
# .git/modules
943+
mod = self.module()
944+
if mod.has_separate_working_tree():
945+
module_abspath = self._module_abspath(self.repo, self.path, new_name)
946+
os.renames(mod.git_dir, module_abspath)
947+
self._write_git_file_and_module_config(mod.working_tree_dir, module_abspath)
948+
# end move separate git repository
949+
950+
return self
951+
878952
#} END edit interface
879953

880954
#{ Query Interface

‎git/repo/base.py

+9
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,15 @@ def archive(self, ostream, treeish=None, prefix=None, **kwargs):
868868
self.git.archive(treeish, *path, **kwargs)
869869
return self
870870

871+
def has_separate_working_tree(self):
872+
""":return: True if our git_dir is not at the root of our working_tree_dir, but a .git file with a
873+
platform agnositic symbolic link. Our git_dir will be whereever the .git file points to
874+
:note: bare repositories will always return False here
875+
"""
876+
if self.bare:
877+
return False
878+
return os.path.isfile(os.path.join(self.working_tree_dir, '.git'))
879+
871880
rev_parse = rev_parse
872881

873882
def __repr__(self):

‎git/test/test_config.py

+13
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,16 @@ def check_test_value(cr, value):
193193
cr = GitConfigParser(fpa, read_only=True)
194194
check_test_value(cr, tv)
195195
cr.release()
196+
197+
def test_rename(self):
198+
file_obj = self._to_memcache(fixture_path('git_config'))
199+
cw = GitConfigParser(file_obj, read_only=False, merge_includes=False)
200+
201+
self.failUnlessRaises(ValueError, cw.rename_section, "doesntexist", "foo")
202+
self.failUnlessRaises(ValueError, cw.rename_section, "core", "include")
203+
204+
nn = "bee"
205+
assert cw.rename_section('core', nn) is cw
206+
assert not cw.has_section('core')
207+
assert len(cw.items(nn)) == 4
208+
cw.release()

‎git/test/test_repo.py

+2
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def test_init(self):
164164
r = Repo.init(path=path, bare=True)
165165
assert isinstance(r, Repo)
166166
assert r.bare is True
167+
assert not r.has_separate_working_tree()
167168
assert os.path.isdir(r.git_dir)
168169

169170
self._assert_empty_repo(r)
@@ -200,6 +201,7 @@ def test_init(self):
200201
os.chdir(git_dir_rela)
201202
r = Repo.init(bare=False)
202203
assert r.bare is False
204+
assert not r.has_separate_working_tree()
203205

204206
self._assert_empty_repo(r)
205207
finally:

‎git/test/test_submodule.py

+23
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,10 @@ def test_git_submodule_compatibility(self, rwdir):
670670
assert module_repo_path.startswith(os.path.join(parent.working_tree_dir, sm_path))
671671
if not sm._need_gitfile_submodules(parent.git):
672672
assert os.path.isdir(module_repo_path)
673+
assert not sm.module().has_separate_working_tree()
673674
else:
674675
assert os.path.isfile(module_repo_path)
676+
assert sm.module().has_separate_working_tree()
675677
assert find_git_dir(module_repo_path) is not None, "module pointed to by .git file must be valid"
676678
# end verify submodule 'style'
677679

@@ -689,6 +691,8 @@ def test_git_submodule_compatibility(self, rwdir):
689691
# Fails because there are new commits, compared to the remote we cloned from
690692
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove, dry_run=True)
691693

694+
# TODO: rename nested submodule
695+
692696
# remove
693697
sm_module_path = sm.module().git_dir
694698

@@ -698,3 +702,22 @@ def test_git_submodule_compatibility(self, rwdir):
698702
assert sm.module_exists() == dry_run
699703
assert os.path.isdir(sm_module_path) == dry_run
700704
# end for each dry-run mode
705+
706+
@with_rw_directory
707+
def test_rename(self, rwdir):
708+
parent = git.Repo.init(os.path.join(rwdir, 'parent'))
709+
sm_name = 'mymodules/myname'
710+
sm = parent.create_submodule(sm_name, 'submodules/intermediate/one', url=self._submodule_url())
711+
parent.index.commit("Added submodule")
712+
assert sm._parent_commit is not None
713+
714+
assert sm.rename(sm_name) is sm and sm.name == sm_name
715+
716+
new_sm_name = "shortname"
717+
assert sm.rename(new_sm_name) is sm
718+
assert sm.exists()
719+
720+
sm_mod = sm.module()
721+
if os.path.isfile(os.path.join(sm_mod.working_tree_dir, '.git')) == sm._need_gitfile_submodules(parent.git):
722+
assert sm_mod.git_dir.endswith(".git/modules/" + new_sm_name)
723+
# end

0 commit comments

Comments
 (0)