Skip to content

Commit a1e6234

Browse files
committed
Inital implementation of Submodule.move including a very simple and to-be-improved test
1 parent b039330 commit a1e6234

File tree

5 files changed

+162
-54
lines changed

5 files changed

+162
-54
lines changed

lib/git/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ def _call_config(self, method, *args, **kwargs):
9191
as first argument"""
9292
return getattr(self._config, method)(self._section_name, *args, **kwargs)
9393

94+
@property
95+
def config(self):
96+
"""return: Configparser instance we constrain"""
97+
return self._config
98+
9499

95100
class GitConfigParser(cp.RawConfigParser, object):
96101
"""Implements specifics required to read git style configuration files.

lib/git/index/fun.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
CE_NAMEMASK,
3131
CE_STAGESHIFT
3232
)
33+
CE_NAMEMASK_INV = ~CE_NAMEMASK
3334

3435
from util import (
3536
pack,
@@ -84,7 +85,7 @@ def write_cache(entries, stream, extension_data=None, ShaStreamCls=IndexFileSHA1
8485
path = entry[3]
8586
plen = len(path) & CE_NAMEMASK # path length
8687
assert plen == len(path), "Path %s too long to fit into index" % entry[3]
87-
flags = plen | entry[2]
88+
flags = plen | (entry[2] & CE_NAMEMASK_INV) # clear possible previous values
8889
write(pack(">LLLLLL20sH", entry[6], entry[7], entry[0],
8990
entry[8], entry[9], entry[10], entry[1], flags))
9091
write(path)

lib/git/objects/submodule.py

Lines changed: 123 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ def sm_name(section):
2828
def mkhead(repo, path):
2929
""":return: New branch/head instance"""
3030
return git.Head(repo, git.Head.to_full_path(path))
31+
32+
def unbare_repo(func):
33+
"""Methods with this decorator raise InvalidGitRepositoryError if they
34+
encounter a bare repository"""
35+
def wrapper(self, *args, **kwargs):
36+
if self.repo.bare:
37+
raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__)
38+
#END bare method
39+
return func(self, *args, **kwargs)
40+
# END wrapper
41+
wrapper.__name__ = func.__name__
42+
return wrapper
43+
3144
#} END utilities
3245

3346

@@ -39,10 +52,14 @@ class SubmoduleConfigParser(GitConfigParser):
3952
with the new data, if we have written into a stream. Otherwise it will
4053
add the local file to the index to make it correspond with the working tree.
4154
Additionally, the cache must be cleared
55+
56+
Please note that no mutating method will work in bare mode
4257
"""
4358

4459
def __init__(self, *args, **kwargs):
4560
self._smref = None
61+
self._index = None
62+
self._auto_write = True
4663
super(SubmoduleConfigParser, self).__init__(*args, **kwargs)
4764

4865
#{ Interface
@@ -59,7 +76,11 @@ def flush_to_index(self):
5976

6077
sm = self._smref()
6178
if sm is not None:
62-
sm.repo.index.add([sm.k_modules_file])
79+
index = self._index
80+
if index is None:
81+
index = sm.repo.index
82+
# END handle index
83+
index.add([sm.k_modules_file], write=self._auto_write)
6384
sm._clear_cache()
6485
# END handle weakref
6586

@@ -102,6 +123,7 @@ def __init__(self, repo, binsha, mode=None, path=None, name = None, parent_commi
102123
:param url: The url to the remote repository which is the submodule
103124
:param branch: Head instance to checkout when cloning the remote repository"""
104125
super(Submodule, self).__init__(repo, binsha, mode, path)
126+
self.size = 0
105127
if parent_commit is not None:
106128
self._parent_commit = parent_commit
107129
if url is not None:
@@ -113,9 +135,7 @@ def __init__(self, repo, binsha, mode=None, path=None, name = None, parent_commi
113135
self._name = name
114136

115137
def _set_cache_(self, attr):
116-
if attr == 'size':
117-
raise ValueError("Submodules do not have a size as they do not refer to anything in this repository")
118-
elif attr == '_parent_commit':
138+
if attr == '_parent_commit':
119139
# set a default value, which is the root tree of the current head
120140
self._parent_commit = self.repo.commit()
121141
elif attr in ('path', '_url', '_branch'):
@@ -235,8 +255,8 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False):
235255
:note: works atomically, such that no change will be done if the repository
236256
update fails for instance"""
237257
if repo.bare:
238-
raise InvalidGitRepositoryError("Cannot add a submodule to bare repositories")
239-
#END handle bare mode
258+
raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
259+
# END handle bare repos
240260

241261
path = to_native_path_linux(path)
242262
if path.endswith('/'):
@@ -280,7 +300,8 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False):
280300
# END verify url
281301

282302
# update configuration and index
283-
writer = sm.config_writer()
303+
index = sm.repo.index
304+
writer = sm.config_writer(index=index, write=False)
284305
writer.set_value('url', url)
285306
writer.set_value('path', path)
286307

@@ -302,11 +323,10 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False):
302323
pcommit = repo.head.commit
303324
sm._parent_commit = pcommit
304325
sm.binsha = mrepo.head.commit.binsha
305-
repo.index.add([sm], write=True)
326+
index.add([sm], write=True)
306327

307328
return sm
308329

309-
310330
def update(self, recursive=False, init=True, to_latest_revision=False):
311331
"""Update the repository of this submodule to point to the checkout
312332
we point at with the binsha of this instance.
@@ -426,6 +446,85 @@ def update(self, recursive=False, init=True, to_latest_revision=False):
426446

427447
return self
428448

449+
@unbare_repo
450+
def move(self, module_path):
451+
"""Move the submodule to a another module path. This involves physically moving
452+
the repository at our current path, changing the configuration, as well as
453+
adjusting our index entry accordingly.
454+
:param module_path: the path to which to move our module, given as
455+
repository-relative path. Intermediate directories will be created
456+
accordingly. If the path already exists, it must be empty.
457+
Trailling (back)slashes are removed automatically
458+
:return: self
459+
:raise ValueError: if the module path existed and was not empty, or was a file
460+
:note: Currently the method is not atomic, and it could leave the repository
461+
in an inconsistent state if a sub-step fails for some reason
462+
"""
463+
module_path = to_native_path_linux(module_path)
464+
if module_path.endswith('/'):
465+
module_path = module_path[:-1]
466+
# END handle trailing slash
467+
468+
# VERIFY DESTINATION
469+
if module_path == self.path:
470+
return self
471+
#END handle no change
472+
473+
dest_path = join_path_native(self.repo.working_tree_dir, module_path)
474+
if os.path.isfile(dest_path):
475+
raise ValueError("Cannot move repository onto a file: %s" % dest_path)
476+
# END handle target files
477+
478+
# remove existing destination
479+
if os.path.exists(dest_path):
480+
if len(os.listdir(dest_path)):
481+
raise ValueError("Destination module directory was not empty")
482+
#END handle non-emptyness
483+
484+
if os.path.islink(dest_path):
485+
os.remove(dest_path)
486+
else:
487+
os.rmdir(dest_path)
488+
#END handle link
489+
else:
490+
# recreate parent directories
491+
# NOTE: renames() does that now
492+
pass
493+
#END handle existance
494+
495+
# move the module into place if possible
496+
cur_path = self.module_path()
497+
if os.path.exists(cur_path):
498+
os.renames(cur_path, dest_path)
499+
#END move physical module
500+
501+
# NOTE: from now on, we would have to undo the rename !
502+
503+
# rename the index entry - have to manipulate the index directly as
504+
# git-mv cannot be used on submodules ... yeah
505+
index = self.repo.index
506+
try:
507+
ekey = index.entry_key(self.path, 0)
508+
entry = index.entries[ekey]
509+
del(index.entries[ekey])
510+
nentry = git.IndexEntry(entry[:3]+(module_path,)+entry[4:])
511+
ekey = index.entry_key(module_path, 0)
512+
index.entries[ekey] = nentry
513+
except KeyError:
514+
raise ValueError("Submodule's entry at %r did not exist" % (self.path))
515+
#END handle submodule doesn't exist
516+
517+
# update configuration
518+
writer = self.config_writer(index=index) # auto-write
519+
writer.set_value('path', module_path)
520+
self.path = module_path
521+
del(writer)
522+
523+
return self
524+
525+
526+
527+
@unbare_repo
429528
def remove(self, module=True, force=False, configuration=True, dry_run=False):
430529
"""Remove this submodule from the repository. This will remove our entry
431530
from the .gitmodules file and the entry in the .git/config file.
@@ -449,10 +548,6 @@ def remove(self, module=True, force=False, configuration=True, dry_run=False):
449548
:note: doesn't work in bare repositories
450549
:raise InvalidGitRepositoryError: thrown if the repository cannot be deleted
451550
:raise OSError: if directories or files could not be removed"""
452-
if self.repo.bare:
453-
raise InvalidGitRepositoryError("Cannot delete a submodule in bare repository")
454-
# END handle bare mode
455-
456551
if not (module + configuration):
457552
raise ValueError("Need to specify to delete at least the module, or the configuration")
458553
# END handle params
@@ -565,31 +660,37 @@ def set_parent_commit(self, commit, check=True):
565660

566661
return self
567662

568-
def config_writer(self):
663+
@unbare_repo
664+
def config_writer(self, index=None, write=True):
569665
""":return: a config writer instance allowing you to read and write the data
570666
belonging to this submodule into the .gitmodules file.
571667
668+
:param index: if not None, an IndexFile instance which should be written.
669+
defaults to the index of the Submodule's parent repository.
670+
:param write: if True, the index will be written each time a configuration
671+
value changes.
672+
:note: the parameters allow for a more efficient writing of the index,
673+
as you can pass in a modified index on your own, prevent automatic writing,
674+
and write yourself once the whole operation is complete
572675
:raise ValueError: if trying to get a writer on a parent_commit which does not
573676
match the current head commit
574677
:raise IOError: If the .gitmodules file/blob could not be read"""
575-
if self.repo.bare:
576-
raise InvalidGitRepositoryError("Cannot change submodule configuration in a bare repository")
577-
return self._config_parser_constrained(read_only=False)
678+
writer = self._config_parser_constrained(read_only=False)
679+
if index is not None:
680+
writer.config._index = index
681+
writer.config._auto_write = write
682+
return writer
578683

579684
#} END edit interface
580685

581686
#{ Query Interface
582687

688+
@unbare_repo
583689
def module(self):
584690
""":return: Repo instance initialized from the repository at our submodule path
585691
:raise InvalidGitRepositoryError: if a repository was not available. This could
586692
also mean that it was not yet initialized"""
587693
# late import to workaround circular dependencies
588-
589-
if self.repo.bare:
590-
raise InvalidGitRepositoryError("Cannot retrieve module repository in bare parent repositories")
591-
# END handle bare mode
592-
593694
module_path = self.module_path()
594695
try:
595696
repo = git.Repo(module_path)

test/git/test_repo.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,12 @@ def test_submodules(self):
562562

563563
assert isinstance(self.rorepo.submodule("lib/git/ext/gitdb"), Submodule)
564564
self.failUnlessRaises(ValueError, self.rorepo.submodule, "doesn't exist")
565+
566+
@with_rw_repo('HEAD', bare=False)
567+
def test_submodule_update(self, rwrepo):
568+
# fails in bare mode
569+
rwrepo._bare = True
570+
self.failUnlessRaises(InvalidGitRepositoryError, rwrepo.submodule_update)
571+
rwrepo._bare = False
572+
573+

test/git/test_submodule.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def _do_base_tests(self, rwrepo):
3636
assert sm.url == 'git://gitorious.org/git-python/gitdb.git'
3737
assert sm.branch.name == 'master' # its unset in this case
3838
assert sm.parent_commit == rwrepo.head.commit
39-
# size is invalid
40-
self.failUnlessRaises(ValueError, getattr, sm, 'size')
39+
# size is always 0
40+
assert sm.size == 0
4141

4242
# some commits earlier we still have a submodule, but its at a different commit
4343
smold = Submodule.iter_items(rwrepo, self.k_subm_changed).next()
@@ -240,7 +240,7 @@ def _do_base_tests(self, rwrepo):
240240
# add a simple remote repo - trailing slashes are no problem
241241
smid = "newsub"
242242
osmid = "othersub"
243-
nsm = Submodule.add(rwrepo, smid, sm_repopath, new_smclone_path, None, no_checkout=True)
243+
nsm = Submodule.add(rwrepo, smid, sm_repopath, new_smclone_path+"/", None, no_checkout=True)
244244
assert nsm.name == smid
245245
assert nsm.module_exists()
246246
assert nsm.exists()
@@ -261,47 +261,39 @@ def _do_base_tests(self, rwrepo):
261261
rwrepo.index.commit("my submod commit")
262262
assert len(rwrepo.submodules) == 2
263263

264-
# if a submodule's repo has no remotes, it can't be added without an explicit url
265-
osmod = osm.module()
266264
# needs update as the head changed, it thinks its in the history
267265
# of the repo otherwise
266+
nsm._parent_commit = rwrepo.head.commit
268267
osm._parent_commit = rwrepo.head.commit
268+
269+
# MOVE MODULE
270+
#############
271+
# renaming to the same path does nothing
272+
assert nsm.move(sm.path) is nsm
273+
274+
# rename a module
275+
nmp = join_path_native("new", "module", "dir") + "/" # new module path
276+
assert nsm.move(nmp) is nsm
277+
nmp = nmp[:-1] # cut last /
278+
assert nsm.path == nmp
279+
assert rwrepo.submodules[0].path == nmp
280+
281+
282+
# REMOVE 'EM ALL
283+
################
284+
# if a submodule's repo has no remotes, it can't be added without an explicit url
285+
osmod = osm.module()
286+
269287
osm.remove(module=False)
270288
for remote in osmod.remotes:
271289
remote.remove(osmod, remote.name)
272290
assert not osm.exists()
273291
self.failUnlessRaises(ValueError, Submodule.add, rwrepo, osmid, csm_repopath, url=None)
274292
# END handle bare mode
275293

276-
277294
# Error if there is no submodule file here
278295
self.failUnlessRaises(IOError, Submodule._config_parser, rwrepo, rwrepo.commit(self.k_no_subm_tag), True)
279296

280-
# TODO: Handle bare/unbare
281-
# latest submodules write changes into the .gitmodules files
282-
283-
# uncached path/url - retrieves information from .gitmodules file
284-
285-
# index stays up-to-date with the working tree .gitmodules file
286-
287-
# changing the root_tree yields new values when querying them (i.e. cache is cleared)
288-
289-
290-
291-
292-
# set_parent_commit fails if tree has no gitmodule file
293-
294-
295-
296-
if rwrepo.bare:
297-
# module fails
298-
pass
299-
else:
300-
# get the module repository
301-
pass
302-
# END bare handling
303-
304-
# Writing of historical submodule configurations must not work
305297

306298
@with_rw_repo(k_subm_current)
307299
def test_base_rw(self, rwrepo):

0 commit comments

Comments
 (0)