贡献一个开源项目

贡献一个开源项目

贡献一个项目的基本流程

1
2
3
4
5
6
7
8
9
fork=>operation: Fork
clone=>operation: Clone
branch=>operation: 创建分支
change=>operation: 贡献代码
pr=>operation: PR
discuss=>operation: 讨论与交流


fork->clone->branch->change(right)->pr(right)->discuss

Git的基本图像

本地存在一套Git仓库,可以用add, commit, branch等命令对本地的仓库进行版本管理。如果只需要在本地做版本管理,那么这些功能就够了。

但是我们必须把自己的repo上传到远程Git仓库,以便于团队协作以及他人使用。此时就出现了本地和远程两套版本库,这个时候就需要作两套版本库的同步工作。

自己在本地进行开发,本地版本库更新了,需要把远程版本库也更新,这时候就要用git push把本地的版本库上传到远程并合并;因团队协作他人commit,远程的版本库就会新于本地的版本库,这时候就需要git fetch拉取远程版本库到本地,再使用git merge合并远程代码到本地。

整个Git版本管理的基本流程就是这样,在实际使用过程中,会存在一些其他问题。比如可能会存在多个分支,会有不同分支之间的merge。或者远程repo是Fork的他人的repo,这时候就会有两级的远程仓库,本地远程不同repo之间的同步就要更复杂一些,但是这些还是在最基本的同步过程里。图像建立起来了学习很多东西就是自然的,在架子里填东西要比直接堆东西有用得多。

Fork

Fork把别人的远程repo复制到自己的Github中。在Github的开源项目右上角有Fork的按钮,点击后可以将项目复制到自己的Github账号下,fork的repo和原始repo在此时是完全独立的。我们拥有对forked repo的全部权限。

Clone

git clone把远程的repo拷贝到本地。

upstream和origin

当clone一个别人的repo到本地时,如果自己不是这个repo的贡献者,那么是没有权限向其提交修改的。此时该远程的repo相对于clone下来的本地repo来说,就是upstream。

而当在Github上fork一个别人的repo时,此时在本地clone自己fork的repo,自己具有forked repo的全部权限,此时forked repo就是origin,而原始的repo就是upstream。fork可以方便自己对一个repo做出修改。

为Fork的项目添加upstream

Fork一个项目之后,fork的项目和原项目之间就没有联系了,当原项目有更新的时候,自己fork的项目也不会保持同步更新。因此我们需要为fork的项目添加上游仓库,建立两个远程repo的联系。使用git remove -v查看当前的远程状态:

1
2
3
git remote -v
# origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
# origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)

添加upstream:

1
git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git

此时远程状态变为:

1
2
3
4
5
git remote -v
# origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
# origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)
# upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (fetch)
# upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (push)

通过git fetch命令,拉取upstream的版本库到本地:

1
git fetch upstream

此时我们再使用git branch -a查看分支情况时,就会出现三组分支:

1
2
3
4
5
6
7
  develop
* master
remotes/origin/HEAD -> origin/master
remotes/origin/develop
remotes/origin/master
remotes/upstream/develop
remotes/upstream/master

一组是本地的分支情况,接下来是fork的repo的分支情况(remotes/origin),最后是原repo的分支情况(remotes/upstream),repo内分支的排序是按照首字母顺序来的。

假如现在想要同步到upstream的master分支,切换到本地master分支后,merge远程对应分支即可:

1
2
git checkout master
git merge upstream/master

想要同步fork的repo的master分支,把本地的master分支push过去就好了:

1
git push origin master

当然Github上提供了一键同步fork项目的服务。

Git更新代码还有git pull这个命令,不建议用这个命令,当作它不存在就好了。git fetch的作用十分清晰,就是把远程repo对应分支的版本库拉过来,拉去这一过程对本地版本库没有任何影响。要更新本地版本库,另外使用git merge合并即可。

创建分支

一个repo的默认分支是master,也就是主分支,主分支一般用于提交经测试的、稳定的版本。在开发过程中,如果不断提交到主分支,会对软件的Release造成污染。

此时可以建立多个分支,将repo开发的流程分开。分叉的分支之间不受影响,在开发完成后,分支上的修改可以合并到主分支中。借助合理的分支,整个项目的开发过程会清楚很多。

