Git工作流(分支管理规范)

原文链接:https://nvie.com/posts/a-successful-git-branching-model/


Note of reflection (March 5, 2020)
反思记录(2020 年 3 月 5 日)
This model was conceived in 2010, now more than 10 years ago, and not very long after Git itself came into being. In those 10 years, git-flow (the branching model laid out in this article) has become hugely popular in many a software team to the point where people have started treating it like a standard of sorts — but unfortunately also as a dogma or panacea.
这个模型是在 2010 年构想出来的,现在已经是 10 多年前了,也就是 Git 本身出现后不久。在这 10 年里,git-flow(本文中介绍的分支模型)在许多软件团队中变得非常流行,以至于人们开始将其视为某种标准——但不幸的是,它也被视为一种教条或灵丹妙药.

During those 10 years, Git itself has taken the world by a storm, and the most popular type of software that is being developed with Git is shifting more towards web apps — at least in my filter bubble. Web apps are typically continuously delivered, not rolled back, and you don’t have to support multiple versions of the software running in the wild.
在这 10 年里,Git 本身已经席卷了整个世界,使用 Git 开发的最流行的软件类型正在更多地转向 Web 应用程序——至少在我的过滤器泡沫中是这样。 Web 应用程序通常是连续交付的,而不是回滚的,而且您不必支持在野外运行的软件的多个版本。

This is not the class of software that I had in mind when I wrote the blog post 10 years ago. If your team is doing continuous delivery of software, I would suggest to adopt a much simpler workflow (like GitHub flow) instead of trying to shoehorn git-flow into your team.
这不是我 10 年前写博客文章时想到的软件类别。如果您的团队正在持续交付软件,我建议采用更简单的工作流程(例如 GitHub 流程),而不是尝试将 git-flow 硬塞进您的团队。

If, however, you are building software that is explicitly versioned, or if you need to support multiple versions of your software in the wild, then git-flow may still be as good of a fit to your team as it has been to people in the last 10 years. In that case, please read on.
但是,如果您正在构建明确版本的软件,或者如果您需要在野外支持软件的多个版本,那么 git-flow 可能仍然适合您的团队,就像它适合其他人一样过去 10 年。在这种情况下,请继续阅读。

To conclude, always remember that panaceas don’t exist. Consider your own context. Don’t be hating. Decide for yourself.
总而言之,永远记住灵丹妙药是不存在的。考虑你自己的背景。不要讨厌。自己决定。


January 05, 2010
2010 年 1 月 5 日

In this post I present the development model that I’ve introduced for some of my projects (both at work and private) about a year ago, and which has turned out to be very successful. I’ve been meaning to write about it for a while now, but I’ve never really found the time to do so thoroughly, until now. I won’t talk about any of the projects’ details, merely about the branching strategy and release management.
在这篇文章中,我介绍了大约一年前我为我的一些项目(包括工作和私人项目)引入的开发模型,结果证明它非常成功。 我一直想写它有一段时间了,但直到现在我才真正找到时间来彻底地写。 我不会谈论任何项目的细节,只会谈论分支策略和发布管理。
Git分支工作流

Why git?

For a thorough discussion on the pros and cons of Git compared to centralized source code control systems, see the web. There are plenty of flame wars going on there. As a developer, I prefer Git above all other tools around today. Git really changed the way developers think of merging and branching. From the classic CVS/Subversion world I came from, merging/branching has always been considered a bit scary (“beware of merge conflicts, they bite you!”) and something you only do every once in a while.
有关 Git 与集中式源代码控制系统相比的优缺点的全面讨论,请参阅网络。 那里有很多争论。 作为一名开发人员,我更喜欢 Git 胜过当今所有其他工具。 Git 确实改变了开发人员对合并和分支的看法。 在我来自的经典 CVS/Subversion 世界中,合并/分支一直被认为有点吓人(“当心合并冲突,它们会咬你!”)而且你只是偶尔才会做的事情。

But with Git, these actions are extremely cheap and simple, and they are considered one of the core parts of your daily workflow, really. For example, in CVS/Subversion books, branching and merging is first discussed in the later chapters (for advanced users), while in every Git book, it’s already covered in chapter 3 (basics).
但是使用 Git,这些操作非常便宜和简单,它们真的是您日常工作流程的核心部分之一。 例如,在 CVS/Subversion 书籍中,分支和合并首先在后面的章节中讨论(针对高级用户),而在每本 Git 书籍中,它已经在第 3 章(基础)中介绍。

As a consequence of its simplicity and repetitive nature, branching and merging are no longer something to be afraid of. Version control tools are supposed to assist in branching/merging more than anything else.
由于其简单性和重复性,分支和合并不再是可怕的事情。 版本控制工具应该比其他任何东西都更有助于分支/合并。

Enough about the tools, let’s head onto the development model. The model that I’m going to present here is essentially no more than a set of procedures that every team member has to follow in order to come to a managed software development process.
工具说得够多了,让我们进入开发模型。 我将在这里介绍的模型本质上只不过是每个团队成员必须遵循的一组程序才能进入托管软件开发过程。

Decentralized but centralized

去中心化但中心化

The repository setup that we use and that works well with this branching model, is that with a central “truth” repo. Note that this repo is only considered to be the central one (since Git is a DVCS, there is no such thing as a central repo at a technical level). We will refer to this repo as origin, since this name is familiar to all Git users.
我们使用的存储库设置与此分支模型配合得很好,是具有中央“真实”存储库的设置。 请注意,这个 repo 只被认为是中央仓库(因为 Git 是 DVCS,所以在技术层面上没有中央仓库之类的东西)。 我们将此 repo 称为 origin,因为所有 Git 用户都熟悉这个名称。

Git仓库管理
Each developer pulls and pushes to origin. But besides the centralized push-pull relationships, each developer may also pull changes from other peers to form sub teams. For example, this might be useful to work together with two or more developers on a big new feature, before pushing the work in progress to origin prematurely. In the figure above, there are subteams of Alice and Bob, Alice and David, and Clair and David.
每个开发人员都拉取并推送到origin。 但除了中心化的推拉关系,每个开发者还可以从其他同行那里拉取变更,形成子团队。 例如,在两个或更多开发人员一起开发一个大的新功能时这种方式很有用,可以避免过早第将将正在进行的工作过早地推送到origin。 上图中,有 Alice 和 Bob、Alice 和 David、Clair 和 David 的子团队。

Technically, this means nothing more than that Alice has defined a Git remote, named bob, pointing to Bob’s repository, and vice versa.
从技术上讲,这意味着 Alice 定义了一个名为 bob 的 Git 远端(remote),指向 Bob 的存储库,反之亦然。

The main branches

main分支
At the core, the development model is greatly inspired by existing models out there. The central repo holds two main branches with an infinite lifetime:
开发模型的核心受到现有模型的极大启发。 中央仓库拥有两个具有无限生命周期的主要分支:

  • master
  • develop(注释:开发分支)

The master branch at origin should be familiar to every Git user. Parallel to the master branch, another branch exists called develop.
每个 Git 用户都应该很熟悉master 分支(注释:GitHub新建一个仓库会默认创建一个master分支)。 与 master 分支平行的是另一个名为develop的分支。
master develop分支
We consider origin/master to be the main branch where the source code of HEAD always reflects a production-ready state.
我们认为 origin/master 是 HEAD 的源代码始终反映生产就绪状态的主要分支。

We consider origin/develop to be the main branch where the source code of HEAD always reflects a state with the latest delivered development changes for the next release. Some would call this the “integration branch”. This is where any automatic nightly builds are built from.
我们认为 origin/develop 是主要分支,其中 HEAD 的源代码始终反映具有下一个版本的最新交付开发更改的状态。 有人将其称为“集成分支”。 这是构建任何自动夜间构建的地方。

When the source code in the develop branch reaches a stable point and is ready to be released, all of the changes should be merged back into master somehow and then tagged with a release number. How this is done in detail will be discussed further on.
当开发分支中的源代码达到稳定点并准备发布时,所有更改都应该以某种方式合并回主控,然后用发布号标记。 (后面将会)将进一步讨论如何详细完成此操作。

Therefore, each time when changes are merged back into master, this is a new production release by definition. We tend to be very strict at this, so that theoretically, we could use a Git hook script to automatically build and roll-out our software to our production servers everytime there was a commit on master.
因此,每次将更改合并回主版本时,根据定义,这就是一个新的生产版本。 我们在这方面往往非常严格,因此理论上,我们可以使用 Git 钩子脚本来自动构建我们的软件并将其推出到我们的生产服务器上,每次在 master 上进行提交。

Supporting branches

支持分支

Next to the main branches master and develop, our development model uses a variety of supporting branches to aid parallel development between team members, ease tracking of features, prepare for production releases and to assist in quickly fixing live production problems. Unlike the main branches, these branches always have a limited life time, since they will be removed eventually.
除了主要分支掌握和开发之外,我们的开发模型使用各种支持分支来帮助团队成员之间的并行开发,轻松跟踪功能,准备生产版本并协助快速修复现场生产问题。 与主分支不同,这些分支的生命周期总是有限的,因为它们最终会被删除。

The different types of branches we may use are:
我们可能使用的不同类型的分支如下:

  • Feature branches
    功能分支
  • Release branches
    发布分支
  • Hotfix branches
    紧急bug修复分支

Each of these branches have a specific purpose and are bound to strict rules as to which branches may be their originating branch and which branches must be their merge targets. We will walk through them in a minute.
这些分支中的每一个都有特定的目的,并且必须遵守严格的规则,即哪些分支可以是它们的原始分支,哪些分支必须是它们的合并目标。 我们将在一分钟内完成它们。

By no means are these branches “special” from a technical perspective. The branch types are categorized by how we use them. They are of course plain old Git branches.
从技术角度来看,这些分支绝不是“特殊的”。 分支类型按我们如何使用它们进行分类。 它们当然是普通的旧 Git 分支。

Feature branches

功能分支
feature分支

  • May branch off from: develop
    从develop切出来

  • Must merge back into: develop
    必须合并到develop分支

  • Branch naming convention: anything except master, develop, release-, or hotfix-
    分支命名约定:除了 master、develop、release-* 或 hotfix-* 之外的任何内容(注释:一般以 feature/功能名或者feat/功能名 命名)

Feature branches (or sometimes called topic branches) are used to develop new features for the upcoming or a distant future release. When starting development of a feature, the target release in which this feature will be incorporated may well be unknown at that point. The essence of a feature branch is that it exists as long as the feature is in development, but will eventually be merged back into develop (to definitely add the new feature to the upcoming release) or discarded (in case of a disappointing experiment).
feature 分支(或有时称为主题分支)用于为即将发布或遥远的未来版本开发新功能。 当开始开发一个功能(feature)时,这个功能什么时候能够合并到发布版本是未知的。 feature分支的本质是,只要feature处于开发阶段,它就存在,但最终会被合并回develop分支中(以明确将新特性添加到即将发布的版本中)或丢弃(实验令人失望)。

Feature branches typically exist in developer repos only, not in origin.
功能分支通常仅存在于开发者存储库中,而不存在于origin中。

(注释:Git具体操作如下)

①Creating a feature branch
创建功能分支
When starting work on a new feature, branch off from the develop branch.
开始开发新功能时,从develop分支切出feature分支。
(注释,原文创建的分支名只有myfeature,我这儿更改了,加上了featrue/)

$ git checkout -b feature/myfeature develop
Switched to a new branch "myfeature"

②Incorporating a finished feature on develop
合并一个已完成的功能到develop分支

Finished features may be merged into the develop branch to definitely add them to the upcoming release:
完成的功能(feature)可以合并到develop分支中,以明确地将它们添加到即将发布的版本中:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop

The --no-ff flag causes the merge to always create a new commit object, even if the merge could be performed with a fast-forward. This avoids losing information about the historical existence of a feature branch and groups together all commits that together added the feature. Compare:
–no-ff 选项使合并始终创建一个新的提交对象,即使可以使用快进执行合并。 这样可以避免丢失有关功能分支的历史存在的信息,并将所有添加该功能的提交组合在一起。 看图:
--no-ff选项
In the latter case, it is impossible to see from the Git history which of the commit objects together have implemented a feature—you would have to manually read all the log messages. Reverting a whole feature (i.e. a group of commits), is a true headache in the latter situation, whereas it is easily done if the --no-ff flag was used.
在后一种情况下,不可能从 Git 历史记录中看到哪些提交对象一起实现了一项功能——您必须手动读取所有日志消息。 在后一种情况下,恢复整个功能(即一组提交)是一个真正令人头疼的问题,而如果使用 --no-ff 标志则很容易做到。

(注释:所以,合并时到用不用–no-ff就见仁见智了,在我看来,小功能的feature合并到develop时可以使用–no-ff,大功能或者需要长期维护发布版本的不使用–no-ff)

Yes, it will create a few more (empty) commit objects, but the gain is much bigger than the cost.
是的,它会创建更多(空)提交对象,但收益远大于成本。

Release branches

发布分支

  • May branch off from: develop
    从develop分支切出来

  • Must merge back into: develop and master
    必须合并回develop和master分支

  • Branch naming convention: release-*
    分支命名约定:realse-*(注释,用realse/*更好)

Release branches support preparation of a new production release. They allow for last-minute dotting of i’s and crossing t’s. Furthermore, they allow for minor bug fixes and preparing meta-data for a release (version number, build dates, etc.). By doing all of this work on a release branch, the develop branch is cleared to receive features for the next big release.
release分支用于准备新的生产版本。它们允许在最后一分钟点 i 和交叉 t。此外,它们允许修复小错误并为发布准备元数据(版本号、构建日期等)。通过在realse分支上完成所有这些工作,develop分支为用于准备下一个大的发布版本。

The key moment to branch off a new release branch from develop is when develop (almost) reflects the desired state of the new release. At least all features that are targeted for the release-to-be-built must be merged in to develop at this point in time. All features targeted at future releases may not—they must wait until after the release branch is branched off.
从develop分支切出一个新的release分支的时间点是develop(几乎)反映了新版本的期望状态(注释:也就是说新功能基本测试通过)。 至少所有需要发布的功能(在各个feature分支)都必须在此时合并到develop中。 而对于未来需要发布的feature则不用合并——它们必须等到发布分支之后。

It is exactly at the start of a release branch that the upcoming release gets assigned a version number—not any earlier. Up until that moment, the develop branch reflected changes for the “next release”, but it is unclear whether that “next release” will eventually become 0.3 or 1.0, until the release branch is started. That decision is made on the start of the release branch and is carried out by the project’s rules on version number bumping.
在release分支的开始给即将发布的版本分配一个版本号——而不是更早。 在那之前(从develop分支切出release分支之前),develop 分支反映了“下一个版本”的变化,但(develop分支)不清楚“下一个版本”最终会变成 0.3 还是 1.0,直到发布分支启动。 (该release分支的版本号)是在切出release分支时做出的,由项目的版本号更新规则决定。

(注释:具体操作如下)
Creating a release branch
创建release分支

Release branches are created from the develop branch. For example, say version 1.1.5 is the current production release and we have a big release coming up. The state of develop is ready for the “next release” and we have decided that this will become version 1.2 (rather than 1.1.6 or 2.0). So we branch off and give the release branch a name reflecting the new version number:
发布分支是从开发分支创建的。 例如,假设版本 1.1.5 是当前的生产版本,我们即将发布一个大版本。 develop分支已为“下一个版本”做好准备,我们已决定这将成为 1.2 版(而不是 1.1.6 或 2.0)。 所以我们分支并给发布分支一个反映新版本号的名称:

$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

After creating a new branch and switching to it, we bump the version number. Here, bump-version.sh is a fictional shell script that changes some files in the working copy to reflect the new version. (This can of course be a manual change—the point being that some files change.) Then, the bumped version number is committed.
在创建一个新分支并切换到它之后,我们会增加版本号。 在这里,bump-version.sh 是一个虚构的 shell 脚本,它更改工作副本中的一些文件以反映新版本。 (这当然可以是手动更改——关键是某些文件发生了更改。)然后,提交增加后的版本号。

This new branch may exist there for a while, until the release may be rolled out definitely. During that time, bug fixes may be applied in this branch (rather than on the develop branch). Adding large new features here is strictly prohibited. They must be merged into develop, and therefore, wait for the next big release.
This new branch may exist there for a while, until the release may be rolled out definitely. During that time, bug fixes may be applied in this branch (rather than on the develop branch). Adding large new features here is strictly prohibited. They must be merged into develop, and therefore, wait for the next big release.
这个新分支可能会在那里存在一段时间,直到发布版本确定推出。 在此期间,会在此分支(而不是develop分支)中修复bug。 严格禁止在此分支添加大的新功能。 大的新功能只能合并到develop分支中,直到下一个大的发布版本。

Finishing a release branch
完成release版本

When the state of the release branch is ready to become a real release, some actions need to be carried out. First, the release branch is merged into master (since every commit on master is a new release by definition, remember). Next, that commit on master must be tagged for easy future reference to this historical version. Finally, the changes made on the release branch need to be merged back into develop, so that future releases also contain these bug fixes.
当发布分支的状态准备好并将真正发布时,需要进行一些动作。 首先,release 分支被合并到 master 中(请记住,master 上的每个提交都是定义的新版本)。 接下来,必须对 master 上的提交打tag,以便将来参考此历史版本。 最后,在release分支上所做的更改都需要合并回develop分支,以便将来的发布版本也修复了相应的bug。

(注释:具体操作如下)
The first two steps in Git:
Git 中的前两个步骤:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2

The release is now done, and tagged for future reference.
发布现已完成,并标记以供将来参考。

Edit: You might as well want to use the -s or -u flags to sign your tag cryptographically.
编辑:您可能还想使用 -s 或 -u 标志以加密方式签署您的标签。

To keep the changes made in the release branch, we need to merge those back into develop, though. In Git:
不过,为了保留在relase分支中所做的更改,我们需要将它们合并回develop分支中。 Git操作如下:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

This step may well lead to a merge conflict (probably even, since we have changed the version number). If so, fix it and commit.
这一步很可能会导致合并冲突(很有可能,因为我们已经更改了版本号)。 如果是这样,修复它并提交。

Now we are really done and the release branch may be removed, since we don’t need it anymore:
现在我们真的完成了,release分支可以被删除,因为我们不再需要它了:

$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

Hotfix branches

Hotfix分支

  • May branch off from: master
    从master分支切出
  • Must merge back into: develop and master
    必须合并回develop分支和master分支
  • Branch naming convention: hotfix-*
    分支命名约定:hotfix-* (注释,更推荐使用hotfix/*)
    hotfix分支示例
    Hotfix branches are very much like release branches in that they are also meant to prepare for a new production release, albeit unplanned. They arise from the necessity to act immediately upon an undesired state of a live production version. When a critical bug in a production version must be resolved immediately, a hotfix branch may be branched off from the corresponding tag on the master branch that marks the production version.
    hotfix分支与release分支非常相似,因为它们也旨在为新的生产版本做准备,尽管是计划外的。 它们源于对现场生产版本的不良状态立即采取行动的必要性。 当必须立即解决生产版本中的严重错误时,可以从master 分支相应的tag中切出一个 hotfix 分支。

The essence is that work of team members (on the develop branch) can continue, while another person is preparing a quick production fix.
本质是团队成员(在develop分支上)的工作可以继续,而另一个人可以进行快速的生产环境bug修复。

(注释:具体操作如下)

①Creating the hotfix branch
创建hotfix分支

Hotfix branches are created from the master branch. For example, say version 1.2 is the current production release running live and causing troubles due to a severe bug. But changes on develop are yet unstable. We may then branch off a hotfix branch and start fixing the problem:
hotfix分支是从master分支创建的。 例如,假设版本 1.2 是当前正在运行的生产版本,并且由于严重的bug而导致了问题。 但是develop分支还不稳定(注释:所以不能从develop分支切出来,release分支正式发布之后已经被删除)。 我们(从master分支)切出一个hotfix分支并开始修复问题:

$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

Don’t forget to bump the version number after branching off!
分支后不要忘记修改版本号!

Then, fix the bug and commit the fix in one or more separate commits.
然后,一个或多个单独的commit中修复bug。

$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

②Finishing a hotfix branch
完成hotfix分支

When finished, the bugfix needs to be merged back into master, but also needs to be merged back into develop, in order to safeguard that the bugfix is included in the next release as well. This is completely similar to how release branches are finished.
完成后,需要将 bugfix 合并回 master,但也需要将 bugfix 合并回 develop,以确保 bugfix 也包含在下一个版本中。 这与release分支的完成方式完全相似。

First, update master and tag the release.
首先,更新master,打tag并发布。

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1

Edit: You might as well want to use the -s or -u flags to sign your tag cryptographically.
编辑:您可能还想使用 -s 或 -u 标志以加密方式签署您的标签。

Next, include the bugfix in develop, too:
接下来,也将bugfix合并回develop中:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

The one exception to the rule here is that, when a release branch currently exists, the hotfix changes need to be merged into that release branch, instead of develop. Back-merging the bugfix into the release branch will eventually result in the bugfix being merged into develop too, when the release branch is finished. (If work in develop immediately requires this bugfix and cannot wait for the release branch to be finished, you may safely merge the bugfix into develop now already as well.)
这个规则的一个例外是,当对应的release分支还存在时,需要将修补程序更改合并到该release分支中,而不是develop分支。 当release分支完成时,将bugfix合并到release分支最终会导致错误修复也被合并到开发中。 (如果开发中的工作立即需要此错误修复并且不能等待发布分支完成,您现在也可以安全地将错误修复合并到开发中。)

$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

Summary

总结

While there is nothing really shocking new to this branching model, the “big picture” figure that this post began with has turned out to be tremendously useful in our projects. It forms an elegant mental model that is easy to comprehend and allows team members to develop a shared understanding of the branching and releasing processes.
虽然这个分支模型并没有什么真正令人震惊的新东西,但这篇文章开头的“大图”在我们的项目中已经证明是非常有用的。 它形成了一个易于理解的优雅模型,并允许团队成员对分支和发布过程形成共同的理解。

A high-quality PDF version of the figure is provided here. Go ahead and hang it on the wall for quick reference at any time.
此处提供了该图的高质量 PDF 版本。 来吧,把它挂在墙上,以便随时快速参考。

Update: And for anyone who requested it: here’s the gitflow-model.src.key of the main diagram image (Apple Keynote).
更新:对于任何请求它的人:这是主图图像(Apple Keynote)的 gitflow-model.src.key。

https://nvie.com/files/Git-branching-model.pdf


我们公司最近将Unity3D的项目从SVN切换到Git来管理了,使用的工作流就是本文所解释的。在使用过程中有一点点的问题:

  • Untiy3D项目中,如果多人都对预制体进行了修改,在合并的时候将会花费很多时间,因为我们项目中的预制体制作非常容易冲突,冲突了又得重新做一遍。为了解决这个问题,将预制体的功能拆分,单独功能的预制体专人维护。
  • Git查看文件的修改没SVN方便,需要用到第三方的Diff工具
  • 大小很大的资源如纹理、模型版本很多时,非常占用硬盘,这是无解的了。

但总体上说,使用Git管理项目还是很方便的。

参考链接

猜你喜欢

转载自blog.csdn.net/sinat_25415095/article/details/124918092