I would like to rename/move a project subtree in Git moving it from
/project/xyz
to
/components/xyz
If I
First create a standalone commit with just a rename.
Then any eventual changes to the file content put in the separate commit.
I followed this multi-step process to move code to the parent directory and retained history.
Step 0: Created a branch 'history' from 'master' for safekeeping
Step 1: Used git-filter-repo tool to rewrite history. This command below moved folder 'FolderwithContentOfInterest' to one level up and modified the relevant commit history
git filter-repo --path-rename ParentFolder/FolderwithContentOfInterest/:FolderwithContentOfInterest/ --force
Step 2: By this time the GitHub repository lost its remote repository path. Added remote reference
git remote add origin git@github.com:MyCompany/MyRepo.git
Step 3: Pull information on repository
git pull
Step 4: Connect the local lost branch with the origin branch
git branch --set-upstream-to=origin/history history
Step 5: Address merge conflict for the folder structure if prompted
Step 6: Push!!
git push
Note: The modified history and moved folder appear to already be committed. enter code here
Done. Code moves to the parent / desired directory keeping history intact!
While the core of Git, the Git plumbing doesn't keep track of renames, the history you display with the Git log "porcelain" can detect them if you like.
For a given git log
use the -M option:
git log -p -M
With a current version of Git.
This works for other commands like git diff
as well.
There are options to make the comparisons more or less rigorous. If you rename a file without making significant changes to the file at the same time it makes it easier for Git log and friends to detect the rename. For this reason some people rename files in one commit and change them in another.
There's a cost in CPU use whenever you ask Git to find where files have been renamed, so whether you use it or not, and when, is up to you.
If you would like to always have your history reported with rename detection in a particular repository you can use:
git config diff.renames 1
Files moving from one directory to another is detected. Here's an example:
commit c3ee8dfb01e357eba1ab18003be1490a46325992
Author: John S. Gruber <JohnSGruber@gmail.com>
Date: Wed Feb 22 22:20:19 2017 -0500
test rename again
diff --git a/yyy/power.py b/zzz/power.py
similarity index 100%
rename from yyy/power.py
rename to zzz/power.py
commit ae181377154eca800832087500c258a20c95d1c3
Author: John S. Gruber <JohnSGruber@gmail.com>
Date: Wed Feb 22 22:19:17 2017 -0500
rename test
diff --git a/power.py b/yyy/power.py
similarity index 100%
rename from power.py
rename to yyy/power.py
Please note that this works whenever you are using diff, not just with git log
. For example:
$ git diff HEAD c3ee8df
diff --git a/power.py b/zzz/power.py
similarity index 100%
rename from power.py
rename to zzz/power.py
As a trial I made a small change in one file in a feature branch and committed it and then in the master branch I renamed the file, committed, and then made a small change in another part of the file and committed that. When I went to feature branch and merged from master the merge renamed the file and merged the changes. Here's the output from the merge:
$ git merge -v master
Auto-merging single
Merge made by the 'recursive' strategy.
one => single | 4 ++++
1 file changed, 4 insertions(+)
rename one => single (67%)
The result was a working directory with the file renamed and both text changes made. So it's possible for Git to do the right thing despite the fact that it doesn't explicitly track renames.
This is an late answer to an old question so the other answers may have been correct for the Git version at the time.
I have faced the issue "Renaming the folder without loosing history". To fix it, run:
$ git mv oldfolder temp && git mv temp newfolder
$ git commit
$ git push
Simply move the file and stage with:
git add .
Before commit you can check the status:
git status
That will show:
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: old-folder/file.txt -> new-folder/file.txt
I tested with Git version 2.26.1.
Extracted from GitHub Help Page.
git log --pretty=email
Example: Extract history of file3
, file4
and file5
my_repo
├── dirA
│ ├── file1
│ └── file2
├── dirB ^
│ ├── subdir | To be moved
│ │ ├── file3 | with history
│ │ └── file4 |
│ └── file5 v
└── dirC
├── file6
└── file7
Set/clean the destination
export historydir=/tmp/mail/dir # Absolute path
rm -rf "$historydir" # Caution when cleaning the folder
Extract history of each file in email format
cd my_repo/dirB
find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';'
Unfortunately option --follow
or --find-copies-harder
cannot be combined with --reverse
. This is why history is cut when file is renamed (or when a parent directory is renamed).
Temporary history in email format:
/tmp/mail/dir
├── subdir
│ ├── file3
│ └── file4
└── file5
Dan Bonachea suggests to invert the loops of the git log generation command in this first step: rather than running git log once per file, run it exactly once with a list of files on the command line and generate a single unified log. This way commits that modify multiple files remain a single commit in the result, and all the new commits maintain their original relative order. Note this also requires changes in second step below when rewriting filenames in the (now unified) log.
Suppose you want to move these three files in this other repo (can be the same repo).
my_other_repo
├── dirF
│ ├── file55
│ └── file56
├── dirB # New tree
│ ├── dirB1 # from subdir
│ │ ├── file33 # from file3
│ │ └── file44 # from file4
│ └── dirB2 # new dir
│ └── file5 # from file5
└── dirH
└── file77
Therefore reorganize your files:
cd /tmp/mail/dir
mkdir -p dirB/dirB1
mv subdir/file3 dirB/dirB1/file33
mv subdir/file4 dirB/dirB1/file44
mkdir -p dirB/dirB2
mv file5 dirB/dirB2
Your temporary history is now:
/tmp/mail/dir
└── dirB
├── dirB1
│ ├── file33
│ └── file44
└── dirB2
└── file5
Change also filenames within the history:
cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'
Your other repo is:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
└── dirH
└── file77
Apply commits from temporary history files:
cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date
--committer-date-is-author-date
preserves the original commit time-stamps (Dan Bonachea's comment).
Your other repo is now:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
├── dirB
│ ├── dirB1
│ │ ├── file33
│ │ └── file44
│ └── dirB2
│ └── file5
└── dirH
└── file77
Use git status
to see amount of commits ready to be pushed :-)
To list the files having been renamed:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
More customizations: You can complete the command git log
using options --find-copies-harder
or --reverse
. You can also remove the first two columns using cut -f3-
and grepping complete pattern '{.* => .*}'
.
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'