簡體   English   中英

如何合並兩個 Git 存儲庫?

[英]How do you merge two Git repositories?

考慮以下場景:

我在自己的 Git 存儲庫中開發了一個小型實驗項目 A。 它現在已經成熟,我希望 A 成為更大項目 B 的一部分,該項目擁有自己的大型存儲庫。 我現在想將 A 添加為 B 的子目錄。

如何將 A 合並到 B,而不會丟失任何一方的歷史記錄?

如果要將project-a合並到project-b

cd path/to/project-b
git remote add project-a /path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

摘自: git合並不同的存儲庫?

這種方法對我來說效果很好,它更短,在我看來更干凈。

如果您想將project-a放入子目錄,您可以使用git-filter-repo不建議使用filter-branch )。 在上述命令之前運行以下命令:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

合並 2 個大存儲庫,將其中一個放入子目錄的示例:https ://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

注意: -- --allow-unrelated-histories參數只存在於 git >= 2.9 之后。 參見Git - git merge 文檔 / --allow-unrelated-history

更新--tags建議添加了--tags以保留標簽。

以下是兩種可能的解決方案:

子模塊

將存儲庫 A 復制到更大的項目 B 中的單獨目錄,或者(也許更好)將存儲庫 A 克隆到項目 B 的子目錄中。然后使用git submodule使該存儲庫成為存儲庫 B 的子模塊

這是松耦合的倉庫,其中一個倉庫繼續發展一個很好的解決方案,以及發展的主要部分是又見一個單獨的獨立發展SubmoduleSupportGitSubmoduleTutorial上的Git維基網頁。

子樹合並

您可以使用子樹合並策略將存儲庫 A 合並到項目 B 的子目錄中。 這在 Markus Prinz 的Subtree Merging and You 中有描述。

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(Git >= 2.9.0 需要選項--allow-unrelated-histories 。)

或者您可以使用 apenwarr (Avery Pennarun) 的git subtree工具( GitHub 上的存儲庫),例如在他的博客文章中宣布的Git 子模塊的新替代方案: git subtree


我認為在您的情況下(A 將成為更大項目 B 的一部分)正確的解決方案是使用subtree merge

另一個存儲庫的單個分支可以輕松放置在保留其歷史記錄的子目錄下。 例如:

git subtree add --prefix=rails git://github.com/rails/rails.git master

這將顯示為單個提交,其中 Rails 主分支的所有文件都添加到“rails”目錄中。 然而,提交的標題包含對舊歷史樹的引用:

從提交<rev>添加 'rails/'

其中<rev>是 SHA-1 提交哈希。 你仍然可以看到歷史,怪一些變化。

git log <rev>
git blame <rev> -- README.md

請注意,您無法從此處看到目錄前綴,因為這是一個完整的實際舊分支。 您應該將此視為通常的文件移動提交:到達它時需要額外的跳轉。

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

有更復雜的解決方案,例如手動執行此操作或按照其他答案中的描述重寫歷史記錄。

git-subtree 命令是官方 git-contrib 的一部分,一些數據包管理器默認安裝它(OS X Homebrew)。 但是除了 git 之外,您可能還需要自己安裝它。

如果您想單獨維護項目,子模塊方法很好。 但是,如果您真的想將兩個項目合並到同一個存儲庫中,那么您還有更多工作要做。

第一件事是使用git filter-branch將第二個存儲庫中所有內容的名稱重寫為您希望它們結束的子目錄中。 因此,而不是foo.cbar.html ,你就必須projb/foo.cprojb/bar.html

然后,您應該能夠執行以下操作:

git remote add projb [wherever]
git pull projb

git pull將執行git fetch然后是git merge 如果您要拉到的存儲庫還沒有projb/目錄,則應該沒有沖突。

進一步搜索表明已經做了類似的事情來將gitk合並到git Junio C Hamano 在這里寫到: http : //www.mail-archive.com/git@vger.kernel.org/msg03395.html

git-subtree很好,但它可能不是你想要的。

