如何将大型Git分支拆分为许多较小的分支?

时间:2012-09-22 03:31:08

标签: git branch cherry-pick

我从SVN导入Git,现在我有一个大分支,如下:

  • 处理功能C
  • 处理功能B
  • 处理功能C
  • 处理功能C
  • 处理功能B
  • 处理功能A

我想要A,B,C的单独功能分支。我正在尝试提交到新的分支,但这不会将它们从原始分支中移除,因此我必须手动跟踪我已经拔出的分支。 / p>

有大约800个提交要分开,可能有50个功能/错误修正。

在git log中以某种方式将我已经拔出的那些反映出来会很好,所以我知道我已经完成了哪些。这可能吗?

我可以重新整理整个分支,跳过我已经撤出的提交,但我担心这会导致很多冲突。我不想在每次提交时解决500个冲突。

从一个超级分支机构撤回提交到较小功能分支的最佳方法是什么,同时跟踪您的进度?

6 个答案:

答案 0 :(得分:28)

在这种情况下我要做的是使用交互式rebase

HEAD,创建分支ABC。还要创建一个"备份"分支(您可以将其命名为backup),以防出现问题并且您需要原来的HEAD

git branch feature-a
git branch feature-b
git branch feature-c
git-branch backup-before-rebase

然后,在您希望它们开始的提交中创建一个分支,可能是在方便的稳定提交中。称之为new_trunk或其他。

git checkout HEAD~50       ## this will be the new tree-trunk
git branch new_trunk

然后,执行交互式rebase并选择要保留在该分支中的提交。使用这种方式,它基本上就像cherry-pick批量。

git checkout feature-a
git rebase -i new_trunk    ## -i is for "Interactive"

当您完成后,如果您仍然需要,则应该有3个分支,其中包含从new_trunk开始的单独历史记录和反映旧backup的{​​{1}}分支。

答案 1 :(得分:8)

就我个人而言,我会真正考虑这些大变化的利弊(如果你已经这样做了,再一次)。如果你遇到冲突(这是一个很大的变形/挑选烦人且难以解决),你可能会在将功能合并回“主”分支时遇到困难。

不会更好/更容易冻结你的大分支,让它“完成”(或“足够好”)并在其上创建新的功能分支? (或只排除一些分支?)

但是你的问题是:

如果您想自动跟踪更改/遗漏提交,请使用git cherry命令。

git cherry featureBranch bigBranch

如果在分享或重新定位功能分支时没有冲突,您可以使用以前的代码和一些额外的管道:

git cherry featureBranch bigBranch | awk '{ print "pick " $2 }' | tee remaining

这将在featureBranch中打印(并保存到名为“remaining”的文件)提交。您可以将它添加到bigBranch上的交互式rebase中,以丢弃您不再需要的提交。 (也许您可以使用“ed”编辑器将其编写为git编辑器并将命令传递给交互式rebase的标准输入,但我没有尝试过。)

答案 2 :(得分:4)

进一步简化willoller's answer

制作功能分支和备份,以防

git branch feature-a
git branch feature-b
git branch feature-c
git branch backup-before-rebase

然后签出一个功能分支,并从您希望它们从

开始的提交中执行交互式rebase
git checkout feature-a
git rebase -i <safecommit>
enter code here

如果您希望某些功能分支共享一些提交以保持树清洁,请不要在开始时创建后面的功能分支,但是一旦您获得了一个重新定义的功能分支,然后使用共享将引用提交为下一个 safecommit

#on branch feature-a
git checkout -b feature-d
git rebase -i <sharedcommit>

答案 3 :(得分:2)

我刚刚发现的另一种方法是使用“git notes”。

http://alblue.bandlem.com/2011/11/git-tip-of-week-git-notes.html

此功能允许在不实际更改分支/需要rebase的情况下向现有提交添加注释。跟踪哪些提交已被撤出的一种方法是为每个提交添加一个git注释:

Cherry-picked to features\xyz 925a5239d4fbcf7ad7cd656020793f83275ef45b

