开源项目参与流程

在开源项目或团队协作开发中,使用 GitHub 是一个常见的实践。以下是一套从 Fork 项目到提交 Pull Request(PR)的完整流程,帮助你高效管理代码并与团队或社区协作。

1. Fork 项目仓库

进入项目的 GitHub 页面,点击右上角的 Fork 按钮,将该项目仓库复制到自己的 GitHub 账户下。这个 Fork 的副本是你对项目的个人副本,你可以在上面进行改动而不会影响原项目。

2. Clone 项目到本地

在你的 GitHub 账户中找到刚刚 Fork 的仓库,复制仓库的链接。在本地执行以下命令将仓库克隆到你的电脑:

1
git clone <your-forked-repo-url>

3. 添加上游仓库 (Upstream)

当你 Fork 了一个仓库后,你的代码会与原项目(官方项目)脱离直接联系。为了能够同步原项目的最新更改,你需要将原项目添加为“上游仓库”。进入克隆的项目文件夹,运行以下命令:

1
git remote add upstream <original-repo-url>

然后验证 git remote -v,出现类似下面的内容:

1
2
3
4
origin  https://github.com/your-username/your-repo.git (fetch)
origin https://github.com/your-username/your-repo.git (push)
upstream https://github.com/original-user/original-repo.git (fetch)
upstream https://github.com/original-user/original-repo.git (push)

4. 创建新分支

在协作开发中,不要在 main 分支上直接进行改动。每个功能或修复都应该单独创建一个分支,便于管理和代码评审。

1
git checkout -b feature-branch-name

命名规范:

  • 功能分支:add-feature-name,例如 add-login-feature
  • 修复分支:fix-issue-name,例如 fix-login-error
  • 实验分支:test-experiment-name,例如 test-algorithm-optimization

5. 同步上游仓库的更新

在开始改动之前,确保你的代码基于上游的最新版本,避免提交包含旧代码或引发冲突。切换到你的分支,然后拉取上游项目的最新更新。

1
2
git checkout <branch>
git fetch upstream

随后有2个选择。使用 git merge upstream/main,将上游分支的改动合并到当前分支,生成一个额外的 merge commit,用于记录合并过程。如果发生冲突,所有冲突会集中到一次解决,并记录在 merge commit 中。提交历史保留分叉点,便于追踪合并记录。

1
2
3
A --- B --- C (upstream/main)
\
D --- E --- M (merge commit)

快速同步,操作简单。适合开发过程中本地更新,但不适合提交 PR

使用 git rebase upstream/main,会将你的分支的提交(如 D 和 E)“摘下”,然后重新应用到 upstream/main 的最新提交后面,生成新的提交。不会产生 merge commit,历史保持线性,最终结果类似: A --- B --- C --- D' --- E'
推荐用于 PR,因为:

  1. 提交历史更整洁:不引入额外的 merge commit,适合开源贡献。
  2. 冲突处理细化:每个提交都会单独处理与上游代码的冲突。
  3. 重复操作安全性:多次 rebase 不会产生混乱,每次都会让你的提交位于上游分支的最新代码之后。

多次执行 git rebase upstream/main 会让你所有的提交都重新排列在官方分支的最新提交之后。比如说下面的操作得到的结果是 A --- B --- E --- C'' --- D'' --- F'。这是因为你的本地提交会被摘下,然后调整顺序后再附加到末尾。(这里的字母表示基于共同起点,新增的commit)。

  1. 第一次 Rebase:C 和 D 提交。上游分支 (upstream/main): A — B,你的分支 (feature): C — D。本地结果是 A — B — C’ — D’ (feature)。
  2. 官方增加 E 后,第二次 Rebase:F的提交。上游分支 (upstream/main): A — B — E,你的分支 (feature): A — B — C’ — D’ — F。本地结果是 A — B — E — C’’ — D’’ — F’。

一般做法:在提交 PR 前,始终建议使用 git rebase upstream/main 来保持历史的整洁和线性化。 如果官方分支频繁更新,定期执行 rebase,确保你的分支与官方最新代码一致。

6. 进行改动并提交

按照项目的规范进行代码更改、添加功能或修复 bug。完成后,将改动添加并提交:

1
git commit -m "清晰的提交信息"

示例提交信息:

  • 修复 (Fix):修复 Bug 或问题,例如 Fix login error in login page
  • 添加 (Add):添加新功能或模块,例如 Add user authentication module
  • 修改 (Update):改进已有功能或文档,例如 Update README with setup instructions
  • 重构 (Refactor):代码结构调整,例如 Refactor authentication module for clarity
  • 删除 (Remove):删除不需要的代码或文件,例如 Remove deprecated API call

如果项目涉及到的文件比较多,那么可以加入前缀,比如在哪个包内做的修改。