例如,如果projectA是在 B 中創建的目錄,則在git subtree

git log projectA

列出一個提交:合並。 合並項目的提交是針對不同路徑的,因此它們不會顯示。

Greg Hewgill 的答案最接近,盡管它實際上並沒有說明如何重寫路徑。


解決方案出奇的簡單。

(1) 在 A 中,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

注意:這會改寫歷史; 您可能需要先備份 A。

注意:如果在文件名或路徑中使用非 ASCII 字符(或白色字符),則必須修改 sed 命令中的替換腳本。 在這種情況下,“ls-files -s”生成的記錄內的文件位置以引號開頭。

(2) 然后在 B 中,運行

git pull path/to/A

瞧! 您在 B 中有一個projectA目錄。如果您運行git log projectA ,您將看到來自 A 的所有提交。


就我而言,我想要兩個子目錄, projectAprojectB 在那種情況下,我也對 B 執行了步驟 (1)。

如果兩個存儲庫具有相同類型的文件(例如不同項目的兩個 Rails 存儲庫),您可以將輔助存儲庫的數據提取到當前存儲庫:

git fetch git://repository.url/repo.git master:branch_name

然后將其合並到當前存儲庫:

git merge --allow-unrelated-histories branch_name

如果您的 Git 版本小於 2.9,請刪除--allow-unrelated-histories

在此之后,可能會發生沖突。 例如,您可以使用git mergetool解決它們。 kdiff3可以單獨與鍵盤一起使用,因此在閱讀代碼時只需幾分鍾即可獲得 5 個沖突文件。

記得完成合並:

git commit

我在使用合並時一直丟失歷史記錄,所以我最終使用了 rebase,因為在我的情況下,這兩個存儲庫不同,不會在每次提交時都合並:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=> 解決沖突,然后繼續,根據需要多次...

git rebase --continue

這樣做會導致一個項目擁有來自 projA 的所有提交,然后是來自 projB 的提交

就我而言,我有一個my-plugin存儲庫和一個main-project存儲庫,我想假裝my-plugin一直是在main-projectplugins子目錄中main-project

基本上,我重寫了my-plugin存儲庫的歷史記錄,以便所有開發都發生在plugins/my-plugin子目錄中。 然后,我將my-plugin的開發歷史添加到main-project歷史中,並將兩棵樹合並在一起。 由於main-project存儲庫中沒有plugins/my-plugin目錄,因此這是一個微不足道的無沖突合並。 生成的存儲庫包含兩個原始項目的所有歷史記錄,並且有兩個根。

TL; 博士

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

長版

首先,創建my-plugin存儲庫的副本,因為我們將重寫此存儲庫的歷史記錄。

現在,導航到my-plugin存儲庫的根目錄,檢查您的主分支(可能是master ),然后運行以下命令。 當然,無論您的實際名稱是什么,您都應該替換my-pluginplugins

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

現在解釋一下。 git filter-branch --tree-filter (...) HEAD在每次可從HEAD訪問的提交上運行(...)命令。 請注意,這直接對為每個提交存儲的數據進行操作,因此我們不必擔心“工作目錄”、“索引”、“暫存”等概念。

如果您運行的filter-branch命令失敗,它將在.git目錄中留下一些文件,下次您嘗試filter-branch它會抱怨這個,除非您為filter-branch提供-f選項。

至於實際的命令,我沒有太多運氣讓bash做我想做的事,所以我使用zsh -c來讓zsh執行命令。 首先,我設置extended_glob選項,這是什么使^(...)的語法mv命令,還有glob_dots選項,它可以讓我選擇點文件(如.gitignore與水珠() ^(...) )。

接下來,我使用mkdir -p命令同時創建pluginsplugins/my-plugin

最后,我使用zsh “負 glob” 功能^(.git|plugins)匹配存儲庫根目錄中除.git和新創建的my-plugin文件夾之外的所有文件。 (此處可能不需要排除.git ,但嘗試將目錄移動到自身中是一個錯誤。)