这可以在一个很大程度上手动的过程中提供帮助 - 您可以编写一个小脚本来挑选对特定分支的提交,然后将相关的git注释添加回原始提交。

或者,如果你想变得非常时髦,你可以通过以下方式自动化整个过程。

  1. 为每个提交添加一个git注释,说明你希望它选择哪个功能分支:TOCHERRYPICK: features\xyz
  2. 编写一个脚本来扫描所有git备注,并自动创建所有功能分支并挑选正确的选定提交。然后它可以将git音符更改为CHERRYPICKED: features\xxx at 925a5239d4fbcf7ad7cd656020793f83275ef45b以允许稍后重新运行该工具以选择更多提交。
  3. 如果您真的热衷于在提交提交时突出显示,您还可以自动创建具有类似名称的标记:CHERRYPICKED:<branch>:SHA

答案 4 :(得分:2)

老实说我不会这样做,除非你有一大堆需要拆分的提交,而且它们是非常独立的功能,即不会改变可以解决冲突的同一行。

正如其他人所建议的那样,为每个功能创建一个新分支,并使用git rebase --interactive包含所需的提交。

要确保没有误入歧途,请按

创建git-rebase-todo个文件的内容
  • 编辑所有所需提交的列表并按功能
  • 对其进行分类
  • 将提交列表分隔为单独的文件

您可以使用

之类的命令创建提交列表
git log --oneline --reverse  44e19^... > log.txt

显示提交44e19以后。这将为您提供这样的文件

44e1936 dSRGratuities (SummaryRecord)
67fedda Receipt Report HEADER: 20! multiply by Paym_FieldCount
69d70e2 Receipt Report: Payment
....

编辑时(添加分类:功能a,b,c等)可能看起来像我的sorted.txt

c 44e1936 dSRGratuities (SummaryRecord)
a 67fedda Receipt Report HEADER: 20! multiply by Paym_FieldCount
b 69d70e2 Receipt Report: Payment
c abea7db Receipt Report: Cashback
a cf96185 Receipt Report: Gratuity
c 70e987a Receipt Report: use amount tendered for printing
a 7722ac8 Receipt Report: use amount tendered for calculations
c 47f1754 Receipt Report: store amount tendered
b b69a73f Receipt Report: Use enum Paym_FieldCount
a 9a0b471 Receipt Report HEADER: enum PaymentEntries (with Paym_FieldCount)
c ad67e79 Use SharpReport enum
b 3c510c6 enum SharpReport
a e470e07 m_Gratuities m_dSSGratuities (SalesSummary)
b 4e0c3e4 m_Gratuities m_szGratuities (SalesSummaryRecord)
b bd054f7 _gx_fn_Cashback

然后使用您喜欢的脚本语言编写脚本,将已排序的列表转换为git-rebase-todo个文件的集合。你的脚本可能类似我刚写的那个。

foreachline text sorted.txt {
    set fields  [split $text { }]
    set branch  [lindex $fields 0]
    set commit  [lindex $fields 1]
    set comment [string range $text 10 end]
    set command "echo pick $commit $comment"
    exec cmd /S /C $command >> $branch.txt
}

该脚本逐行读取提交排序文件,并按空格字符{}拆分以获取两个字段branchcommit,并获取子字符串(字符10以后)以获取描述提交。描述不是必需的,但它对我们人类检查错误很有用。

然后将一行放入相应的git-rebase-todo文件中,为每个功能创建一个文件。我通过执行一个非常丑陋的Windows echo string >> file命令来攻击它。

这会创建许多文件,例如我的档案a.txt

pick 67fedda Receipt Report HEADER: 20! multiply by Paym_FieldCount
pick cf96185 Receipt Report: Gratuity
pick 7722ac8 Receipt Report: use amount tendered for calculations
pick 9a0b471 Receipt Report HEADER: enum PaymentEntries (with Paym_FieldCount)
pick e470e07 m_Gratuities m_dSSGratuities (SalesSummary)

整件事情很难看。除非你必须这样做并且善于编写脚本,否则我不推荐它。