7. 将更改推送到 Fork 的仓库

推送到远程分支:

1
git push origin feature-branch-name

8. 创建 Pull Request (PR)

  1. 回到你的 GitHub 仓库页面,会看到一个 Compare & Pull Request 的按钮,点击它。
  2. 在 Pull Request 页面中,填写你的改动描述,包括做了哪些更改、为什么进行这些更改等。
  3. 提交 Pull Request,等待项目维护者的审核。

9. 与项目维护者沟通

项目维护者可能会对你的 Pull Request 提出建议或要求你修改。根据反馈进行调整,并再次提交到你的分支。更新 PR 时不需要重新创建 PR,直接推送到同一个分支即可 git push origin <branch-name>

提交的最佳实践

在协作开发中,良好的提交习惯可以大幅提升团队效率,同时让代码历史更加清晰,便于代码审查和问题追踪。

提交时机

一般来说,每个提交应当专注于一个小而清晰的更改,以下情况是理想的提交时机:

  • 完成一个小的功能或逻辑:如实现一个函数、模块或特定的功能。
  • 修复一个 Bug:针对特定问题的修复应单独提交,以便追踪问题。
  • 文档更新:更新 README、注释或其他文档内容时,单独提交。
  • 代码重构:调整代码结构但不影响功能时,例如提高可读性或优化性能。
  • 测试用例更新:添加或修改测试用例以验证功能的正确性。

避免以下频繁提交的情况:

  • 过于细碎的改动:如仅修改一行代码,除非是重要的修复。
  • 未经测试的代码:在提交之前,应尽量保证代码稳定,或明确标记为“临时提交”以避免误导。

提交原则

保持提交的原子性

一个提交完成一项具体的任务:避免将多个不相关的改动混合在一个提交中,避免“万能提交”。
示例:

  • 错误示例:将 Bug 修复和功能开发放在一个提交中。
  • 正确示例:先提交 Fix login error,再提交 Add JWT authentication

定期同步上游代码

如果官方分支有更新, 这样可以减少冲突,并确保提交基于最新代码开发。使用以下命令将最新代码与本地分支同步:

1
2
git fetch upstream
git rebase upstream/main

在本地充分测试

提交前应在本地运行测试,确保改动不会破坏现有功能。如果改动无法完全验证,应明确标记为实验性或草稿提交。

避免提交多余文件

提交前检查是否有无关的临时文件或调试代码。使用 .gitignore 忽略不需要的文件,避免意外提交。

描述清晰的提交信息

提交信息应简明扼要,清楚地描述改动内容和原因。以动词开头,如 Fix、Add、Update 等,便于他人快速理解。

清晰的提交信息有助于团队快速了解改动内容。推荐以下格式:

1
2
3
4
5
6
7
8
9
10
11
<类型>: <简短描述>

[可选详细描述]

<类型> 包括:

- Fix: 修复问题
- Add: 添加功能
- Update: 修改功能或文档
- Refactor: 重构代码
- Remove: 删除内容

示例提交信息:

  1. 功能添加。
1
2
3
4
Add JWT authentication for login module

- Implement token-based authentication.
- Add middleware to validate tokens in requests.
  1. Bug 修复:
1
2
3
Fix null pointer exception in login module

- Resolve null pointer issue caused by uninitialized variable.
  1. 文档更新:
1
2
3
Update README with setup instructions

- Add instructions for setting up local development environment.

总结

  1. 保持提交小而清晰:每个提交完成一个明确任务,避免提交过大或内容混杂。
  2. 定期同步代码:在官方分支更新后及时 rebase,减少冲突。
  3. 提交前充分测试:验证改动稳定性,确保代码不会破坏现有功能。
  4. 清晰描述提交信息:让每个提交的目的和内容一目了然,方便协作者审查。
  5. 通过遵循这些最佳实践,你的提交记录将更加有条理,协作者能够更高效地审查和管理代码,同时增强整个代码库的可维护性。

内部项目开发

当团队成员对主分支(main 或 develop)有直接提交权限时,需要特别注意代码质量、提交规范以及协作流程,避免代码冲突或影响项目稳定性。

分支策略要清晰:

主分支 (main):仅存放稳定的代码版本。应通过代码评审或 CI/CD 流程自动合并,避免直接提交。
开发分支 (develop):用于集成团队的开发代码。可以直接提交,但要确保不影响其他人开发。
功能分支 (feature-branch):每个功能、任务或 Bug 修复应该单独创建一个分支。

  1. 功能的添加、Bug 修复等依然推荐单独创建分支。完成后将分支合并到 develop 或通过 Pull Request (PR) 合并到目标分支,便于管理。
  2. 功能模块化管理,使用功能分支,确保每个功能改动独立,便于后续追溯和回滚。同一时间尽量不要多人修改同一模块,开发团队要多沟通,分配明确的开发任务,避免冲突。
  3. 实现最小化的测试函数。每个新功能或修复提交时,必须附带最小化的测试函数,确保业务逻辑正确。检查 1. 新增的核心功能(如 API 请求返回结果是否正确);2. 对现有功能的影响(回归测试)。并且比避免测试函数失败的代码提交。

Git 常用命令

怎么提交

git commit -m:仅提交已在暂存区的文件,不影响未暂存的文件。
git commit -am:将所有已跟踪文件的改动添加到暂存区并提交,但不包含新文件。

合并 Commit

场景一:多个commit合并

  1. 找到目标提交的哈希值:首先,使用以下命令查看提交历史,找到要保留的提交(即你希望将后续更改合并到的提交)。git log --oneline

  2. 重置到指定提交:使用 git reset 将分支回退到指定提交。这样会取消指定提交后的所有提交,但保留文件改动在工作区中。git reset --soft <commit-hash>
    其中 <commit-hash> 是目标提交的哈希值,例如 abc1234。
    –soft 参数确保保留所有更改在暂存区,以便进行下一步的合并提交。hard 就会删除,请不要使用!

  3. 重新提交所有更改:现在,所有更改都在暂存区中,你可以将它们合并为一个新的提交。git commit -m "将所有更改合并为一次提交"

场景二:将未提交的更改合并到上一次提交。
如果在最近的提交之后还有未提交的更改,并希望将它们合并到该提交中,可以使用 git commit --amend。

  1. 暂存未提交的更改:首先,将所有未提交的更改添加到暂存区。git add .
  2. 使用 --amend 更新上一个提交:将这些暂存的更改添加到上一个提交中。git commit --amend -m "更新上一个提交的信息"
  3. 这将打开编辑器,允许你修改上一次提交的信息。如果你希望保留原始信息,只需保存并关闭编辑器即可。

要复原某个文件或者上次提交

场景一:将文件从暂存区移除,但保留在工作区中,改动还存在,但是 git 不记录。git reset <filename>
场景二:直接复原文件到上次提交。git checkout -- <filename>
场景三:直接复原整个项目到上次提交。git reset --hard HEAD

合并修改,建议在 IDE 里进行,很方便。更多的遇到的时候再学习吧。

使用Git 模块

Git 子模块用于管理嵌套的仓库,通常用于依赖项目或共享代码。子模块在主仓库中的文件夹表现为普通目录,但其内容实际上是独立的 Git 仓库。

子模块在 Git 中通过一些特殊文件和目录进行管理。这些文件记录了子模块的配置信息、关联的远程仓库、当前版本等。

.gitmodules 是一个配置文件,用于描述主仓库中的子模块信息,包含子模块的路径、远程仓库 URL 等。子模块在 .gitmodules 文件中配置后,初始化时会将其信息写入 .git/config

当子模块被初始化后,Git 会在主仓库的 .git/modules 目录下为每个子模块创建一个子目录。这些目录存储子模块的元数据,包括配置、对象和引用信息。

  1. 克隆包含子模块的仓库 git clone --recursive <repository-url>
  2. 添加子模块 git submodule add <submodule-repo-url> <path>
  3. 更新子模块git submodule update --remote
  4. 移除子模块
1
2
3
git submodule deinit -f <path>
rm -rf .git/modules/<path>
git rm -f <path>

一般用途:

  1. 依赖管理:如项目需要多个共享库(API、工具库)。
  2. 大型项目:将不同模块分成多个独立的子仓库,方便团队协作。
  3. 外部代码集成:如集成开源项目。

常见问题1:clone 时忘记使用 --recursive。可以通过这样补充

1
git submodule update --init --recursive
  1. 初始化子模块:如果子模块未被初始化(即 .git/modules 中没有子模块的相关信息),这个选项会初始化子模块并将其拉取到本地。
  2. 递归处理嵌套子模块:如果子模块本身还有嵌套的子模块,此选项会确保所有嵌套的子模块也被初始化并更新。

常见问题2:需要的版本不一致,拉取模块的指定版本。

1
git submodule update --recursive

常见问题3:更新模块到最新

  1. 进入子模块目录:cd submodule1

  2. 切换到远程分支并拉取最新代码:

    1
    2
    git checkout main
    git pull origin main
  3. 返回主仓库,并更新子模块的版本记录:

    1
    2
    3
    cd ..
    git add submodule1
    git commit -m "Update submodule1 to the latest version"

本地忽视文件

在 Git 中,想要排除一些文件但又不影响其他人,可以通过使用本地的 .git/info/exclude 文件。打开 打开 .git/info/exclude 文件,然后里面每一行都会像是 .gitignore 一样,忽视对应的文件。

1
echo ".python-version" >> .git/info/exclude