这个是备忘录。原网页(https://medium.com/@porteneuve/mastering-git-subtrees-943d29a798ec ,

http://cncc.bingj.com/cache.aspx?q=master+git+subtree&d=5034897297048421&mkt=zh-CN&setlang=en-US&w=LLr-ePxnq8vxmyPDrHjzRWkbxVPwbcO4)被gfw墙,从cache中复制过来的,以备忘。

 

exploring Git submodules; I told you then our next in-depth article would be about subtrees, which are the main alternative.

 project instead if you want that kind of goodness.

As before, we’ll dive deep and perform every common use-case step by step to illustrate best practices.

I urge you to read it first, if only to be able to contrast and compare both in a useful way, and to grasp the core needs better.

don’t have a choice and must resort to submodules or subtrees instead of a clean, versioned dependency management (which is always better, when doable).

submodules whenever relevant; if you have read the article in question, that’ll help you better internalize these details.

“container.”

Subtree fundamentals

git root).

no special tricks to keep in mind for commands and workflows, it’s business as usual. Ain’t life sweet?

Three approaches: pick one!

There are three technical ways to handle your subtrees; although it’s sometimes possible to mix these approaches, I recommend you pick one and stick with it, at least on a per-repo basis, to avoid trouble.

The manual way

read-tree).

complete freedom in how we manage history (including its graph) and branches…

git subtree contrib script

git subtree and feel like it were a “native” command.

contrib/ directory of your Git install.

--squash…

clutter your graph forever, and I, for one, have a strong distaste for this.

all-or-nothing affair. This contradicts one of the key benefits of subtrees, which is to be able to mix container-specific customizations with general-purpose fixes and enhancements.

One of the key benefits of subtrees is to be able to mix container-specific customizations with general-purpose fixes and enhancements.

Still, it’s been here for a while and has therefore been considerably tested (both in the test suite and battle-testing sense), which is not to be dismissed.

git-subrepo

For a while, we used our own custom solution, named git-stree, that did a reasonable job meeting all our needs, but had a number of dusty corner cases where it would just fall apart. This article used to detail that tool, but starting March 25, 2016 it’s officially deprecated.

git-subrepo. If you want to play with subrepo management in a flexible, well-tested, well-documented and rock-solid way, check it out.

This article won’t demonstrate the git-subrepo approaches just now, but rest assured they work. We may find time for that in the future. In the meantime, their docs and guides are great, give it a spin!

Learn more.

Subtrees, step by step

So, let’s start exploring every common use-case for subtrees in a collaborative project; we’ll detail each of the three approaches, every time.

git-subs directory it creates:

Download the example repos

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 what approach we’re using and which repo we’re into.

git-subs directory as many times as you need (once, or twice) so you can compare the procedures as you go.

Our subtree structure

It’s pretty simple:

.
├── README.md
├── lib
│ └── index.js
└── plugin-config.json

vendor/plugins/demo subfolder.

Adding a subtree

Manually

Let’s start by defining a named remote for our subtree’s central repo, so we don’t clutter our CLIs with its path/URL later:

manually/main (master u=) $ git remote add plugin ../remotes/plugin
manually/main (master u=) $ git fetch plugin
warning: no common commits
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 11 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (11/11), done.
From ../remotes/plugin
* [new branch] master -> plugin/master
manually/main (master u=) $

-u option so the working directory is maintained along with the index.

manually/main (master u=) $ git read-tree \
--prefix=vendor/plugins/demo -u plugin/master
manually/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:   vendor/plugins/demo/README.md
new file: vendor/plugins/demo/lib/index.js
new file: vendor/plugins/demo/plugin-config.json

Awesome. Now let’s finalize that with a commit:

manually/main (master + u=) $ git commit \
-m "Added demo plugin subtree in vendor/plugins/demo"
[master 76b347a] Added demo plugin subtree in vendor/plugins/demo
3 files changed, 19 insertions(+)
create mode 100644 vendor/plugins/demo/README.md
create mode 100644 vendor/plugins/demo/lib/index.js
create mode 100644 vendor/plugins/demo/plugin-config.json
manually/main (master u+1) $

There we are! Nothing too fancy!

With git subtree

add subcommand:

git-subtree/main (master u=) $ git remote add plugin \
../remotes/plugin/
git-subtree/main (master u=) $ git subtree add \
--prefix=vendor/plugins/demo plugin master
git fetch plugin master
warning: no common commits
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 11 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (11/11), done.
From ../remotes/plugin
* branch master -> FETCH_HEAD
* [new branch] master -> plugin/master
Added dir 'vendor/plugins/demo'
git-subtree/main (master u+4) $

