Amomum
Amomum

Reputation: 6473

Git subtree: move subtree to a different directory and pull it

Let's say I created a subtree like this:

git subtree --add --prefix=subdir <path_to_remote> <remote_branch> --squash

Then I wanted to move/rename subdir, so I do this: git mv subdir dir2/subdir

Now when I try to pull that subtree to a new prefix:

git subtree --pull --prefix=dir2/subdir <path_to_remote> <remote_branch> --squash

git says:

Can't squash-merge: 'dir2/subdir' was never added.

How can I do it properly?

Upvotes: 6

Views: 4475

Answers (3)

vrqq
vrqq

Reputation: 566

Rerun split with --rejoin argument to let git-subtree generate a commit with metadata.

If you already have commit in subtree, please don't forget to change system time before rerun split, otherwise you may have wrong timeline in main branch.

# change current date before the new commit in subtree branch
date -s MMDDYYYY
date -s HHMMSS

# re-split into local branch
git subtree split -P <dir> -b subtree/<dir> --rejoin

# here we can show commit log of main branch, the commit has been generated like below
# Split '<dir>' into commit '<commit_id>'
# git-subtree-dir: <dir>
# git-subtree-mainline: '....'
# git-subtree-split: <commit_id>

git log

# pull the latest subtree from remote into local main branch
# (If you have some untracked folder, move out them before pull)
git subtree pull -P <dir> [email protected]:username/remote_repo.git the_latest_branch_of_subtree

# Here we can see a merge commit after pull success
git log

# Do not forget to resume OS datatime
chronyc makestep

# Show detail
chronyc tracking

Upvotes: 0

ychin
ychin

Reputation: 71

As the other answer points out, the issue here is that git-subtree embeds the folder name into the commit, and refuses to acknowledge that you have renamed it. That answer suggests doing a split/rejoin which I think could end up making your commit history a little bloated and complicated.

A much simpler way is to make a simple commit to teach git-subtree to do the right thing.

  1. Find the last commit that git-subtree did a squashed merge. I don't think git-subtree provides a native tool to do so, but one way to do it is by manually searching for it (assuming your folder is called "basedir"):

    git log -1 --grep "git-subtree-dir: basedir"
    

    It probably looks something like this in the end of the commit:

    git-subtree-dir: basedir
    git-subtree-split: 607efa887537d5544b3a99e2a0a2dd64abcc72ee
    

    You want to note the value of git-subtree-split: in the commit message. Let's say it is 607efa887537d5544b3a99e2a0a2dd64abcc72ee.

  2. Rename the folder like you were doing: git mv basedir basedir2 && git commit

  3. Now, make an empty commit by doing the following (assuming you want to rename it to "basedir"):

    git commit --allow-empty
    

    Use the following commit message (you can customize it, of course):

    Reassign subtree dir from basedir to basedir2
    
    git-subtree-dir: basedir2
    git-subtree-split: 607efa887537d5544b3a99e2a0a2dd64abcc72ee
    

    Note that the hash 607efa887537d5544b3a99e2a0a2dd64abcc72ee is the exact same one as the one before.

You are now good to go! You basically didn't do anything other than making an empty commit message and manually renamed git-subtree-dir from "basedir" to "basedir2". Git-subtree is kind of a simple tool in a way and will simply use that to remember what the subdir is. I feel like it should really include this as part of the tool instead of users needing to manually construct such a commit, but it works regardless (at least it worked when I was testing it out).

Upvotes: 2

FRob
FRob

Reputation: 4041

The git subtree command knows about your subtree, because it stores the name in the first commit when you add a subtree:

Add 'subdir/' from commit 'c7fbc973614eced220dcef1663d43dad90676e00'

git-subtree-dir: subdir
git-subtree-mainline: c166dc69165f6678d3c409df344c4ed9577a2e11
git-subtree-split: c7fbc973614eced220dcef1663d43dad90676e00

git subtree pull with the --squash option looks for a commit containing git-subtree-dir to find the most-recent commit from the remote repository and hence the point from which to apply and squash all commits.

In many cases a git subtree split --rejoin operation will be successful:

$ git subtree split --rejoin --prefix=dir2/subdir HEAD
Merge made by the 'ours' strategy.
25070c483647f8136655d0e0c6c3d62f469177aa

The resulting commit looks like:

Split 'dir2/subdir/' into commit '25070c483647f8136655d0e0c6c3d62f469177aa'

git-subtree-dir: dir2/subdir
git-subtree-mainline: 59cc3c770e78dbc30bdfe36a6b4e14ce83b38f6c
git-subtree-split: 25070c483647f8136655d0e0c6c3d62f469177aa

This commit will be found and the next git subtree pull --squash will succeed in most cases. Be aware that sometimes subtree operations fail and leave you with a branch of the subtree in your working copy of your repo. Make sure you delete any residual temporary branches to start with a clean slate.

Sometimes the above operation does not succeed, but I never found the reason why. In those cases you can rebase to the commit that added the subtree and change the directory name manually by amending the commit message. This operation will corrupt your whole history for everybody else, though.

Upvotes: 12

Related Questions