在我的存儲庫中,初始提交不包含任何文件,因此mv命令在初始提交時返回一個錯誤(因為沒有可移動的內容)。 因此,我添加了一個|| true || true以便git filter-branch不會中止。

--all選項告訴filter-branch重寫存儲庫中所有分支的歷史記錄,額外的--是必要的,它告訴git將其解釋為分支重寫的選項列表的一部分,而不是作為一個選項filter-branch本身。

現在,導航到您的main-project存儲庫並檢查您想要合並到的任何分支。 添加my-plugin存儲庫的本地副本(修改其歷史記錄)作為main-project的遠程:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

您現在將在提交歷史記錄中有兩個不相關的樹,您可以使用以下方法很好地可視化:

$ git log --color --graph --decorate --all

要合並它們,請使用:

$ git merge my-plugin/master --allow-unrelated-histories

請注意,在 2.9.0 之前的 Git 中,不存在--allow-unrelated-histories選項。 如果您使用這些版本之一,只需省略該選項: --allow-unrelated-histories阻止的錯誤消息也在2.9.0 中添加。

您不應該有任何合並沖突。 如果這樣做,則可能意味着filter-branch命令無法正常工作,或者main-project已經存在plugins/my-plugin目錄。

確保為任何未來的貢獻者輸入一個解釋性的提交消息,他們想知道黑客正在做什么來創建一個具有兩個根的存儲庫。

您可以使用上面的git log命令可視化新的提交圖,它應該有兩個根提交。 請注意,只有master分支會被合並 這意味着,如果您要合並到main-project樹中的其他my-plugin分支上有重要工作,則在完成這些合並之前,您應該避免刪除my-plugin遠程。 如果不這樣做,那么來自這些分支的提交仍將位於main-project存儲庫中,但有些將無法訪問並且容易受到最終垃圾收集的影響。 (此外,您必須通過 SHA 引用它們,因為刪除遠程會刪除其遠程跟蹤分支。)

或者,在合並了要從my-plugin保留的所有內容后,您可以使用以下命令刪除my-plugin遠程:

$ git remote remove my-plugin

您現在可以安全地刪除您更改其歷史記錄的my-plugin存儲庫的副本。 就我而言,在合並完成並推送后,我還在真正的my-plugin存儲庫中添加了棄用通知。


在 Mac OS X El Capitan 上使用git --version 2.9.0zsh --version 5.2 你的旅費可能會改變。

參考:

我在這里收集了很多關於 Stack OverFlow 等的信息,並設法將腳本放在一起,為我解決了問題。

需要注意的是,它只考慮了每個存儲庫的“開發”分支,並將其合並到一個全新存儲庫中的單獨目錄中。

標簽和其他分支被忽略——這可能不是你想要的。

該腳本甚至處理功能分支和標簽 - 在新項目中重命名它們,以便您知道它們來自哪里。

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            eitch@eitchnet.ch
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=$'\n'

# Detect proper usage
if [ "$#" -ne "2" ] ; then
  echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"
  exit 1
fi


## Script variables
PROJECT_NAME="${1}"
PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}"
TIMESTAMP="$(date +%s)"
LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log"
REPO_FILE="${2}"
REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}"


# Script functions
function failed() {
  echo -e "ERROR: Merging of projects failed:"
  echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1
  echo -e "$1"
  exit 1
}

function commit_merge() {
  current_branch="$(git symbolic-ref HEAD 2>/dev/null)"
  if [[ ! -f ".git/MERGE_HEAD" ]] ; then
    echo -e "INFO:   No commit required."
    echo -e "INFO:   No commit required." >>${LOG_FILE} 2>&1
  else
    echo -e "INFO:   Committing ${sub_project}..."
    echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"
    fi
  fi
}


# Make sure the REPO_URL_FILE exists
if [ ! -e "${REPO_URL_FILE}" ] ; then
  echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!"
  exit 1
fi


# Make sure the required directories don't exist
if [ -e "${PROJECT_PATH}" ] ; then
  echo -e "ERROR: Project ${PROJECT_NAME} already exists!"
  exit 1