merged our plugin’s history with our container’s. Let’s verify that with a log:

git-subtree/main (master u+4) $ git log --oneline --graph \
--decorate
* 32e539d (HEAD, master) Add 'vendor/plugins/demo/' from…
|\
| * fe64799 (plugin/master) Fix repo name for main project…
| * 89d24ad Main files (incl. subdir) for plugin, to populate its…
| * cc88751 Initial commit
* b90985a (origin/master) Main files for the project, to populate…
* e052943 Initial import

git merge --squash produces a squash commit instead of a regular merge, which better matches what we’re after.

Think again:

git-subtree/main (master u+4) $ git reset --hard @{u}
HEAD is now at b90985a Main files for the project, to populate its…
git-subtree/main (master u=) $ git subtree add \
--prefix=vendor/plugins/demo --squash plugin master
git fetch plugin master
From ../remotes/plugin
* branch master -> FETCH_HEAD
Added dir 'vendor/plugins/demo'
git-subtree/main (master u+2) $

u+1? Let’s check:

git-subtree/main (master u+2) $ git log --oneline --graph \
--decorate
* 352af7a (HEAD, master) Merge commit '03e04026fdba2ff1200a226c3…
|\
| * 03e0402 Squashed 'vendor/plugins/demo/' content from commit…
* b90985a (origin/master) Main files for the project, to populate…
* e052943 Initial import

There you have it… Instead of doing a regular squash commit, it squashes the subtree’s history, makes a commit out of it its dedicated “branch” (not an actual branch, but an unnamed, untagged sequence of commits), and merges that.

I don’t like it. I just don’t think it’s worth polluting your graph like that (as you’ll see in later updates, it gets ugly pretty fast).

Grabbing/updating a repo that uses subtrees

what do our colleagues have to do to get these in their local repos?

git submodule update --init --recursive for an existing repo. Ain’t life fun.

just one repo: the container.

With subtrees, cloning/pulling just works.

git push.

git-subtree/main (master u+2) $ git push

manually/main (master u+1) $ git push

you just need a regular clone/pull. This works regardless of your original adding approach, so I’ll just show it once:

manually/main (master u=) $ cd ..
manually $ git clone remotes/main colleague
Cloning into 'colleague'...
done.
manually $ cd colleague
manually/colleague (master u=) $ tree vendor
vendor
└── plugins
└── demo
├── README.md
├── lib
│ └── index.js
└── plugin-config.json
3 directories, 3 files

command instead.)

Getting an update from the subtree’s remote

colleague) in place to collaborate, we’ll switch to a third person’s cap: the one in charge of maintaining the plugin. Let’s hop to it:

manually/colleague (master u=) $ cd ../plugin
manually/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 make two pseudo-commits and publish them on the remote:

manually/plugin (master u=) $ date > fake-work
manually/plugin (master % u=) $ git add fake-work
manually/plugin (master + u=) $ git commit -m "Pseudo-commit #1"
[master 5048a7d] Pseudo-commit #1
1 file changed, 1 insertion(+)
create mode 100644 fake-work
manually/plugin (master u+1) $ date >> fake-work
manually/plugin (master * u+1) $ git commit -am "Pseudo-commit #2"

manually/plugin (master u+2) $ git push

Finally, let’s switch back to our “first developer” cap:

manually/plugin (master u=) $ cd ../main
manually/main (master u=) $

git subtree)…

Let’s now see how we can go about getting these two new commits back in our container’s subtree.

Manually

subtree merge (using a squash commit, too, to avoid merging histories). Most of the time, we won’t even have to specify the subdirectory prefix, Git will figure it out:

manually/main (master u=) $ git fetch plugin
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 ../remotes/plugin
fe64799..dc995bf master -> plugin/master
manually/main (master u=) $ git merge -s subtree --squash \
plugin/master
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
manually/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:   vendor/plugins/demo/fake-work
manually/main (master + u=) $ git commit -m "Updated the plugin"
[master 4f9a839] Updated the plugin
1 file changed, 2 insertions(+)
create mode 100644 vendor/plugins/demo/fake-work
manually/main (master u+1) $

As always, a squash merge doesn’t finalize the commit; it’s quite handy, too, as we may need to adjust other parts of the container code to work properly with the subtree’s updated code. This way we can make a single, working commit.

