Is it possible to move/rename files in Git and maintain their history?

前端 未结 14 2168
梦如初夏
梦如初夏 2020-11-22 04:42

I would like to rename/move a project subtree in Git moving it from

/project/xyz

to

/components/xyz

If I

相关标签:
14条回答
  • 2020-11-22 05:23

    First create a standalone commit with just a rename.

    Then any eventual changes to the file content put in the separate commit.

    0 讨论(0)
  • 2020-11-22 05:26

    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!

    0 讨论(0)
  • 2020-11-22 05:26

    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.

    0 讨论(0)
  • 2020-11-22 05:29

    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
    
    0 讨论(0)
  • 2020-11-22 05:29

    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.

    0 讨论(0)
  • 2020-11-22 05:33

    Yes

    1. You convert the commit history of files into email patches using git log --pretty=email
    2. You reorganize these files in new directories and rename them
    3. You convert back these files (emails) to Git commits to keep the history using git am.

    Limitation

    • Tags and branches are not kept
    • History is cut on path file rename (directory rename)

    Step by step explanation with examples

    1. Extract history in email format

    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.


    2. Reorganize file tree and update filenames

    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"' {} ';'
    

    3. Apply new history

    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 :-)


    Extra trick: Check renamed/moved files within your repo

    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 '{.* => .*}'
    
    0 讨论(0)
提交回复
热议问题