fi


# create the new project
echo -e "INFO: Logging to ${LOG_FILE}"
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..."
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
cd ${ROOT_DIR}
mkdir ${PROJECT_NAME}
cd ${PROJECT_NAME}
git init
echo "Initial Commit" > initial_commit
# Since this is a new repository we need to have at least one commit
# thus were we create temporary file, but we delete it again.
# Deleting it guarantees we don't have conflicts later when merging
git add initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
git rm --quiet initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
echo


# Merge all projects into the branches of this project
echo -e "INFO: Merging projects into new repository..."
echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ "${url:0:1}" == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO: Project ${sub_project}"
  echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1
  echo -e "----------------------------------------------------"
  echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1

  # Fetch the project
  echo -e "INFO:   Fetching ${sub_project}..."
  echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote add "${sub_project}" "${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then
    failed "Failed to fetch project ${sub_project}"
  fi

  # add remote branches
  echo -e "INFO:   Creating local branches for ${sub_project}..."
  echo -e "INFO:   Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read branch ; do
    branch_ref=$(echo $branch | tr " " "\t" | cut -f 1)
    branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-)

    echo -e "INFO:   Creating branch ${branch_name}..."
    echo -e "INFO:   Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1

    # create and checkout new merge branch off of master
    if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi
    if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi

    # Merge the project
    echo -e "INFO:   Merging ${sub_project}..."
    echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}"
    fi

    # And now see if we need to commit (maybe there was a merge)
    commit_merge "${sub_project}/${branch_name}"

    # relocate projects files into own directory
    if [ "$(ls)" == "${sub_project}" ] ; then
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level."
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1
    else
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..."
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1
      mkdir ${sub_project}
      for f in $(ls -a) ; do
        if  [[ "$f" == "${sub_project}" ]] ||
            [[ "$f" == "." ]] ||
            [[ "$f" == ".." ]] ; then
          continue
        fi
        git mv -k "$f" "${sub_project}/"
      done

      # commit the moving
      if ! git commit --quiet -m  "[Project] Move ${sub_project} files into sub directory" ; then
        failed "Failed to commit moving of ${sub_project} files into sub directory"
      fi
    fi
    echo
  done < <(git ls-remote --heads ${sub_project})


  # checkout master of sub probject
  if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "sub_project ${sub_project} is missing master branch!"
  fi

  # copy remote tags
  echo -e "INFO:   Copying tags for ${sub_project}..."
  echo -e "INFO:   Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read tag ; do
    tag_ref=$(echo $tag | tr " " "\t" | cut -f 1)
    tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3)

    # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0
    tag_name="${tag_name_unfixed%%^*}"

    tag_new_name="${sub_project}/${tag_name}"
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..."
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1
    if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}"
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1
    fi
  done < <(git ls-remote --tags --refs ${sub_project})

  # Remove the remote to the old project
  echo -e "INFO:   Removing remote ${sub_project}..."
  echo -e "INFO:   Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote rm ${sub_project}

  echo
done


# Now merge all project master branches into new master
git checkout --quiet master
echo -e "INFO: Merging projects master branches into new repository..."
echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ ${url:0:1} == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO:   Merging ${sub_project}..."
  echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
  if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "Failed to merge branch ${sub_project}/master into master"
  fi

  # And now see if we need to commit (maybe there was a merge)
  commit_merge "${sub_project}/master"

  echo
done


# Done
cd ${ROOT_DIR}
echo -e "INFO: Done."
echo -e "INFO: Done." >>${LOG_FILE} 2>&1
echo

exit 0

您也可以從http://paste.ubuntu.com/11732805獲取它

首先使用每個存儲庫的 URL 創建一個文件,例如:

git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git

然后調用給出項目名稱和腳本路徑的腳本:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

腳本本身有很多注釋,可以解釋它的作用。

幾天來我一直在嘗試做同樣的事情,我正在使用 git 2.7.2。 子樹不保留歷史記錄。