我上次写了上面的文字,我对事情进行了一些反思。上面我暗示这是一项很多工作而且不值得做,但我已经看到过某些情况,看起来有人做了上述事情并且非常值得。

我记得Visual Studio for MFC / C ++的版本,其中每个新版本都有编译器更改,IDE更改,MFC改进,以及在更高版本的Windows上运行。这意味着如果你想让你的编译器远离VS6和Windows XP,你可能必须进行语言更改以满足编译器,并且函数调用更改以满足MFC等。

现在假设Microsoft在开发Visual Studio时已经进行了每周备份,并且有人有条不紊地接受旧备份并将代码更改提交到像Git这样的版本控制系统。然后他们开始对变化进行分类......

  • 一个。 =编译器更改
  • 湾=库更改
  • ℃。 = IDE更改
  • d。 =安全改进

Microsoft可以为每个分支创建分支,并开始拥有最新且最好的IDE(包括c),在最新的Windows上运行,并且仍然能够使用该语言编译旧的遗留程序(no {{ 1}})和库(没有a)是为它们编写的。

以前锁定在旧版软件中的开发人员可以以逻辑和增量方式进行改进,例如:语言更改和库彼此独立地更改,并在最新和最好的Visual Studio上进行,而无需通过所有中间版本。

示例

b

现在我并不是说这就是发生的事情,但在我看来,Visual Studio的最新版本在允许遗留程序更新,而不是丢弃和(从不?)重写方面要好得多,而且它在我看来是由于版本控制和将旧软件更改组织成逻辑分支:编译器版本,DLL /库版本。

所以,我可以看到将大量旧提交分成不同分支的情况可能是值得的。

在Visual Studio 2019上,我可以添加行

  <LanguageStandard>stdcpp14</LanguageStandard>

到一个配置文件,并设法编译并运行一个无法编译并与VS 2015和VS 2017链接的旧Windows程序。看起来非常像微软的某个人已经将某些旧软件的性能和安全性改进重新定位了遗漏了经常带来现代化的<PlatformToolset>v141_xp</PlatformToolset>

答案 5 :(得分:0)

// 在开发分支上

import sys
from functools import cached_property

from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets


class WebView(QtWebEngineWidgets.QWebEngineView):
    def createWindow(self, type_):
        if not isinstance(self.window(), Browser):
            return

        if type_ == QtWebEngineWidgets.QWebEnginePage.WebBrowserTab:
            return self.window().tab_widget.create_tab()


class TabWidget(QtWidgets.QTabWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setTabsClosable(True)

    def create_tab(self):
        view = WebView()

        index = self.addTab(view, "(Untitled)")
        self.setTabIcon(index, view.icon())
        view.titleChanged.connect(self.update_title)
        view.iconChanged.connect(self.update_icon)
        self.setCurrentWidget(view)
        return view

    def update_title(self, title):
        view = self.sender()
        index = self.indexOf(view)
        self.setTabText(index, title)

    def update_icon(self, icon):
        view = self.sender()
        index = self.indexOf(view)
        self.setTabIcon(index, icon)


class Browser(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        QtWebEngineWidgets.QWebEngineProfile.defaultProfile().downloadRequested.connect(
            self.on_downloadRequested, QtCore.Qt.UniqueConnection
        )

        self.setCentralWidget(self.tab_widget)

        view = self.tab_widget.create_tab()
        view.load(QtCore.QUrl("https://www.remove.bg"))

    @cached_property
    def tab_widget(self):
        return TabWidget()

    def on_downloadRequested(self, download):
        download.finished.connect(self.download_finished)
        default_path = download.downloadFileName()
        suffix = QtCore.QFileInfo(default_path).suffix()
        path, _ = QtWidgets.QFileDialog.getSaveFileName(
            self, "Save File", default_path, "*." + suffix
        )
        if path:
            download.setPath(path)
            download.accept()

    def download_finished(self):
        self.tab_widget.removeTab(self.tab_widget.currentIndex())


def main():
    app = QtWidgets.QApplication(sys.argv)

    w = Browser()
    w.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()