Skip to content

Commit 1687283

Browse files
committed
RootModule.update: initial implementation of update method, which should be able to handle submodule removals, additions, path changes and branch changes. All this still needs to be tested though
1 parent 7cc4d74 commit 1687283

File tree

3 files changed

+203
-23
lines changed

3 files changed

+203
-23
lines changed

lib/git/objects/submodule.py

Lines changed: 191 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ def wrapper(self, *args, **kwargs):
4141
wrapper.__name__ = func.__name__
4242
return wrapper
4343

44+
def find_remote_branch(remotes, branch):
45+
"""Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError"""
46+
for remote in remotes:
47+
try:
48+
return remote.refs[branch.name]
49+
except IndexError:
50+
continue
51+
# END exception handling
52+
#END for remote
53+
raise InvalidGitRepositoryError("Didn't find remote branch %r in any of the given remotes", branch
54+
4455
#} END utilities
4556

4657

@@ -375,7 +386,8 @@ def update(self, recursive=False, init=True, to_latest_revision=False):
375386

376387
# see whether we have a valid branch to checkout
377388
try:
378-
remote_branch = mrepo.remotes.origin.refs[self.branch.name]
389+
# find a remote which has our branch - we try to be flexible
390+
remote_branch = find_remote_branch(mrepo.remotes, self.branch)
379391
local_branch = self.branch
380392
if not local_branch.is_valid():
381393
# Setup a tracking configuration - branch doesn't need to
@@ -447,14 +459,18 @@ def update(self, recursive=False, init=True, to_latest_revision=False):
447459
return self
448460

449461
@unbare_repo
450-
def move(self, module_path):
462+
def move(self, module_path, module_only=False):
451463
"""Move the submodule to a another module path. This involves physically moving
452464
the repository at our current path, changing the configuration, as well as
453465
adjusting our index entry accordingly.
454466
:param module_path: the path to which to move our module, given as
455467
repository-relative path. Intermediate directories will be created
456468
accordingly. If the path already exists, it must be empty.
457469
Trailling (back)slashes are removed automatically
470+
:param module_only: if True, only the repository managed by this submodule
471+
will be moved, not the configuration. This will effectively
472+
leave your repository in an inconsistent state unless the configuration
473+
and index already point to the target location.
458474
:return: self
459475
:raise ValueError: if the module path existed and was not empty, or was a file
460476
:note: Currently the method is not atomic, and it could leave the repository
@@ -475,6 +491,13 @@ def move(self, module_path):
475491
raise ValueError("Cannot move repository onto a file: %s" % dest_path)
476492
# END handle target files
477493

494+
index = self.repo.index
495+
tekey = index.entry_key(module_path, 0)
496+
# if the target item already exists, fail
497+
if not module_only and tekey in index.entries:
498+
raise ValueError("Index entry for target path did alredy exist")
499+
#END handle index key already there
500+
478501
# remove existing destination
479502
if os.path.exists(dest_path):
480503
if len(os.listdir(dest_path)):
@@ -502,23 +525,23 @@ def move(self, module_path):
502525

503526
# rename the index entry - have to manipulate the index directly as
504527
# 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)
528+
if not module_only:
529+
try:
530+
ekey = index.entry_key(self.path, 0)
531+
entry = index.entries[ekey]
532+
del(index.entries[ekey])
533+
nentry = git.IndexEntry(entry[:3]+(module_path,)+entry[4:])
534+
index.entries[tekey] = nentry
535+
except KeyError:
536+
raise ValueError("Submodule's entry at %r did not exist" % (self.path))
537+
#END handle submodule doesn't exist
538+
539+
# update configuration
540+
writer = self.config_writer(index=index) # auto-write
541+
writer.set_value('path', module_path)
542+
self.path = module_path
543+
del(writer)
544+
# END handle module_only
522545

523546
return self
524547

@@ -543,6 +566,7 @@ def remove(self, module=True, force=False, configuration=True, dry_run=False):
543566
this flag enables you to safely delete the repository of your submodule.
544567
:param dry_run: if True, we will not actually do anything, but throw the errors
545568
we would usually throw
569+
:return: self
546570
:note: doesn't work in bare repositories
547571
:raise InvalidGitRepositoryError: thrown if the repository cannot be deleted
548572
:raise OSError: if directories or files could not be removed"""
@@ -624,6 +648,8 @@ def remove(self, module=True, force=False, configuration=True, dry_run=False):
624648
self.config_writer().remove_section()
625649
# END delete configuration
626650

651+
return self
652+
627653
def set_parent_commit(self, commit, check=True):
628654
"""Set this instance to use the given commit whose tree is supposed to
629655
contain the .gitmodules blob.
@@ -859,6 +885,152 @@ def _clear_cache(self):
859885
pass
860886

861887
#{ Interface
888+
889+
def update(self, previous_commit=None, recursive=True, force_remove=False, init=True, to_latest_revision=False):
890+
"""Update the submodules of this repository to the current HEAD commit.
891+
This method behaves smartly by determining changes of the path of a submodules
892+
repository, next to changes to the to-be-checked-out commit or the branch to be
893+
checked out. This works if the submodules ID does not change.
894+
Additionally it will detect addition and removal of submodules, which will be handled
895+
gracefully.
896+
897+
:param previous_commit: If set to a commit'ish, the commit we should use
898+
as the previous commit the HEAD pointed to before it was set to the commit it points to now.
899+
If None, it defaults to ORIG_HEAD otherwise, or the parent of the current
900+
commit if it is not given
901+
:param recursive: if True, the children of submodules will be updated as well
902+
using the same technique
903+
:param force_remove: If submodules have been deleted, they will be forcibly removed.
904+
Otherwise the update may fail if a submodule's repository cannot be deleted as
905+
changes have been made to it (see Submodule.update() for more information)
906+
:param init: If we encounter a new module which would need to be initialized, then do it.
907+
:param to_latest_revision: If True, instead of checking out the revision pointed to
908+
by this submodule's sha, the checked out tracking branch will be merged with the
909+
newest remote branch fetched from the repository's origin"""
910+
if self.repo.bare:
911+
raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
912+
# END handle bare
913+
914+
repo = self.repo
915+
916+
# HANDLE COMMITS
917+
##################
918+
cur_commit = repo.head.commit
919+
if previous_commit is None:
920+
symref = SymbolicReference(repo, SymbolicReference.to_full_path('ORIG_HEAD'))
921+
try:
922+
previous_commit = symref.commit
923+
except Exception:
924+
pcommits = cur_commit.parents
925+
if pcommits:
926+
previous_commit = pcommits[0]
927+
else:
928+
# in this special case, we just diff against ourselve, which
929+
# means exactly no change
930+
previous_commit = cur_commit
931+
# END handle initial commit
932+
# END no ORIG_HEAD
933+
else:
934+
previous_commit = repo.commit(previous_commit) # obtain commit object
935+
# END handle previous commit
936+
937+
938+
# HANDLE REMOVALS
939+
psms = type(self).list_items(repo, parent_commit=previous_commit)
940+
sms = self.children()
941+
spsms = set(psms)
942+
ssms = set(sms)
943+
944+
# HANDLE REMOVALS
945+
###################
946+
for rsm in (spsms - ssms):
947+
# fake it into thinking its at the current commit to allow deletion
948+
# of previous module. Trigger the cache to be updated before that
949+
#rsm.url
950+
rsm._parent_commit = repo.head.commit
951+
rsm.remove(configuration=False, module=True, force=force_remove)
952+
# END for each removed submodule
953+
954+
# HANDLE PATH RENAMES + url changes + branch changes
955+
for csm in (spsms & ssms):
956+
psm = psms[csm.name]
957+
sm = sms[csm.name]
958+
959+
if sm.path != psm.path and psm.module_exists():
960+
# move the module to the new path
961+
psm.move(sm.path, module_only=True)
962+
# END handle path changes
963+
964+
if sm.module_exists():
965+
# handle url change
966+
if sm.url != psm.url:
967+
# Add the new remote, remove the old one
968+
# This way, if the url just changes, the commits will not
969+
# have to be re-retrieved
970+
nn = '__new_origin__'
971+
smm = sm.module()
972+
rmts = smm.remotes
973+
assert nn not in rmts
974+
smr = smm.create_remote(nn, sm.url)
975+
srm.fetch()
976+
977+
# now delete the changed one
978+
orig_name = None
979+
for remote in rmts:
980+
if remote.url == psm.url:
981+
orig_name = remote.name
982+
smm.delete_remote(remote)
983+
break
984+
# END if urls match
985+
# END for each remote
986+
987+
# rename the new remote back to what it was
988+
# if we have not found any remote with the original url
989+
# we may not have a name. This is a special case,
990+
# and its okay to fail her
991+
assert orig_name is not None, "Couldn't find original remote-repo at url %r" % psm.url
992+
smr.rename(orig_name)
993+
# END handle url
994+
995+
if sm.branch != psm.branch:
996+
# finally, create a new tracking branch which tracks the
997+
# new remote branch
998+
smm = sm.module()
999+
smmr = smm.remotes
1000+
tbr = git.Head.create(smm, sm.branch.name)
1001+
tbr.set_tracking_branch(find_remote_branch(smmr, sm.branch))
1002+
1003+
# figure out whether the previous tracking branch contains
1004+
# new commits compared to the other one, if not we can
1005+
# delete it.
1006+
try:
1007+
tbr = find_remote_branch(smmr, psm.branch)
1008+
if len(smm.git.cherry(tbr, psm.branch)) == 0:
1009+
psm.branch.delete(smm, psm.branch)
1010+
#END delete original tracking branch if there are no changes
1011+
except InvalidGitRepositoryError:
1012+
# ignore it if the previous branch couldn't be found in the
1013+
# current remotes, this just means we can't handle it
1014+
pass
1015+
# END exception handling
1016+
#END handle branch
1017+
#END handle
1018+
# END for each common submodule
1019+
1020+
# FINALLY UPDATE ALL ACTUAL SUBMODULES
1021+
##########################################
1022+
for sm in sms:
1023+
sm.update(recursive=True, init=init, to_latest_revision=to_latest_revision)
1024+
1025+
# update recursively depth first - question is which inconsitent
1026+
# state will be better in case it fails somewhere. Defective branch
1027+
# or defective depth
1028+
if recursive:
1029+
type(cls)(sm.module()).update(recursive=True, force_remove=force_remove,
1030+
init=init, to_latest_revision=to_latest_revision)
1031+
#END handle recursive
1032+
# END for each submodule to update
1033+
8621034
def module(self):
8631035
""":return: the actual repository containing the submodules"""
8641036
return self.repo

lib/git/util.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,14 @@ def __getitem__(self, index):
316316
return getattr(self, index)
317317
except AttributeError:
318318
raise IndexError( "No item found with id %r" % (self._prefix + index) )
319+
320+
def __contains__(self, item):
321+
try:
322+
self[item]
323+
return True
324+
except IndexError:
325+
return False
326+
# END handle exception
319327

320328

321329
class Iterable(object):

test/git/test_submodule.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def _do_base_tests(self, rwrepo):
202202
sm.module().index.reset(working_tree=True)
203203

204204
# this would work
205-
sm.remove(dry_run=True)
205+
assert sm.remove(dry_run=True) is sm
206206
assert sm.module_exists()
207207
sm.remove(force=True, dry_run=True)
208208
assert sm.module_exists()
@@ -213,7 +213,7 @@ def _do_base_tests(self, rwrepo):
213213
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
214214

215215
# forcibly delete the child repository
216-
csm.remove(force=True)
216+
assert csm.remove(force=True) is csm
217217
assert not csm.exists()
218218
assert not csm.module_exists()
219219
assert len(sm.children()) == 0
@@ -263,8 +263,8 @@ def _do_base_tests(self, rwrepo):
263263

264264
# needs update as the head changed, it thinks its in the history
265265
# of the repo otherwise
266-
nsm._parent_commit = rwrepo.head.commit
267-
osm._parent_commit = rwrepo.head.commit
266+
nsm.set_parent_commit(rwrepo.head.commit)
267+
osm.set_parent_commit(rwrepo.head.commit)
268268

269269
# MOVE MODULE
270270
#############

0 commit comments

Comments
 (0)