如果您不再使用舊項目,則可以使用此方法。

我建議你先分支 B 並在分支中工作。

以下是沒有分支的步驟:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

如果您現在記錄 subdir A 中的任何文件,您將獲得完整的歷史記錄

git log --follow A/<file>

這是幫助我做到這一點的帖子:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/

如果您想將 repo B 中分支中的文件放在 repo A 的子樹保留歷史記錄,請繼續閱讀。 (在下面的示例中,我假設我們希望 repo B 的 master 分支合並到 repo A 的 master 分支中。)

在 repo A 中,首先執行以下操作以使 repo B 可用:

git remote add B ../B # Add repo B as a new remote.
git fetch B

現在我們在 repo A 中創建一個全新的分支(只有一次提交),我們稱之為new_b_root 結果提交將包含在 repo B 的 master 分支的第一次提交中提交的文件,但放在名為path/to/b-files/的子目錄中。

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

說明: checkout 命令的--orphan選項從 A 的 master 分支檢出文件,但不創建任何提交。 我們可以選擇任何提交,因為接下來我們無論如何都要清除所有文件。 然后,還沒有提交( -n ),我們從 B 的主分支中挑選第一個提交。 (cherry-pick 保留了原始提交消息,而直接結帳似乎沒有這樣做。)然后我們創建子樹,我們要將所有來自 repo B 的文件放在其中。然后我們必須移動在櫻桃采摘到子樹。 在上面的示例中,只有一個README文件要移動。 然后我們提交我們的 B-repo 根提交,同時,我們還保留原始提交的時間戳。

現在,我們將在新創建的new_b_root之上創建一個新的B/master分支。 我們稱新分支為b

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

現在,我們將b分支合並到A/master

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

最后,您可以刪除B遠程和臨時分支:

git remote remove B
git branch -D new_b_root b

最終圖形將具有如下結構:

在此處輸入圖片說明

我知道事實已經很久了,但我對在這里找到的其他答案並不滿意,所以我寫了這個:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

如果您試圖簡單地將兩個存儲庫粘合在一起,則子模塊和子樹合並是錯誤的工具,因為它們不會保留所有文件歷史記錄(正如人們在其他答案中所指出的那樣)。 在此處查看此答案了解執行此操作的簡單且正確的方法。

我遇到了類似的挑戰,但就我而言,我們在 repo A 中開發了一個版本的代碼庫,然后將其克隆到一個新的 repo repo B 中,以用於產品的新版本。 在修復了 repo A 中的一些錯誤后,我們需要將更改導入 repo B。最終執行以下操作:

  1. 向倉庫 B 添加一個指向倉庫 A 的遙控器(git remote add...)
  2. 拉取當前分支(我們沒有使用 master 來修復錯誤)(git pull remoteForRepoA bugFixBranch)
  3. 將合並推送到 github

辛苦了:)

合並 2 個倉庫

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master

與@Smar 類似,但使用文件系統路徑,在 PRIMARY 和 SECONDARY 中設置:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

然后你手動合並。

(改編自Anar Manafov 的帖子

當您想在一次提交中合並三個或更多項目時,請按照其他答案( remote add -f , merge )中所述執行步驟。 然后,(軟)將索引重置為舊頭(未發生合並)。 添加所有文件( git add -A )並提交它們(消息“將項目 A、B、C 和 D 合並到一個項目中)。這現在是 master 的提交 ID。

現在,使用以下內容創建.git/info/grafts

<commit-id of master> <list of commit ids of all parents>

運行git filter-branch -- head^..head head^2..head head^3..head 如果您有三個以上的分支,只需添加與分支一樣多的head^n..head 要更新標簽,請附加--tag-name-filter cat 不要總是添加它,因為這可能會導致一些提交的重寫。 有關詳細信息,請參閱filter-branch 的手冊頁,搜索“grafts”。

現在,您的最后一次提交已關聯了正確的父項。

在 B 中合並 A:

1)在項目A中

git fast-export --all --date-order > /tmp/ProjectAExport