subtree option. The merge command would then be:

git merge -X subtree=vendor/plugins/demo --squash plugin/master

A tad longer, but handy when default subtree heuristics lose their marbles.

With git subtree

--squash. And we need to repeat the entire settings for every call:

git-subtree/main (master u=) $ git subtree pull \
--prefix=vendor/plugins/demo --squash plugin master
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 ../remotes/plugin
* branch master -> FETCH_HEAD
fe64799..2872e5d master -> plugin/master
Merge made by the 'recursive' strategy.
vendor/plugins/demo/fake-work | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 vendor/plugins/demo/fake-work
git-subtree/main (master u+2) $

still maintain that pseudo-branch and smear the graph:

git-subtree/main (master u+2) $ git log --oneline --graph \
--decorate
* 1782c08 (HEAD, master) Merge commit '636facf64d416210e90bb8b83…
|\
| * 636facf Squashed 'vendor/plugins/demo/' changes from fe64799..…
* | 352af7a (origin/master) Merge commit '03e04026fdba2ff1200a22…
|\ \
| |/
| * 03e0402 Squashed 'vendor/plugins/demo/' content from commit fe…
* b90985a Main files for the project, to populate its tree a bit.
* e052943 Initial import

master branch right now. Clearly reflected by this graph, right? Tsk tsk.

Updating a subtree in-place in the container

backporting it to its remote.

customize the subtree’s code in a container-specific way, without pushing these changes back upstream.

You should be careful to distinguish between both situations, putting each use-case into its own commits.

you don’t have to make two separate commits for it (one for subtree code, one for container code): the commands we’ll use later for backporting can figure the split out, and this will spare you a failing-tests, partly-implemented commit in the container codebase…

enormous advantage over submodules, for which this section would be waaaay longer…

Subtree updates can be freely performed within the container codebase.

Let’s unroll a scenario in which we’ll mix four types of commits:

  • touching only the subtree, intended for backport (e.g. fixes);
  • touching only container code;
  • intended for backport;
  • not to be backported.

main folder of every copy you made (one per approach). Make sure you read the commands’ output and check nothing seems to break, though! You never know…

git push
echo '// Now super fast' >> vendor/plugins/demo/lib/index.js
git ci -am "[To backport] Faster plugin"
date >> main-file-1
git ci -am "Container-only work"
date >> vendor/plugins/demo/fake-work
date >> main-file-2
git ci -am "[To backport] Timestamping (requires container tweaks)"
echo '// Container-specific' >> vendor/plugins/demo/lib/index.js
git ci -am "Container-specific plugin update"

Backporting to the subtree’s remote

Now let’s see how to backport the necessary commits, once for each approach. We’ll start by looking at our recent commits to keep our history fresh in mind:

manually/main (master u+4) $ git log --oneline --decorate --stat -5
28e310b (master) Container-specific plugin update
vendor/plugins/demo/lib/index.js | 1 +
1 file changed, 1 insertion(+)
71d2d12 [To backport] Timestamping (requires container tweaks)
main-file-2 | 1 +
vendor/plugins/demo/fake-work | 1 +
2 files changed, 2 insertions(+)
c693673 Container-only work
main-file-1 | 1 +
1 file changed, 1 insertion(+)
92bc02d [To backport] Faster plugin
vendor/plugins/demo/lib/index.js | 1 +
1 file changed, 1 insertion(+)
4f758af (origin/master) Updated the plugin
vendor/plugins/demo/fake-work | 2 ++
1 file changed, 2 insertions(+)

Manually

We could create synthetic commits in the middle of nowhere, but that’s fugly. I favor creating a local branch specifically for backporting, and have it track the proper remote for our plugin:

manually/main (master u+4) $ git checkout -b backport-plugin \
plugin/master
manually/main (backport-plugin u=) $

-x into the mix so the commit message has extra lines detailing the source for each cherry pick).

manually/main (backport-plugin u=) $ git cherry-pick -x master~3
[backport-plugin 953ec4d] [To backport] Faster plugin
Date: Thu Jan 29 21:54:45 2015 +0100
1 file changed, 1 insertion(+)
manually/main (backport-plugin u+1) $ git cherry-pick -x \
--strategy=subtree master^
[backport-plugin 34f50a4] [To backport] Timestamping (requires con…
Date: Thu Jan 29 21:55:00 2015 +0100
1 file changed, 1 insertion(+)
manually/main (backport-plugin u+1) $ git log --oneline \
--decorate --stat -2
34f50a4 (HEAD, backport-plugin) [To backport] Timestamping (requir…
fake-work | 1 +
1 file changed, 1 insertion(+)
953ec4d [To backport] Faster plugin
lib/index.js | 1 +
1 file changed, 1 insertion(+)
manually/main (backport-plugin u+2) $ git push
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (7/7), 877 bytes | 0 bytes/s, done.
Total 7 (delta 2), reused 0 (delta 0)
To ../remotes/plugin
dc995bf..34f50a4 backport-plugin -> master

we didn’t even have to specify the subtree strategy whenever the heuristics worked out, thanks to non-ambiguous paths in our working directories (the backport branch has different, unprefixed contents).

deleted by us conflict). So you’d better use that specific option all the time, just to be on the safe side.

log above confirms the backported files are put in the “plugin root,” properly unprefixed. And the final push lets us publish that backport to the central remote for the plugin.

With git subtree

it backports every single commit that touched the subtree. You can’t pick the relevant commits. So our last commit, which was container-specific, gets cargoed along… Grmbl. This is not what we want here, but I’ll show you the command anyway:

# BEWARE: NOT WHAT WE WANT. Backports everything, no choice.
git-subtree/main (master u+4) $ git subtree push \
-P vendor/plugins/demo plugin master
git push using: plugin master
-n 1/ 10 (0)
-n 2/ 10 (1)
-n 3/ 10 (2)
-n 4/ 10 (2)
-n 5/ 10 (3)
-n 6/ 10 (3)
-n 7/ 10 (4)
-n 8/ 10 (5)
-n 9/ 10 (6)
-n 10/ 10 (7)
Counting objects: 11, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (11/11), 1.11 KiB | 0 bytes/s, done.
Total 11 (delta 4), reused 0 (delta 0)
To ../remotes/plugin/
2872e5d..e857a74 e857a74119c3e1c1b237b367c4a6c8f79deca1a7 -> m…
git-subtree/main (master u+4) $ git log --oneline --decorate -4 \
plugin/master
e857a74 (plugin/master) Container-specific plugin update
ddabc13 [To backport] Timestamping (requires container tweaks)
73a22ea [To backport] Faster plugin
2872e5d Pseudo-commit #2

Note the latest (top) backport, that we don’t want here…

Removing a subtree

git rm will do, regardless of the approach you used.

main (master u=) $ git rm -r vendor/plugins/demo
rm 'vendor/plugins/demo/README.md'
rm 'vendor/plugins/demo/fake-work'
rm 'vendor/plugins/demo/lib/index.js'
rm 'vendor/plugins/demo/plugin-config.json'
main (master + u=) $ git commit -m "Removing demo subtree"
[master 3893865] Removing demo subtree
4 files changed, 24 deletions(-)
delete mode 100644 vendor/plugins/demo/README.md
delete mode 100644 vendor/plugins/demo/fake-work
delete mode 100644 vendor/plugins/demo/lib/index.js
delete mode 100644 vendor/plugins/demo/plugin-config.json
main (master u+1) $

Turning a directory into a subtree

extract it for sharingbetween multiple codebases.

Let’s start by creating a “local remote” folder. You can copy-paste these:

cd ..
mkdir remotes/myown
cd remotes/myown
git init --bare
cd ../../main

We’ll then perform a series of mixed commits touching (or not) a subdirectory in our codebase. I’ll re-use the earlier commands, but change the directory name.

git subtree, in the matching copy as well:

mkdir -p lib/plugins/myown/lib
echo '// Yo!' > lib/plugins/myown/lib/index.js
git add lib/plugins/myown
git ci -m "Plugin sez: Yo, dawg."
date >> main-file-1
git ci -am "Container-only work"
echo '// Now super fast' > lib/plugins/myown/lib/index.js
date >> main-file-2
git ci -am "Faster plugin (requires container tweaks)"
git push

+n in our prompts in the following examples).

Manually

filter down its history so it only keeps commits that touched the subdirectory, rewriting the tree root as it goes.

--subdirectory-filter option. See for yourself:

manually/main (master u=) $ git checkout -b split-plugin
manually/main (split-plugin) $ git filter-branch \
--subdirectory-filter lib/plugins/myown
Rewrite 973cfacecb645f66b89accedac8780c19140401b (2/2)
Ref 'refs/heads/split-plugin' was rewritten

manually/main (split-plugin) $ git log --oneline --decorate
5af0de1 (HEAD, split-plugin) Faster plugin (requires container twe…
4fc711a Plugin sez: Yo, dawg.

manually/main (split-plugin) $ tree
.
└── lib
└── index.js

1 directory, 1 file

 instead.)

Now we just need to push that to the proper remote:

manually/main (split-plugin) $ git remote add myown \
../remotes/myown
manually/main (split-plugin) $ git push -u myown \
split-plugin:master
Counting objects: 8, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (8/8), 617 bytes | 0 bytes/s, done.
Total 8 (delta 0), reused 0 (delta 0)
To ../remotes/myown
* [new branch] split-plugin -> master
Branch split-plugin set up to track remote branch master from myow…
manually/main (split-plugin u=) $

At this stage, you can kill the backport branch if you think you won’t need it anymore for later backports. Otherwise just let it be…

merge -s subtree --squash calls will work just fine, as if you had injected the contents as a subtree in the first place. Isn’t it handy?

With git subtree

split subcommand intended for about the same thing. Assuming you copy-pasted the series of commit commands from earlier, it would look like this:

git-subtree/main (master u=) $ git subtree split \
-P lib/plugins/myown -b split-plugin
-n 1/ 14 (0)
-n 2/ 14 (1)
-n 3/ 14 (2)
-n 4/ 14 (3)
-n 5/ 14 (4)
-n 6/ 14 (5)
-n 7/ 14 (6)
-n 8/ 14 (7)
-n 9/ 14 (8)
-n 10/ 14 (9)
-n 11/ 14 (10)
-n 12/ 14 (11)
-n 13/ 14 (12)
-n 14/ 14 (13)
Created branch 'split-plugin'
a54c695c65db858a68720dd9b93061ea28d13243

git-subtree/main (master u=) $

You can then repeat the remote-updating commands we had, to publish the final result:

git-subtree/main (split-plugin) $ git remote add myown \
../remotes/myown
git-subtree/main (split-plugin) $ git push -u myown \
split-plugin:master
Counting objects: 8, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (8/8), 556 bytes | 0 bytes/s, done.
Total 8 (delta 1), reused 0 (delta 0)
To ../remotes/myown
* [new branch] split-plugin -> master
Branch split-plugin set up to track remote branch master from myow…
git-subtree/main (split-plugin u=) $

git subtree will refuse later squash pulls, as it doesn’t find any trace of earlier adds, and it doesn’t rely on Git’s builtin heuristics to figure it out, using its own technical implementation instead:

git-subtree/main (split-plugin u=) $ git checkout master
git-subtree/main (master u=) $ git subtree pull --squash \
-P lib/plugins/myown myown master
From ../remotes/myown
* branch master -> FETCH_HEAD
Can't squash-merge: 'lib/plugins/myown' was never added.

Long story short, you either forget about squashes and merge the subtree’s history from now on (ugh!), or replace the legacy subdirectory with a formal subtree addition:

git-subtree/main (master u=) $ git rm -r lib/plugins/myown
rm 'lib/plugins/myown/lib/index.js'

git-subtree/main (master + u=) $ git commit \
-m "Removing lib/plugins/myown for subtree replacement"
[master bf59e62] Removing lib/plugins/myown for subtree replacement
1 file changed, 1 deletion(-)
delete mode 100644 lib/plugins/myown/lib/index.js

git-subtree/main (master u+1) $ git subtree add \
-P lib/plugins/myown --squash myown master
git fetch myown master
From ../remotes/myown
* branch master -> FETCH_HEAD
Added dir 'lib/plugins/myown'

git-subtree/main (master u+3) $

git subtree pull -P lib/plugins/myown --squash myown master will work… Now that’s a nice set of flaming hoops to jump through…

So, which approach should I use?

grok the manual approach: it lets you do what you want, however you want to do it, and therefore devise a series of commands that best fits your strategic choices about branches, commits, backports, etc.

Want to learn more?

a number of Git articles, and you might be particularly interested in the following ones:

upvote it on HN! Thanks a bunch!

let us know!

 

相关文章:

  • 2021-12-29
  • 2022-12-23
  • 2022-01-30
  • 2022-02-21
  • 2021-08-22
  • 2022-12-23
  • 2021-10-28
  • 2021-04-17
猜你喜欢
  • 2022-12-23
  • 2021-11-20
  • 2021-11-21
  • 2021-10-27
  • 2021-07-29
  • 2021-11-27
相关资源
相似解决方案