一个典型的项目可能会包含两个分支,一个master分支用于提交稳定的发行版本,一个dev(development)分支用于开发新功能。比如帝国理工开源的PyFR。

执行git branch new_branch可以创建新的分支,git checkout new_branch可以切换到新的分支,或者执行git checkout -b new_branch直接创建并切换到新的分支。

创建远程分支

本地分支和远程分支是独立的。上面执行git checkout -b new_branch是在本地建立并切换到new_branch分支,执行git push origin new_branch将本地新建的new_branc分支推送到远程。

此时在对新分支贡献代码,push到对应远程分支时,必须指定分支名new_branch。可以设定本地分支的默认推送分支:

1
git branch --set-upstream-to=origin/new_branch new_branch

此时将远程的origin/new_branch分支和本地的new_branch分支相关联,再在本地的new_branch分支上执行git push时,默认就会推送到远程的new_branch分支上。

使用git branch -vv可以查看当前本地分支的默认远程分支。

拉取远程分支

在使用git clone一个远程repo时,默认只会clone主分支,如果想clone指定分支,可以使用命令

1
git clone -b develop https://github.com/User/repo.git

如果已经clone了一个repo,现在想拉取远程的指定分支,可以使用命令:

1
git checkout -b new_branch origin/new_branch

关于branch和fork

Git是一个分散型版本管理系统,Github是一个开源平台,提供Git仓库的托管服务,同时还为开发者提供了一系列功能,帮其轻松地与朋友共享代码,进行高效率的代码编写。

Fork并不是一个Git操作,不属于原生Git工具中的一个命令,而是Github额外提供的一个功能,帮助大家把感兴趣的项目克隆到自己的账户中。可以在自己克隆的项目中对原项目进行改进,改进完成后,可以Pull Request,一旦项目的主人接受了了自己的更改,自己的代码就可以合并到主人的原项目中啦。Fork不属于项目本身的一部分,只是你想要对一个不属于你的项目做贡献时,提供方便的一个方式。

branch是一个Git操作,对于一个远程repo,默认有一个master的分支(旧项目的分支是master,现在新repo默认变成main分支),也叫作主干。分支是项目本身的一部分,一个项目本身可能包含多个分支,比如一个master分支用于提交稳定版本,一个develop分支用于开发。分支是项目所有者管理维护项目的一种手段。

即使在Fork的repo中,最好也先新建一个特性分支后再修改代码,在Github上发送PR(Pull Request)时,一般都是发送特性分支。这样一来,PR就有了更加明确的主题。让对方了解自己修改代码的意图,有助于代码审查。

贡献代码

基本框架

Git本地管理,大致可以分为三个区,工作区、暂存区和版本库。

  • 工作区(Working Directory)是直接编辑的地方
  • 暂存区(Stage)数据暂存的地方
  • 版本库(Commit History)记录修改版本记录,push的时候,就是把commit后的数据推送到远程仓库

因此在本地进行开发的时候,总的来说要进行三个步骤,把修改后的文件add到暂存区,执行commit命令本地提交暂存区的内容到版本库,进行版本记录。最后执行push命令把版本库中的内容提交到远程仓库。

git diff查看工作区和暂存区的区别

git diff head查看工作区和版本库的区别

git diff --cached查看暂存区和版本库的区别

在每次准备执行git commit命令进行提交之前,都可以先执行git diff head,确认一下本次提交和上次提交之间的区别。

P.S. 为什么建议用纯文本进行日常开发和工作(markdown, tex, csv, etc.)而不用二进制保存的文件(docx, pptx, xls, etc.)?因为Git的差异比较可以清晰地看出纯文本格式文件的更改记录,而二进制文件是没法查看的。全程用纯文本进行开发和记录,整个项目的细节和发展都可以清晰地保留和比对。

status

git status可以查看当前的分支,对应的远程分支,以及当前的工作区、暂存区、版本库的情况,比对当前提交和远程repo是否相同。

add

如果只是在工作区中创建了文件,该文件并不会被记入Git仓库的版本管理对象当中。因此使用git status命令查看文件夹时,该文件会被标记为Untracked files。要想让文件成为Git仓库的管理对象,就需要用git add命令将它加入到暂存区。执行git add .可以将文件夹中的所有文件添加到暂存区。

即使之前将文件添加到Git仓库中了,如果修改后没有把文件重新add到暂存区,该修改也不会被Git版本库所记录,只有提交到暂存区中的新文件或者修改才能被commit,成为一系列版本历史提交中的一份。

commit

git commit将修改实际提交到版本记录中,对于本地的Git仓库管理来说,到这一步就完成了Git的作用。可以用-m参数来说明提交信息。

push

1
git push <远程主机名> <分支名>

常用的远程主机名就是origin和upstream。

Pull Request(PR)

PR是用户修改代码后向对方仓库发送采纳请求的功能,也是Github的核心功能。PR不属于Git版本管理的一部分,是Github提供的方便用户贡献开源项目的功能。

提交

在Fork仓库、clone到本地、新建分支、commit以及push修改到自己fork的仓库之后,可以在Pull requests下新建PR,选择自己fork的项目的分支,和预计merge到原项目的目标分支,Github会显示这两次提交间的差别。确认无误后,填写请求对方采纳的评论,发送即可,此时原仓库的PR界面下就会出现一个新的PR,项目管理者也会接到通知。

每一个PR都会有一个对应标签页,和Issue一样,也会分配一个编号。下面说的对Issue成立的commit,对PR也适用。

在Github上,可以尽早创建PR,即只要在想发起讨论时,就发送PR,不必等待代码最终完成。即使某个功能尚在开发中,也可以加入Takelist,能清楚地反映出哪些功能已经实现,以及将来要做哪些工作。向发送过PR的分支添加提交时,该提交会自动添加至原repo的对应PR页面中,这也是为什么最好新建一个特征分支后,再开始开发贡献代码。为防止开发到一半的PR被误合并,可以在PR的标题前加上“[WIP]”(Work In Progress),等所有功能都实现后,再消去该前缀。

另外注意不要在同一PR中添加无关的修改,处理与主题无关的作业应另建分支,否则会让原本清晰的讨论变得混乱。

讨论与修改

Issue

Issue是开发者之间交流的工具,当

  • 发现软件的Bug
  • 与开发者询问、讨论
  • 列出今后准备实施的任务

时,可以使用Issue功能。

Issue非常实用的一点是,每一个Issue标题的下面都分配了一个诸如“#24”的编号,只要在commit的提交信息中加入"#24",对应的Issue中就会显示该提交的相关信息。如果一个处于Open状态的Issue已经处理完毕,只要在master分支中,以下列任一种方式描述提交信息,对应的Issue就会被Close,如果提交不是在默认分支,这个Issue将不会被关闭但是在它下面会有一个提示信息。

  • fix #24
  • fixes #24
  • fixd #24
  • close #24
  • closes #24
  • closed #24
  • resolve #24
  • resolves #24
  • resolved #24

在实际项目中用Git分支管理

A successful Git branching model

主分支

一个repo中存在两条主分支:master和develop

master分支永远处在可发布的状态,不可随便更改,一旦merge更改,就应该是稳定的下一版本的软件。

develop分支用作开发,不断向develop分支提交修改,直到达到了一个稳定的状态,可以发布成下一版本的软件了,就把develop分支merge到master分支中,并用版本号(release number)来标记这次merge,作为一个新稳定版本的标志。

这两个主分支的生命周期都是整个项目周期,从项目诞生到结束为止,这两个主分支应当始终存在。

辅助分支

辅助分支来帮助开发过程,和主分支不一样,辅助分支的生命周期有限,一旦它们完成了自己的使命,该分支就merge回主分支,然后删除。

一般来说,辅助分支可以分为三种类型

  • Feature branches
  • Release branches
  • Hotfix branches

这些辅助分支都有不同的用途,并且它们应该严格遵守对应的规范,比如这些分支从哪个分支产生,最终又merge回哪个分支。

Feature branches

Feature branches从develop分支产生,最终merge回develop分支。Feature分支用来开发新功能,一旦新功能开发完毕,这个分支就merge回develop分支,然后被删除。

Feature分支的常用名是feature-xxx,常用的命令为:

1
2
3
4
5
6
7
8
9
10
11
# 从develop分支建立Feature分支
(develop)$: git checkout -b feature-xxx develop
# 开发
(feature-xxx)$: develop...
(feature-xxx)$: git add xxx
(feature-xxx)$: git commit -m 'Feature xxx completed'
(feature-xxx)$: git checkout develop
# 合并分支
(develop)$: git merge feature-xxx --no-ff
(develop)$: git branch -d feature-xxx
(develop)$: git push origin develop

执行git checkout -b new_branch会默认从当前所在的分支长出新的分支,也可以像上面的代码块一样,手动指定从哪个分支长出来。

--no-ff表示禁用Fast forward模式,通常合并分支时,Git会首先采用ff模式,此时在merge分支后会丢掉分支的信息。禁用后,Git就会在merge时生成一个新的commit,这样从提交历史上就可以看到曾经的分支信息,在版本历史中会出现一个分支。

从这里可以看到,在底层分支上,用一系列的add、commit或push进行实际的代码共享,而上层分支用merge来合并代码,主要起到管理和维护的作用。

Release branches

Release branches从develop分支分出来,可能merge回develop branch或master branch,一般命名为release-*,*代表数字。Release branch用于即将发布新版本软件的准备,allow for last-minute dotting,同时用于一些微小bug的修复,发布的元数据的准备等。在Release branch上做完了所有的工作后,可以清空develop branch,开始下一个发布版本的开发工作。

当develop分支中的工作已经几乎可以达到新版本所需的状态时,此时就可以创建新的release分支了。

1
2
3
4
5
6
# 从develop分支建立Release分支
(develop)$: git checkout -b release-1.2 develop

# 元数据修改
(feature-xxx)$: change some meta data on version 1.2...
(feature-xxx)$: git commit -a -m "Bumped version number to 1.2"

-a附加了git add步骤,但只对修改和删除文件有效。

Release branch的生命周期持续到新的稳定版本roll out为止。在生命周期内,bug的修复应当直接在Release branch上进行,而不是提交到develop branch。在这一阶段,必须严格禁止添加新的功能,这种修改只应该提交到develop分支上,直到下一次稳定版本的发布。

当Release branch已经准备好发布了,我们此时可以做一些收尾工作。首先,把Release branch merge到master分支上(master分支的commit必须是新的稳定版本)。同时,master分支上的commit必须被tag,明确标定版本号。最后,把Release分支上做的细小修改merge回develop分支,准备下一阶段的开发。

1
2
3
4
5
6
7
8
9
# master分支
(release)$: git checkout master
(master)$: git merge --no-ff release-1.2
(master)$: git tag -a 1.2

# develop分支
(master)$: git checkout develop
(develop)$: git merge --no-ff release-1.2
(develop)$: git branch -d release-1.2

这里-a是annotated的缩写,Git支持两种标签,轻量标签(lightweight)和附注标签(annotated),轻量标签只是某个特定提交的引用。而附注标签是存储在Git数据库中的一个完整对象,它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间,此外还有一个标签信息,并且可以使用GPG签名并验证。

Hotfix branches

Hotfix branches可能从master分支分出,必须merge回develop分支和master分支,命名为hotfix-*。

当在master上发布的稳定版本发现了重大bug必须立即修复,此时可以从上一个稳定版本创建hotfix分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# bug fix
(master)$: git checkout -b hotfix-1.2.1 master
(hotfix-1.2.1)$: meta data changed to 1.2.1...
(hotfix-1.2.1)$: git commit -a -m "Bumped version number to 1.2.1"
(hotfix-1.2.1)$: bug fix...
(hotfix-1.2.1)$: git commit -m "Fixed severe production problem"

# merged back to master
(hotfix-1.2.1)$: git checkout master
(master)$: git merge --no-ff hotfix-1.2.1
(master)$: git tag -a 1.2.1

# merged back to develop
(master)$: git checkout develop
(develop)$: git merge --no-ff hotfix-1.2.1

# remove temporary branch
(develop)$: git branch -d hotfix-1.2.1

总结

整个项目的开发流程如上图所示。作为一个python-based科学计算方面的开源项目,帝国理工PyFR的层次架构以及版本管理都是值得学习的,很整洁。