2)在項目B中

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

在此分支中執行您需要執行的所有操作並提交它們。

C) 然后回到 master 和兩個分支之間的經典合並:

git checkout master
git merge projectA

我稍微手動合並項目,這使我無需處理合並沖突。

首先,從另一個項目中復制你想要的文件。

cp -R myotherproject newdirectory
git add newdirectory

歷史上的下一次拉動

git fetch path_or_url_to_other_repo

告訴 git 合並到上次獲取的事物的歷史記錄中

echo 'FETCH_HEAD' > .git/MERGE_HEAD

現在提交但是你通常會提交

git commit

此函數將遠程倉庫克隆到本地倉庫目錄,合並后所有提交將被保存, git log將顯示原始提交和正確路徑:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

如何使用:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

如果稍作更改,您甚至可以將合並的 repo 的文件/目錄移動到不同的路徑中,例如:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd "$path"
git remote add -f "$remote" "file://$tmp/.git"
git pull "$remote/master"
git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
git remote remove "$remote"
rm -rf "$tmp"

通知
路徑通過sed替換,因此請確保合並后它在正確的路徑中移動。
--allow-unrelated-histories參數只存在於 git >= 2.9 之后。

我今天必須按如下方式解決它:項目 A 在 bitbucket 中,項目 B 在代碼提交中......兩者都是相同的項目,但必須合並從 A 到 B 的更改。(訣竅是在項目中創建相同名稱的分支A,同項目B)

  • git checkout 項目A
  • git遠程刪除原點
  • git remote add origin 項目B
  • git 結帳分支
  • git 添加 *
  • git commit -m "我們已經移動了代碼"

給定命令是我建議的最佳解決方案。

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master

我想將一個小項目移動到一個大項目的子目錄中。 由於我的小項目沒有很多提交,我使用了git format-patch --output-directory /path/to/patch-dir 然后在更大的項目中,我使用了git am --directory=dir/in/project /path/to/patch-dir/*

這種感覺不是一個過濾器,分支那么可怕和方式更加清潔。 當然,它可能不適用於所有情況。

https://github.com/hraban/tomono作為基於腳本的解決方案的另一個提及。

我不是作者,但使用它並且它完成了工作。

一個積極的方面是您將所有分支和所有歷史記錄放入最終回購中。 對於我的存儲庫(存儲庫中沒有重復的文件夾 - 實際上,它們來自 tfs2git 遷移)沒有沖突,並且一切都是自動化的。

它主要用於(見名稱)創建 monorepos。

對於 Windows 用戶:git bash 可以執行 .sh 文件。 它帶有標准的 git 安裝。

請考慮以下情形:

我已經在自己的Git倉庫中開發了一個小型實驗項目A。 它現在已經成熟,我希望A成為較大項目B的一部分,該項目B具有自己的大型存儲庫。 我現在想將A添加為B的子目錄。

如何將A合並為B,而又不會丟失任何歷史記錄?

除了使用remote add -> fetch -> merge策略的所有答案之外:如果您想保留來自其他存儲庫的標簽但不想將它們全部溢出到公共命名空間中(並且可能會發生沖突),您可能想要稍微改變 fetch 命令:

git fetch --no-tags other_repo
git fetch --no-tags other_repo 'refs/tags/*:refs/tags/other_repo/*'

第一個命令像往常一樣獲取所有分支,但省略了附加到提交的標簽,第二個命令也省略了通常的標簽獲取機制( git help fetch了解更多),並使用 git 的other_repo/X獲取將它們從X映射到other_repo/X所有標簽功能。

引用(分支、標簽)只是 git 中的文件,您可以使用目錄進行命名空間。 上面的兩個命令將按原樣保留第一個存儲庫中的標簽,另一個存儲庫中的標簽將以other_repo/為前綴

操作后最好把另一個遙控器拿掉,這樣你就不會不小心按正常方式獲取標簽而弄得一團糟。

谷歌有一個 Copybara 工具用於更復雜的用例 - https://github.com/google/copybara

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM