“Git 的历史与现状”
Git是Linux的创始人Linus Torvalds的又一力作。在2002年,他在Linux内核的版本控制中使用Bitkeeper,但由于Bitkeeper是一款受版权保护的软件,备受质疑。后来,Andrew Tridgell对Bitkeeper进行了逆向工程,导致BitMover收回了Linux开发者对Bitkeeper的免费使用权。Linus对此十分愤怒,于是花了10天时间编写了Git。
Git这个名字的含义是:“egotistical bastard”(自恋的混蛋)。
如今,Git已经成为绝大多数开发者的首选。由Tom Preston-Werner、Chris Wanstrath和PJ Hyett于2007年10月推出的GitHub已经发展成为全球最大的开发者网站,许多团队在上面做出了重要的贡献。
值得一提的是,即使是一直自家造轮子的微软,也计划将庞大的300GB Windows源代码迁移到Git上进行管理。为了应对巨大代码仓库的性能问题,他们为Git提供了全新的GVFS实现。
此外,还值得一提的是Docker的二进制镜像管理也是基于Git实现的。
Git和SVN在设计理念上存在根本区别。SVN采用中心化管理,具有更好的稳定性和安全性。相反,Git是一款去中心化的版本控制工具,最初是为了满足Linux操作系统开发的需求。它更适合多人协作的开源项目,允许在任何一个点将远程代码与本地代码合并。随着时间的推移,Git还演变出了更多强大的功能和完整的操作流程,使其也适用于商业软件的开发。
Workspace(工作区):当前工作区,即修改的初始状态。
Staging(暂存区):修改后,添加到准备提交的缓存状态。
Local repository(本地代码库):本地的代码仓库,只对自己的代码生效。与SVN的一个区别在于,SVN commit之后直接提交到远程服务器,而Git commit之后只是到本地代码库。
Remote repository(远程代码库):远程代码库,用于同步本地代码库到远程,以供其他开发者分享。
以下是几个常用命令的简单介绍:
PS:图中未提及rebase和cherry-pick命令,这两个命令也非常强大,后文有提到,可关注一下。
补丁 diff:
之前提到过,补丁是Git/SVN代码版本管理的基础概念。它实际上是以行为单位的文件修改历史,增加行以+号开头,删除行以-号开头。而修改一行则是先删除再添加。
在Git中,可以通过git diff命令,或在Linux/Mac/Conemu中使用diff -Naur生成文件对比结果,类似于下图。这构成了整个代码管理的基础概念,所有分支、标签、远程都是在此基础上衍生的。
1.克隆代码到本地开发环境 - Clone
$ git clone [REPOSITORY_URL]
类似于 SVN 的 checkout 命令,用于将远程代码克隆到本地。与 SVN 一样,REPOSITORY_URL 的协议非常灵活。之前常用的是 SSH/SCP 协议,但现在 HTTPS/HTTP 协议也逐渐流行。
2.更新代码 - Status/Commit/Log
使用 git status 命令查看四种场景中的前两种状态。新建的代码会显示为 Untracked files(Workspace)状态。执行 add 后,代码变为 Changes to be committed(Stage)状态。对 Stage 中的文件进行修改后,状态变为 Changes not staged for commit。执行 commit 后,代码从 Stage 转移到 Local repository 中。使用 git log 可查看代码提交历史。
3.Branch 和 Tag
Branch 和 Tag 都可视为补丁的时序化集合,Branch 可以相互合并。在克隆 repository 后,默认有一个主线分支称为 master。Tag 用于发布后标记版本。与 SVN 不同,SVN 的 Branch 和 Tag 是将整个 Trunk 代码库拷贝出来,而 Git 只是将补丁引用重新应用在当前代码上。因此,Git 的 Branch/Tag 非常轻量,切换起来非常便捷。在使用 Git 时,建议充分利用分支来提高开发效率。稍后在介绍 Git flow 时,将说明如何使用分支进行代码功能开发管理。
3.1 新建分支的两种办法
$ git checkout -b [BRANCH_NAME] # 在当前版本切换并新建分支
$ git branch [BRANCH_NAME] # 直接新建分支
3.2 切换分支
$ git checkout [BRANCH_NAME]
3.3 合并分支的两种办法
$ git merge [BRANCH_NAME] # 将另外一个分支的代码,合并到当前分支之后。
$ git rebase [BRANCH_NAME] # 不推荐,对代码进行比较,将本分支修改后的代码合并到另外一个分支之后
rebase 通常情况下不推荐使用,因为 rebase 完下游分支,再从上游分支 merge 的时候会丢失分支合并的 commit。对于已经推到远程仓库的 commit,不建议 rebase,因为这会导致其他人 pull 时出现大量冲突。
3.4 删除分支
$ git branch -d [BRANCH_NAME] # 已经合并到 master
$ git branch -D [BRANCH_NAME] # 该分支未合并到 master,强制删除
即使删除了分支等,也可以用 git reflogs 找回来。
3.5 取消修改
git stash # 取消全部修改,它可以恢复过来
git reset --soft [REV] # 保留修改内容,从 Local repository 撤销,也可用于回退历史记录
git reset --hard [REV] # 丢掉修改内容,从 Local repository 撤销,也可用于回退历史记录
推送本地代码到远程仓库
$ git push [REMOTE] [BRANCH]
REMOTE 默认为 origin,如果不填,将推送到 origin。BRANCH 默认为当前分支,可省略。若要推送本地功能分支,建议使用 --set-upstream 参数。
郑重警告:永远不要对主线 master 分支执行 --force。
获取远程分支更新
$ git pull # 更新到 workspace
$ git fetch # 更新到 Local repository,可能需要通过 merge 合并到 workspace 一次
远程仓库
Clone 后会有一个默认的远程仓库 origin,如果需要添加其他远程仓库,可使用以下命令:
$ git remote add [REMOTE_NAME] [URL] # 添加远程仓库
$ git fetch [REMOTE_NAME] # 获取远程仓库更新
$ git branch -a # 查看所有分支,包括远程仓库内的
$ git push [REMOTE_NAME] [BRANCH_NAME] # 推送到远程仓库
GitHub在Git Remote的基础上引入了一套机制,以便更方便地参与开源项目。一般的开源项目参与流程是,首先注册一个GitHub账号,然后将感兴趣的开源项目Fork到自己的命名空间下。接下来,拆分分支进行修改,并提交到自己的GitHub存储库。然后,发起一个Pull Request(PR),请求项目维护者合并你的代码。在此过程中,项目维护者会对你的代码进行审查和点评,你需要按照维护者的要求进行修改(在这里,经常使用rebase来保持代码整洁)。一旦修改得到批准,并获得维护者的同意,维护者将代码合并到项目中。
具体的流程一经尝试就会明白,GitLab的Merge Request的原理与此一致。
Git Flow
Git Flow在Git分支的基础上实现了一套简单的功能模块化开发流程,主要思想是将分支划分为上下游几个层级,然后通过一套命令行工具进行维护。
master分支: 与线上版本保持一致,当出现线上问题时可以轻松修复。
develop分支: 功能开发的基线分支,功能开发完成后合并到此分支。所有功能开发完成后经过测试上线,然后合并回master。
*feature/功能开发分支: 从develop分支拆分,需要定期将develop的更新合并过来,保持与上游分支的一致性。功能开发完成后,可合并到develop。通过与上游分支保持一致,可以避免误删他人代码。所有代码冲突必须在下游分支解决,测试完毕后才可合并到上游分支。
*hotfix/热修复分支: 主要用于在线上修复bug,修复后应同时合并到master和develop两个分支。
然后,git flow提供了一套命令行工具,更轻松地进行这些代码合并的操作。
$ git flow init # 初始化git flow分支模型
$ git flow feature start [NAME] # 开始一个功能分支
$ git flow feature finish [NAME] # 将功能分支合并到develop
$ git flow hotfix start [NAME] # 开始一个热修复分支
$ git flow hotfix finish [NAME] # 将补丁合并到develop和master
$ git flow release [NAME] # 发布一个新版本,打标签