rm: delete .gitmodules entry of submodules removed from the work tree
Currently using "git rm" on a submodule removes the submodule's work tree from that of the superproject and the gitlink from the index. But the submodule's section in .gitmodules is left untouched, which is a leftover of the now removed submodule and might irritate users (as opposed to the setting in .git/config, this must stay as a reminder that the user showed interest in this submodule so it will be repopulated later when an older commit is checked out).
Let "git rm" help the user by not only removing the submodule from the work tree but by also removing the "submodule.<submodule name>" section from the .gitmodules file and stage both. This doesn't happen when the "--cached" option is used, as it would modify the work tree. This also silently does nothing when no .gitmodules file is found and only issues a warning when it doesn't have a section for this submodule. This is because the user might just use plain gitlinks without the .gitmodules file or has already removed the section by hand before issuing the "git rm" command (in which case the warning reminds him that rm would have done that for him). Only when .gitmodules is found and contains merge conflicts the rm command will fail and tell the user to resolve the conflict before trying again.
In t7610 three uses of "git rm submod" had to be replaced with "git rm --cached submod" because that test expects .gitmodules and the work tree to stay untouched. Also in t7400 the tests for the remaining settings in the .gitmodules file had to be changed to assert that these settings are missing.
---
This patch applies on top of my mv-submodules series as it reuses the stage_updated_gitmodules() function introduced there. This change was part of my initial "git rm for submodules" series ($gmane/201015), but was not accepted at that time. Since then "git submodule" learned the deinit command which allows the user to state he doesn't want a checked out submodule anymore (which is different from committing the removal of a submodule now possible using "git rm"). The mv-submodules series currently in pu doesn't make much sense without changing the config data in .gitmodules, so maybe it's time to let "git rm" affect the .gitmodules file accordingly.
The .gitmodules file is the source of the path <=> name mapping needed to make the submodule's GIT_DIR path independent, which was the point of moving that to the .git/modules/<name> of the superproject. Other git-core commands like fetch need that information to be up-to-date so they're able to fetch commits for renamed submodules too, so I believe it's time to make git-core not only parse the .gitmodules file, but to also manipulate it where appropriate.
builtin/rm.c | 14 +++++++-- submodule.c | 31 ++++++++++++++++++++ submodule.h | 1 + t/t3600-rm.sh | 72 ++++++++++++++++++++++++++++++++++++++++++---- t/t7400-submodule-basic.sh | 14 +++------ t/t7610-mergetool.sh | 6 ++-- 6 files changed, 117 insertions(+), 21 deletions(-)
builtin/rm.c +12 -2
@@ -219,6 +219,7 @@ int cmd_rm(int argc, const char **argv, const char *prefix)const char **pathspec;char *seen; +gitmodules_config ();git_config (git_default_config,NULL ); argc = parse_options(argc, argv, prefix, builtin_rm_options,
@@ -334,13 +335,15 @@ int cmd_rm(int argc, const char **argv, const char *prefix)* in the middle) */ if (!index_only) { -int removed =0 ; +int removed =0 , gitmodules_modified =0 ;for (i =0 ; i < list.nr; i++) {const char *path = list.entry[i].name;if (list.entry[i].is_submodule) {if (is_empty_dir (path)) {if (!rmdir (path)) { removed =1 ; +if (!remove_path_from_gitmodules (path)) + gitmodules_modified =1 ;continue ; } }else {
@@ -348,9 +351,14 @@ int cmd_rm(int argc, const char **argv, const char *prefix)strbuf_addstr (&buf, path);if (!remove_dir_recursively (&buf,0 )) { removed =1 ; +if (!remove_path_from_gitmodules (path)) + gitmodules_modified =1 ;strbuf_release (&buf);continue ; - } + }else if (!file_exists (path)) +/* Submodule was removed by user */ +if (!remove_path_from_gitmodules (path)) + gitmodules_modified =1 ;strbuf_release (&buf);/* Fallthrough and let remove_path() fail. */ }
@@ -362,6 +370,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)if (!removed)die_errno ("git rm: '%s'" , path); } +if (gitmodules_modified) +stage_updated_gitmodules (); }if (active_cache_changed) {
submodule.c +31 -0
@@ -63,6 +63,37 @@ int update_path_in_gitmodules(const char *oldpath, const char *newpath)return 0 ; } +/* +* Try to remove the "submodule.<name>" section from .gitmodules where the +* given path is configured. +*/ +int remove_path_from_gitmodules (const char *path) +{ +struct strbuf sect =STRBUF_INIT ; +struct string_list_item *path_option; + +if (!file_exists (".gitmodules" ))/* Do nothing whithout .gitmodules */ +return -1 ; + +if (gitmodules_is_unmerged) +die (_ ("Cannot change unmerged .gitmodules, resolve merge conflicts first" )); + + path_option =unsorted_string_list_lookup (&config_name_for_path, path); +if (!path_option) { +warning (_ ("Could not find section in .gitmodules where path=%s" ), path); +return -1 ; + } +strbuf_addstr (§,"submodule." ); +strbuf_addstr (§, path_option->util); +if (git_config_rename_section_in_file (".gitmodules" , sect.buf,NULL ) <0 ) { +/* Maybe the user already did that, don't error out here */ +warning (_ ("Could not remove .gitmodules entry for %s" ), path); +return -1 ; + } +strbuf_release (§); +return 0 ; +} +void stage_updated_gitmodules (void ) {struct strbuf buf =STRBUF_INIT ;
submodule.h +1 -0
@@ -12,6 +12,7 @@ enum { };int update_path_in_gitmodules (const char *oldpath,const char *newpath); +int remove_path_from_gitmodules (const char *path);void stage_updated_gitmodules (void );void set_diffopt_flags_from_submodule_config (struct diff_options *diffopt,const char *path);
t/t3600-rm.sh +66 -6
121 lines not on this page · raw message
t/t7400-submodule-basic.sh +4 -10
@@ -708,13 +708,11 @@ test_expect_success 'submodule add --name allows to replace a submodule with ano test_cmp expect .git ) && echo "repo" >expect && - git config -f .gitmodules submodule.repo.path >actual && - test_cmp expect actual && + test_must_fail git config -f .gitmodules submodule.repo.path && git config -f .gitmodules submodule.repo_new.path >actual && test_cmp expect actual&& echo "$submodurl/repo" >expect && - git config -f .gitmodules submodule.repo.url >actual && - test_cmp expect actual && + test_must_fail git config -f .gitmodules submodule.repo.url && echo "$submodurl/bare.git" >expect && git config -f .gitmodules submodule.repo_new.url >actual && test_cmp expect actual &&
@@ -734,12 +732,8 @@ test_expect_success 'submodule add with an existing name fails unless forced' ' git rm repo && test_must_fail git submodule add -q --name repo_new "$submodurl/repo.git" repo && test ! -d repo && - echo "repo" >expect && - git config -f .gitmodules submodule.repo_new.path >actual && - test_cmp expect actual&& - echo "$submodurl/bare.git" >expect && - git config -f .gitmodules submodule.repo_new.url >actual && - test_cmp expect actual && + test_must_fail git config -f .gitmodules submodule.repo_new.path && + test_must_fail git config -f .gitmodules submodule.repo_new.url && echo "$submodurl/bare.git" >expect && git config submodule.repo_new.url >actual && test_cmp expect actual &&
t/t7610-mergetool.sh +3 -3
@@ -253,7 +253,7 @@ test_expect_success 'deleted vs modified submodule' ' git checkout -b test6 branch1 && git submodule update -N && mv submod submod-movedaside && - git rm submod && + git rm --cached submod && git commit -m "Submodule deleted from branch" && git checkout -b test6.a test6 && test_must_fail git merge master &&
@@ -322,7 +322,7 @@ test_expect_success 'file vs modified submodule' ' git checkout -b test7 branch1 && git submodule update -N && mv submod submod-movedaside && - git rm submod && + git rm --cached submod && echo not a submodule >submod && git add submod && git commit -m "Submodule path becomes file" &&
@@ -453,7 +453,7 @@ test_expect_success 'submodule in subdirectory' ' test_expect_success 'directory vs modified submodule' ' git checkout -b test11 branch1 && mv submod submod-movedaside && - git rm submod && + git rm --cached submod && mkdir submod && echo not a submodule >submod/file16 && git add submod/file16 &&
Signature · 1 line
1.8.2.1.346.gec071ca
Quoted 7 lines · Jens Lehmann
builtin/rm.c | 14 +++++++-- submodule.c | 31 ++++++++++++++++++++ submodule.h | 1 + t/t3600-rm.sh | 72 ++++++++++++++++++++++++++++++++++++++++++---- t/t7400-submodule-basic.sh | 14 +++------ t/t7610-mergetool.sh | 6 ++-- 6 files changed, 117 insertions(+), 21 deletions(-)
Should be very similar to your mv series, as it's essentially the same challenge.
Quoted 9 lines
submodule.c +1
-0@@ -63,6 +63,37 @@ int update_path_in_gitmodules(const char *oldpath, const char *newpath)return 0 ; } +/*
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 76
Quoted 10 lines
submodule.c +10
-0+* Try to remove the "submodule.<name>" section from .gitmodules where the +* given path is configured. +*/ +int remove_path_from_gitmodules (const char *path) +{ +struct strbuf sect =STRBUF_INIT ; +struct string_list_item *path_option; + +if (!file_exists (".gitmodules" ))/* Do nothing whithout .gitmodules */ +return -1 ;
- Why are you doing this here? Is there no initialization function?
- Why are you hard-coding ".gitmodules" instead of using a simple #define?
- Why are you returning -1, instead of an error() with a message?
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 79
Quoted 2 lines
submodule.c +2
-0+if (gitmodules_is_unmerged) +die (_ ("Cannot change unmerged .gitmodules, resolve merge conflicts first" ));
Again, no sanity-checking initialization code?
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 85
Quoted 5 lines
submodule.c +5
-0+ path_option =unsorted_string_list_lookup (&config_name_for_path, path); +if (!path_option) { +warning (_ ("Could not find section in .gitmodules where path=%s" ), path); +return -1 ; + }
Repetition from your mv series. Why copy-paste, when you can factor it out into a function?
Why are you calling warning() and then returning -1? (does return warning() not work?) How is it a warning if you just stop all processing and return?
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 87
Quoted 2 lines
submodule.c +2
-0+strbuf_addstr (§,"submodule." ); +strbuf_addstr (§, path_option->util);
What do you have against strbuf_addf()? I noticed a lot of ugly addstr() calls in your mv series also.
Why is your variable named "sect"? Did you mean "section"?
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 88
Quoted 1 line
submodule.c +1
-0+if (git_config_rename_section_in_file (".gitmodules" , sect.buf,NULL ) <0 ) {
You hardcoded ".gitmodules" again!
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 91
Quoted 3 lines
submodule.c +3
-0+/* Maybe the user already did that, don't error out here */ +warning (_ ("Could not remove .gitmodules entry for %s" ), path); +return -1 ;
Maybe the user already did what? What happens if she didn't do "it" and failure is due to some other cause?
Thanks.
Hi,
Thanks for looking it over.
Quoted 1 line · Ramkumar Ramachandra
- Why are you hard-coding ".gitmodules" instead of using a simple #define?
Advantage of ".gitmodules": it's obvious what it means. Advantage of DOT_GITMODULES: protection against spelling errors.
Git has a lot of use of both styles of string constant, for better or worse. Consistency means following what the surrounding code does, and making changes if appropriate in a separate patch.
Quoted 1 line
- Why are you returning -1, instead of an
error()with a message?
I think the idea is that remove_path_from_gitmodules() is idempotent:
if that path isn't listed in gitmodules, that's considered fine and .gitmodules is left alone, instead of making a user that tries to first remove a .gitmodules file and then all submodules suffer.
Perhaps a return value of '0 if gitmodules unmodified, 1 if modified' would make it clearer that this isn't an error condition.
[...]
Quoted 8 lines
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 85
Quoted 5 lines
submodule.c +5
-0+ path_option =unsorted_string_list_lookup (&config_name_for_path, path); +if (!path_option) { +warning (_ ("Could not find section in .gitmodules where path=%s" ), path); +return -1 ; + }Repetition from your mv series. Why copy-paste, when you can factor it out into a function?
Do you mean that update_path_in_gitmodules should treat newpath == NULL as a cue to remove that entry, or something similar?
Quoted 1 line
Why are you calling
warning()and then returning -1?
Sure, "return warning(...)" is a good shortcut.
Quoted 2 lines
warning()not work?) How is it a warning if you just stop all processing and return?
Probably it shouldn't warn in this case.
Quoted 4 lines
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 87
Quoted 2 lines
submodule.c +2
-0+strbuf_addstr (§,"submodule." ); +strbuf_addstr (§, path_option->util);What do you have against
strbuf_addf()?
I think both addf and addstr are pretty clear. The implementation of addf is more complicated, which can be relevant in performance-critical code (not here).
Quoted 1 line
Why is your variable named "sect"? Did you mean "section"?
I think both "sect" and "section" are pretty clear.
[...]
Quoted 6 lines
submodule.c int update_path_in_gitmodules(const char *oldpath, const char *newpath) · line 91
Quoted 3 lines
submodule.c +3
-0+/* Maybe the user already did that, don't error out here */ +warning (_ ("Could not remove .gitmodules entry for %s" ), path); +return -1 ;Maybe the user already did what? What happens if she didn't do "it" and failure is due to some other cause?
git_config_rename_section_in_file() can fail for the following reasons:
* invalid new section name (NULL is valid, so doesn't apply here)
* could not lock config file
* write error
* could not commit config file
If the old section is missing, it doesn't even fail (it just returns 0). So I agree: this should be an error instead of a warning.
Hope that helps,
Jonathan