git submodule 使用
Submodules, step by step
We’ll now explore every step of using submodules in a collaborative project, making sure we highlight default behaviors, traps and available improvements.
In order to facilitate your following along, I’ve put together a few example repos with their “remotes” (actually just directories). You can uncompress the archive wherever you want, then open a shell (or Git
You’ll find three directories in there:
- main acts as the container repo, local to the first collaborator,
- plugin acts as the central maintenance repo for the module, and
- remotes contains the filesystem remotes for the two previous repos.
In the example commands below, the prompt always displays which repo we’re into.
Adding a submodule
Let’s start by adding our plugin as a submodule inside our container (which is in main). The plugin itself has a simple structure:
.
├── README.md
├── lib
│ └── index.js
└── plugin-config.json
So let’s go into main
Because we use paths instead of URLs here for our remotes, we hit a weird, albeit well-known, snag: relative paths for remotes are interpreted relative to our main remote, no to our repo’s root directory. This is super weird, not described anywhere, but I’ve seen it happen every time. So instead of saying ../remotes/plugin, we just say ../plugin.
main (master u=) $ git submodule add ../plugin vendor/plugins/demo
Cloning into 'vendor/plugins/demo'…
done.
main (master + u=) $
This added some settings in our local configuration:
main (master + u=) $ cat .git/config
…
[submodule "vendor/plugins/demo"]
url = ../remotes/plugin
And this also staged two files:
main (master + u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitmodules
new file: vendor/plugins/demo
Huh?! What’s this .gitmodules file? Let’s look at it:
main (master + u=) $ cat .gitmodules
[submodule "vendor/plugins/demo"]
path = vendor/plugins/demo
url = ../plugin
This furiously resembles our local config… So why the duplication? Well, precisely because our local config is… local. Our collaborators won’t see it (which is perfectly normal), so they need a mechanism to get the definitions of all submodules they need to set up in their own repos. This is what .gitmodules is for; it will be read later by the git submodule init command, as we’ll see in a moment.
While we’re on status, note how minimalistic it is when it comes to our submodule: it just goes with an overly generic new file instead of telling us more about what’s going on inside it. Our submodule was indeed injected in the subdirectory:
…
└── vendor
└── plugins
└── demo
├── .git
├── README.md
├── lib
│ └── index.js
└── plugin-config.json
Status, like logs and diffs, is limited to the active repo (right now, the container), not to submodules, which are nested repos. This is often problematic (it’s super easy to miss a regression when limited to this view), so I recommend you set up a submodule-aware status once and for all:
git config --global status.submoduleSummary true
And now:
main (master + u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitmodules
new file: vendor/plugins/demo
Submodule changes to be committed:
* vendor/plugins/demo 0000000...fe64799 (3):
> Fix repo name for main project companion demo repo
Aaaah, this is vastly better. The status extends its base information to add that the submodule present at vendor/plugins/demo got 3 news commits in (as we just created it, it means the remote branch had only three commits), the last one being an addition (note the right-pointing angle bracket >) with a first commit message line that reads “Fix repo name…”.
In order to really bring home that we deal with two separate repos here, let’s get into the submodule’s directory:
main (master + u=) $ cd vendor/plugins/demo
demo (master u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
The active repo has changed, because a new .git takes over: in the current directory (demo, the submodule’s directory), a .git exists indeed, a single file too, not a directory. Let’s look inside:
demo (master u=) $ cat .git
gitdir: ../../../.git/modules/vendor/plugins/demo
Again, since Git 1.7.8, Git does not leave repo directories inside the container’s working directory, but centralizes these in the container’s .git directory (inside .git/modules), and uses a gitdir reference in submodules.
The rationale behind this is simple: it allows the container repo to have submodule-less branches, without having to scrap the submodule’s repo from the working directory and restoring it later.
Naturally, when adding the submodule, you can elect to use a specific branch, or even a specific commit, using the -b CLI option (as per usual, the default is master). Note we’re not, right now, on a detached head, unlike what will happen later: this is because Git checked out master, not a specific SHA1. We would have had to specify a SHA1 to -b to get a detached head from the get-go.
So, back to the container repo, and let’s finalize the submodule’s addition and push that to the remote:
demo (master u=) $ cd -
main (master + u=) $ git commit -m "Ajout submodule plugin demo"
main (master u+1) $ git push
Grabbing a repo that uses submodules
In order to illustrate the issues with collaborating on a repo that uses submodules, we’ll split personalities and act as our colleague, who clones the container’s remote to start working with us. We’ll clone that in a colleague directory, so we can immediately tell which personality cap we have on at any given time.
main (master u=) $ cd ..
git-subs $ git clone remotes/main colleague
Cloning into 'colleague'...
done.
git-subs $ cd colleague
colleague (master u=) $
The first thing to notice is that our submodule is missing from the working directory; only its base directory is here:
vendor
└── plugins
└── demo
How did that happen? This is simply due to the fact that, so far, our new repo (colleague) is not aware of our submodule yet: the information for it is nowhere in its local configuration (check its .git/config if you don’t believe me). We’ll need to fill that in, based on what .gitmodules has to say, which is precisely what git submodule init does:
colleague (master u=) $ git submodule init
Submodule 'vendor/plugins/demo' (/tmp/git-subs/remotes/plugin) registered for path 'vendor/plugins/demo'
Our .git/config is now aware of our submodule. However, we still haven’t fetched it from its remote, to say nothing of having it present in our working directory. And yet, our status shows up as clean!
See, we need to grab the relevant commits manually. It’s not something our initial clone did, we need to do it on every pull. We’ll come back to that in a minute, as this is a behavior clone can actually automate, when properly called.
colleague (master u=) $ git submodule update
Cloning into 'vendor/plugins/demo'...
done.
Submodule path 'vendor/plugins/demo': checked out 'fe6479991d214f4d95ac2ae959d7252a866e01a3'
In practice, when dealing with submodule-using repos, we usually group the two commands (init and update) in one:
colleague (master u=) $ git submodule update --init
It is still a shame that Git has you do all that yourself. Just imagine, on larger FLOSS projects, when submodules have their own submodules, and so forth and so on… This would quickly become a nightmare.
It so happens that Git does provide a CLI option for clone to automatically git submodule update --init recursively right after cloning: the rather aptly-named --recursive option.
So let’s try the whole thing again:
colleague (master u=) $ cd -
git-subs $ rm -fr colleague
git-subs $ git clone --recursive remotes/main colleague
Cloning into 'colleague'...
done.
Submodule 'vendor/plugins/demo' (/tmp/git-subs/remotes/plugin) registered for path 'vendor/plugins/demo'
Cloning into 'vendor/plugins/demo'...
done.
Submodule path 'vendor/plugins/demo': checked out 'fe6479991d214f4d95ac2ae959d7252a866e01a3'
Now that’s better! Note that we’re now on a detached head inside the submodule (as we’ll be from now on):
git-subs $ cd colleague/vendor/plugins/demo
demo ((master)) $
See the double set of parentheses in my prompt, instead of a single set? If your prompt is not configured like mine, to display detached head as describes (with Git’s built-in prompt script, you’d have to define the GIT_PS1_DESCRIBE_STYLE=branch environment variable), you’ll rather see something like this:
demo ((fe64799...)) $
At any rate, status confirms where we’re at:
demo ((master)) $ git status
HEAD detached at fe64799
nothing to commit, working directory clean
Getting an update from the submodule’s remote
OK, now that we have our own repo (main) and our “colleague’s” (colleague) all set up to collaborate, let’s step into the shoes of a third person: the one who maintains the plugin. Here, let’s move to it:
colleague (master u=) $ cd ../plugin
plugin (master u=) $ git log --oneline
fe64799 Fix repo name for main project companion demo repo
89d24ad Main files (incl. subdir) for plugin, to populate its tree.
cc88751 Initial commit
Now, let’s add two pseudo-commits and publish these to the remote:
plugin (master u=) $ date > fake-work
plugin (master % u=) $ git add fake-work
plugin (master + u=) $ git commit -m "Pseudo-commit #1"
[master e6f5bb6] Pseudo-commit #1
1 file changed, 1 insertion(+)
create mode 100644 fake-work
plugin (master u+1) $ date >> fake-work
plugin (master * u+1) $ git commit -am "Pseudo-commit #2"
…
plugin (master u+2) $ git push
Finally, let’s put our “first developer” cap on again:
plugin (master u=) $ cd ../main
main (master u=) $
Suppose we now want to get these two commits inside our submodule. To achieve this, we need to update its local repo, starting by moving into its working directory so it becomes our active repo.
On a side note, I would not recommend using pull for this kind of update. To properly get the updates in the working directory, this command requires that you’re on the proper active branch, which you usually aren’t (you’re on a detached head most of the time). You’d have to start with a checkout of that branch. But more importantly, the remote branch could very well have moved further ahead since the commit you want to set on, and a pull would inject commits you may not want in your local codebase.
Therefore, I recommend splitting the process manually: first git fetch to get all new data from the remote in local cache, then log to verify what you have and checkout on the desired SHA1. In addition to finer-grained control, this approach has the added benefit of working regardless of your current state (active branch or detached head).
main (master u=) $ cd vendor/plugins/demo
demo (master u=) $ git fetch
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From /private/tmp/git-subs/main/../remotes/plugin
fe64799..0e90143 master -> origin/master
demo (master u-2) $ git log --oneline origin/master -10
0e90143 Pseudo-commit #2
e6f5bb6 Pseudo-commit #1
fe64799 Fix repo name for main project companion demo repo
89d24ad Main files (incl. subdir) for plugin, to populate its tree.
cc88751 Initial commit
OK, so we’re good, no extraneous commit. Be that as it may, let’s explicitly set on the one we’re interested in (obviously you have a different SHA1):
demo (master u-2) $ git checkout -q 0e90143
(The -q is only there to spare us Git blabbering about how we’re ending up on a detached head. Usually this would be a healthy reminder, but on this one we know what we’re doing.)
Now that our submodule is updated, we can see the result in the container repo’s status:
demo ((remotes/origin/HEAD)) $ cd -
main (master * u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: vendor/plugins/demo (new commits)
Submodules changed but not updated:
* vendor/plugins/demo fe64799...0e90143 (2):
> Pseudo-commit #2
> Pseudo-commit #1
no changes added to commit (use "git add" and/or "git commit -a")
In the “classical” part of the status, we see a new commits change type, which means the referenced commit changed. Another possibility (which could be compounded to this one) is new contents, which would mean we made local changes to the submodule’s working directory.
The lower part, enabled by our status.submoduleSummary = true setting earlier on, explicitly states the introduced commits (as they use a right-pointing angle bracket >) since our last container commit that had touched the submodule.
In the “terrible default behaviors” family, git diff leaves a lot to be desired:
main (master * u=) $ git diff
diff --git i/vendor/plugins/demo w/vendor/plugins/demo
index fe64799..0e90143 160000
--- i/vendor/plugins/demo
+++ w/vendor/plugins/demo
@@ -1 +1 @@
-Subproject commit fe6479991d214f4d95ac2ae959d7252a866e01a3 +Subproject commit 0e9014309fe6c663e806c9f91297a592ee04cb6c
What the — ? There’s a CLI option that lets us see something more useful:
main (master * u=) $ git diff --submodule=log
Submodule vendor/plugins/demo fe64799..0e90143:
> Pseudo-commit #2
> Pseudo-commit #1
There are no other local changes right now besides the submodule’s referenced commit… Notice this matches almost exactly the lower part of our enhanced git status display.
Having to type that kind of CLI option every time (which, by the way, does not show up in Git’s current completion offers) is rather unwieldy. Fortunately, there is a matching configuration setting:
git config --global diff.submodule log
main (master * u=) $ git diff
Submodule vendor/plugins/demo fe64799..0e90143:
> Pseudo-commit #2
> Pseudo-commit #1
We now only need to perform the container commit that finalizes our submodule’s update. If you had to touch the container’s code to make it work with this update, commit it along, naturally. On the other hand, avoid mixing submodule-related changes and other stuff that would just pertain to the container code: by neatly separating the two, later migrations to other code-reuse approaches are made easier (also, as usual, atomic commits FTW).
As we’re about to grab this submodule update in our colleague’s repo, we’ll push right after committing (which is not a general good practice).
main (master * u=) $ git commit -am "Setting submodule on PC2"
main (master u+1) $ git push
Pulling a submodule-using repo
Click! “Colleague” cap on!
So we’re pulling updates from the container repo’s remote…
colleague (master u=) $ git pull
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From /tmp/git-subs/remotes/main
c995ed0..ac96c22 master -> origin/master
Fetching submodule vendor/plugins/demo
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From /tmp/git-subs/remotes/plugin
fe64799..0e90143 master -> origin/master
Successfully rebased and updated refs/heads/master.
colleague (master * u=) $
(You might not have the “Successfully rebased and updated…” and see a “Merge made by the ‘recursive’ strategy” instead. If so, my heart goes out to you, and you should immediately learn why pulls should rebase).
Note the second half of this display: it’s about the submodule, starting with “Fetching submodule…”.
This behavior became the default with Git 1.7.5, with the configuration setting fetch.recurseSubmodules now defaulting to on-demand: if a container project gets updates to referenced submodule commits, these submodules get fetched automatically. (Remember fetching is the first part of pulling.)
Still, and this is critical: Git auto-fetches, but does not auto-update. Your local cache is up-to-date with the submodule’s remote, but the submodule’s working directory stuck to its former contents. At least, you can shut that laptop, hop onto a plane, and still forge ahead once offline. Although this auto-fetching is limited to already-known submodules: any new ones, not yet copied into local configuration, are not auto-fetched.
Git auto-fetches, but does not auto-update.
The current prompt, with its asterisk (*), does hint at local modifications, because our WD is not in sync with the index, the latter being aware of the newly referenced submodule commits. Check out the status:
colleague (master * u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: vendor/plugins/demo (new commits)
Submodules changed but not updated:
* vendor/plugins/demo 0e90143...fe64799 (2):
< Pseudo-commit #2
< Pseudo-commit #1
no changes added to commit (use "git add" and/or "git commit -a")
Notice how the angle brackets point left (<)? Git sees that the current WD does not have these two commits, contrary to the container project’s expectations.
This is the massive danger: if you don’t explicitly update the submodule’s working directory, your next container commit will regress the submodule. This is a first-order trap.
Is is therefore mandatory that you finalize the update:
colleague (master * u=) $ git submodule update
Submodule path 'vendor/plugins/demo': checked out '0e9014309fe6c663e806c9f91297a592ee04cb6c'
As long as we’re trying to form generic good habits, the preferred command here would be a git submodule update --init --recursive, in order to auto-init any new submodule, and to recursively update these if need be.
There is another edge case: if the submodule’s remote URL changed since last used (perhaps one of the collaborators changed it in the .gitmodules), you have to manually update your local config to match this. In such a situation, before the git submodule update, you’d need to run a git submodule sync.
I should mention, for completeness’ sake, that even if git submodule update defaults to checking out the referenced SHA1, you can change that to, for instance, rebase any local submodule work (we’ll talk about that very soon) on top of it. You’d do that by setting the update configuration setting for your submodule to rebase inside your container’s local configuration.
And I’m sorry but no, there’s no local configuration setting, or even CLI option for that matter, that can auto-update on pull. To automate such things, you’d need to use either aliases, custom scripts, or carefully crafted local hooks. Here’s an example spull alias (single line, split here for display):
git config --global alias.spull '!git pull && git submodule sync --recursive && git submodule update --init --recursive'
If you want to retain the ability to pass custom arguments to git pull, you can either define a function on-the-fly and call it, or go with a custom script. The first approach would look like this (again, single line):
git config --global alias.spull '__git_spull() { git pull "[email protected]" && git submodule sync --recursive && git submodule update --init --recursive; }; __git_spull'
Not very readable, eh? I prefer the custom script approach. Let’s say you’d put a git-spull script file somewhere inside your PATH (I have a ~/perso/bin directory in my PATH just for such things):
#! /bin/bash
git pull "[email protected]" &&
git submodule sync --recursive &&
git submodule update --init --recursive
We then give it execution rights:
chmod +x git-spull
And now we can use it just like we’d have used the alias.
Updating a submodule in-place in the container
This is the hardest use-case, and you should stay away from it as much as possible, preferring maintenance through the central, dedicated repo.
However, it can happen that submodule code cannot be tested, or even compiled, outside container code. Many themes and plugins have such constraints.
The first thing to understand is, because you’re going to make commits, you must start from a proper basis, which will be a branch tip. You therefore need to verify that the branch’s latest commits won’t “break” your container project. If they do, well, creating your own container-specific branch in the submodule sounds tempting, but that path leads to strong coupling between submodule and container, which is not advisable. You may want to stop “submoduling” that code in this particular project, and just embed it like any regular contents instead.
Let’s admit that you can, in good conscience, add to the submodule’s current master branch. Let’s start by syncing our local state on the remote’s:
colleague (master u=) $ cd vendor/plugins/demo
demo ((remotes/origin/HEAD)) $ git checkout master
Previous HEAD position was 0e90143... Pseudo-commit #2
Switched to branch 'master'
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
demo (master u-2) $ git pull --rebase
First, rewinding head to replay your work on top of it...
Fast-forwarded master to 0e9014309fe6c663e806c9f91297a592ee04cb6c.
demo (master u=) $
Another way to go about this would be, from the container repo, to explicitly sync the submodule’s local branch over its tracked remote branch (single line on top, last -- followed by whitespace):
colleague (master u=) $ git submodule update --remote --rebase -- vendor/plugins/demo
…
colleague (master u=) $ cd vendor/plugins/demo
demo (master u=) $
We can now edit the code, make it work, test it, etc. Once we’re all set, we can then perform the two commits and the two necessary pushes (it’s super easy, and in practice all too frequent, to forget some of that).
Let’s simply add fake work and make the two related commits, at the submodule and container levels:
demo (master u=) $ date >> fake-work
demo (master * u=) $ git commit -am "Pseudo-commit #3"
[master 12e3a52] Pseudo-commit #3
1 file changed, 1 insertion(+)
demo (master u+1) $ cd ../../..
colleague (master * u=) git commit -am "Using PC3 on the submodule"
[master ad9da82] Using PC3 on the submodule
1 file changed, 1 insertion(+), 1 deletion(-)
colleague (master u+1) $
At this point, the major danger is forgetting to push the submodule. You get back to the container project, commit it, and only push the container. It’s an easy mistake to make, especially inside an IDE or GUI. When your colleagues try to get updates, all hell breaks loose. Look at the first step:
colleague (master u+1) $ git push
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 355 bytes | 0 bytes/s, done.
Total 4 (delta 1), reused 0 (delta 0)
To /tmp/git-subs/remotes/main
766cd47..ad9da82 master -> master
colleague (master u=) $ cd ../main
main (master u=) $ git pull
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From ../remotes/main
766cd47..ad9da82 master -> origin/master
Fetching submodule vendor/plugins/demo
Successfully rebased and updated refs/heads/master.
main (master * u=) $
There is absolutely no indication that Git could not fetch the referenced commit from the submodule’s remote. The first hint of this is in the status:
main (master * u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: vendor/plugins/demo (new commits)
Submodules changed but not updated:
* vendor/plugins/demo 12e3a52...0e90143:
Warn: vendor/plugins/demo doesn't contain commit 12e3a529698c519b2fab790630f71bd531c45727
no changes added to commit (use "git add" and/or "git commit -a")
Notice the warning: apparently, the newly referenced commit for the submodule is nowhere to be found. Indeed, if we attempt updating the submodule’s working directory, we get:
main (master * u=) $ git submodule update
fatal: reference is not a tree: 12e3a529698c519b2fab790630f71bd531c45727
Unable to checkout '12e3a529698c519b2fab790630f71bd531c45727' in submodule path 'vendor/plugins/demo'
You can plainly see how important it is to remember pushing the submodule too, ideally before pushing the container. Let’s do that in colleague and attempt the update again:
main (master * u=) $ cd ../colleague/vendor/plugins/demo
demo (master u+1) $ git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 329 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To /tmp/git-subs/remotes/plugin
0e90143..12e3a52 master -> master
demo (master u=) $ cd -
main (master * u=) $ git submodule update
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /private/tmp/git-subs/main/../remotes/plugin
0e90143..12e3a52 master -> origin/master
Submodule path 'vendor/plugins/demo': checked out '12e3a529698c519b2fab790630f71bd531c45727'
I should note there is a CLI option that will verify whether currently referenced submodule commits need to be pushed too, and if so will push them: it’s git push --recurse-submodules=on-demand (quite a mouthful, admittedly). It needs to have something container-level to push to work, though: only submodules won’t cut it.
What’s more, [EDIT Jan 14, 2016] (there’s no configuration setting for this, so you’d have to standardize procedures around an alias, e.g. spush:) — starting with Git 2.7.0, there’s now a push.recurseSubmodules configurations setting you can define (to on-demand or check).
git config --global alias.spush 'push --recurse-submodules=on-demand'
Removing a submodule
There are two situations where you’d want to “remove” a submodule:
- You just want to clear the working directory (perhaps before archiving the container’s WD) but want to retain the possibility of restoring it later (so it has to remain in .gitmodules and .git/modules);
- You wish to definitively remove it from the current branch.
Let’s see each case in turn.
Temporarily removing a submodule
The first situation is easily handled by git submodule deinit. See for yourself:
main (master u=) $ git submodule deinit vendor/plugins/demo
Cleared directory 'vendor/plugins/demo'
Submodule 'vendor/plugins/demo' (../remotes/plugin) unregistered for path 'vendor/plugins/demo'
main (master u=) $
This has no impact on container status whatsoever. The submodule is not locally known anymore (it’s gone from .git/config), so its absence from the working directory goes unnoticed. We still have the vendor/plugins/demo directory but it’s empty; we could strip it with no consequence.
The submodule must not have any local modifications when you do this, otherwise you’d need to --force the call.
Any later subcommand of git submodule will blissfully ignore this submodule until you init it again, as the submodule won’t even be in local config. Such commands include update, foreach and sync.
On the other hand, the submodule remains defined in .gitmodules: an init followed by an update (or a single update --init) will restore it as new:
main (master u=) $ git submodule update --init
Submodule 'vendor/plugins/demo' (../remotes/plugin) registered for path 'vendor/plugins/demo'
Submodule path 'vendor/plugins/demo': checked out '12e3a529698c519b2fab790630f71bd531c45727'
main (master u=) $
Permanently removing a submodule
This means you want to get rid of the submodule for good: a regular git rm will do, just like for any other part of the working directory. This will only work if your submodule uses a gitfile (a .git which is a file, not a directory), which is the case starting with Git 1.7.8. Otherwise you’ll have to handle this by hand (I’ll tell you how at the end).
In addition to stripping the submodule from the working directory, the command will update the .gitmodules file so it does not reference the submodule anymore. Here you go:
main (master u=) $ git rm vendor/plugins/demo
rm 'vendor/plugins/demo'
main (master + u=) $ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: .gitmodules
deleted: vendor/plugins/demo
fatal: Not a git repository: 'vendor/plugins/demo/.git'
Submodule changes to be committed:
* vendor/plugins/demo 12e3a52...0000000:
Naturally, advanced status info trip over themselves here, because the gitfile for the submodule is gone (actually, the entire demo directory vanished).
main (master +