Skip to content

第三讲:开源世界生存基础

Tip

建议同学们携带笔记本电脑,并在课前按照 2.2 在自己的电脑上安装好 Git,以便课上实践。

What & Why

开源(Open Source)是指将源代码公开到互联网上,任何人都可以在开源许可证(License)的约束下查看、修改甚至重新发布代码。

为了便于管理和协作,通常会使用版本控制系统(Version Control Systems,VCS)来管理代码,其中最流行的工具是 Git。与此同时,既然代码是公开的,就需要一个代码托管平台来存储和管理代码,最为知名的平台是 GitHub。考虑到国内的网络环境,本节课将以浙江大学超算队提供的 ZJU Git 为示范平台。

Example

  • 本节课的讲义其实就是开源的,你可以在 GitHub ckc-agc/study-assist 仓库中找到本课程的所有源代码。
  • 大家使用的大部分浏览器,例如 Chrome、Edge、Arc、360 极速浏览器,都是基于开源项目 Chromium 开发的。后者的源代码托管在谷歌自己的代码托管平台 Google Git 上,你可以在这里查看 Chromium 的源代码(因为众所周知的原因你可能需要魔法才能访问这里还有它的 GitHub 镜像。
  • 著名的操作系统 Linux 也是开源的,它托管于 Linus Torvalds GitHub 仓库中。

很好,开源看上去很酷,但我为什么要学它呢?

  • 最现实的原因:你躲不掉的。朱 🎳 学长在上节课提到过,早在大一下学期《数据结构基础》课程开始,就有老师要求使用 Git 等工具来管理你的代码。
  • 因为确实好用,掌握 Git 后你会觉得相见恨晚,它和其他版本控制方法相比简直是降维打击
  • 因为开源世界超好玩的~如果某个项目你觉得有问题,你可以直接向开发者提出修改建议;如果你觉得某个项目很棒,你可以直接参与进去。这种自由开放的氛围是无法比拟的,你甚至可以 GitHub 上吃瓜

很棒!那我们开始吧!

Git:最优秀的版本控制工具

你是否遇到过这样的情景?

git_1

更进一步,当你高高兴兴地写完了报告,刚把各种备份文件删除,回收站清空,然后导师突发奇想,告诉你他觉得以前的版本更好,叫你回退以前的版本,这时你的心情是什么样的?

Git 就是为了解决这种问题而生的。通过高效的数据结构,它可以帮你记录文件的自创建以来的每一个版本,甚至允许你在不同版本之间自由切换

远古时期的 Linux 代码

你甚至可以找到 Linux 内核在 GitHub 上的第一次提交(实际上,因为年代过于久远,这个提交在 GitHub 提交列表上是隐藏的,感兴趣的同学可以自己试试看看能不能把它找出来😉)

git_2
梦开始的地方

历史

Linus Torvalds 在开发 Linux 内核时,原本使用的版本管理系统叫 BitKeeper。但是由于当时 BitKeeper 的免费版本加入了限制,他决定开发一款自由免费的版本管理系统,顺带解决一下历代 VCS 的缺陷,于是 Git 诞生了。

  • 2005 4 8 日,Git 实现自托管

    git_3
    梦开始的地方开始的地方

  • 10 天后,Linux 内核的开发就转向了 GitFig. 梦开始的地方)

Git 安装及配置

  • Linux / macOS:大部分 Linux 发行版和 macOS 都自带 Git。如果没有,可以通过包管理器直接安装:apt install git / brew install git / ...
  • Windows:访问 git-scm.com/downloads/win,下载 64 位版本(64-bit Git for Windows Setup,以默认选项安装即可。

打开终端,输入 git --version,如果正常输出版本号,则说明安装成功。

接下来,我们需要配置 Git 的一些基本信息,例如用户名和邮箱。

git config --global user.name "<你的用户名>"
git config --global user.email "<你的邮箱>"

Git 的数据模型

Git 的交互指令出发很容易让人听得一头雾水,所以我们从底层出发,自下而上地学习 Git

model
图片来源:TonyCrane 的讲义

Git 启动时,会在当前目录下创建一个名为 .git隐藏文件夹,这个文件夹就是 Git 版本库(Repository

版本库中,保存着所有文件的历史(History,以及一个叫暂存区(Stage)的东西。

快照

为了记录文件的历史,Git 会给每一个版本创建一个快照(Snapshot,又称 Commit提交,不同的快照之间通过一种叫做有向无环图(Directed Acyclic Graph,DAG)的数据结构(大家会在大一下《数据结构基础》的课程中学到)联系起来。

history_1
一种可能的历史
若未特殊说明,本讲义中手绘风格示意图均为原创,转载请注明出处

在上面的例子中,历史在 C 快照处产生了两个分支(Branch,并在 I 快照处合并(Merge。这种分支和合并的操作是 Git 的强大之处,我们会在后面详细讲解。

Git 中,每一个快照都由一个十六进制数唯一标识,它相当于快照的 ID。通常只需要前几位就可以唯一确定一个快照,例如之前图片中的 1da177e。但很显然,这种十六进制数不适合人类记忆。因此 Git 允许给快照创建引用(Reference,它是一个指向快照的指针,例如人们通常使用 mastermain 等名字来表示最新的快照。

Git 有一个特殊的指针叫做 HEAD,它指向当前所在的分支或者快照,也就是当前正在工作的版本。

history_2
指向分支的指针

通过这种方式,你可以很方便地在不同快照之间切换

暂存区

你可能会想象,每次提交都会对当前工作目录(Working Directory)打一个快照,然后保存到版本库中,但实际上并非如此。

设想这样一个场景:你同时修改了两个文件,但只想提交其中一个,这时候就需要一个中间状态来保存你的修改。这个中间状态就是所谓的暂存区

stage
图片来源:TonyCrane 的讲义

基础用法

Let's get our hands dirty!

git_xkcd
图片来源:XKCD GIT

初始化版本库

快速回顾

Linux / macOS 系统中,你可以使用 ls 命令查看当前目录下的文件和文件夹,使用 cd 命令切换目录。

对于 Windows 系统,这里推荐大家使用系统自带的 Windows Terminal 进行实验,其运行的默认终端是 Powershell,使用习惯更接近 Linux(相较于 cmd 而言

  • 按下 Win + R,输入 wt,回车即可打开 Windows Terminal
  • Linux 系统中查看当前目录隐藏文件的命令是 ls -a,在 Powershell 中是 ls -Hidden(简写 ls -h

要使用 Git,首先需要创建一个 Git 版本库,这个过程是通过 git init 命令完成的。

动手做:初始化版本库

在自己的电脑上打开终端,创建一个新的文件夹(mkdir 命令,然后进入这个文件夹(cd 命令,并使用 git init 初始化一个 Git 版本库。

mkdir my-repo
cd my-repo
git init

尝试使用 ls 命令查看当前目录的内容,能找到 .git 文件夹吗?为什么?

年轻人的第一个 Commit

现在我们已经有了一个空的 Git 版本库,接下来我们尝试往里面添加一些文件。

动手做:一些准备工作

VSCode 打开这个文件夹,创建一个 hello.c 文件,内容如下:

#include <stdio.h>

int main() {
    printf("Hello World");
    return 0;
}

这是一个简单的 Hello World 程序,实际使用中的场景可能是你的实验报告、项目代码等,这里只是为了演示。

现在的工作目录中有了一个 hello.c 文件,我们可以使用 git status 命令查看当前版本库的状态。

git status

查看当前工作区和暂存库的状态

文件有三个类别:未跟踪(Untracked已追踪(Tracked被忽略(Ignored

动手做 2.4.1

运行 git status 命令,你看到了什么?你能够解释这些信息吗?

git add <FILE>

将所选的文件或文件夹添加到暂存区

动手做 2.4.2

hello.c 添加到暂存区。这个时候再次运行 git status,看看有什么变化。

思考:如果我想一次性添加多个文件,或者说添加所有文件,应该怎么做呢?

git commit -m "<MESSAGE>"

将暂存区的文件提交到版本库-m 参数后面是提交的信息,用于描述这次提交的内容。

git log

查看版本库的历史

动手做 2.4.3

提交 hello.c 文件到版本库,提交信息可以是任意的,例如 Add hello.c

git commit -m "Add hello.c"

试一试 git log 命令,你能解释这些信息吗?

🎉 恭喜你完成了自己的第一个 Commit!下面让我们多添加一些文件。

动手做:年轻人的第 i Commit(i = 2, 3, ...)

一个优秀的项目只有代码怎么行?让我们添加一个介绍文档吧!创建 README.txt,内容如下:

hello.c:一个简单的 Hello World 程序(真的很简单!)。

然后提交这个文件。再次运行 git log,你看到了什么?

试着修改创建删除,看看 git status,用 git diff 查看当前工作区和暂存区的差异,然后提交这些修改。

除此之外,还有一些其他常用的命令:

  • git rm:同时删除本地版本库中的文件(等价于rm + git add
  • git rm --cached:将一个已暂存的文件取消暂存
  • git mv重命名文件(等价于mv +git rm + git add
  • git log 的一些参数:
    • --oneline:在一行中显示。
    • --graph:显示分支结构
    • --stat:显示文件的删改信息
    • --all:显示所有分支的历史(默认只显示当前分支)
    • 参数可以组合使用,例如 git log --all --graph --oneline
  • git diff <A> <B>比较两个快照之间的差异,<A><B> 可以是 Commit IDID 简写、引用、HEAD、文件名等。
  • git show <COMMIT>:查看某次提交的详细信息<COMMIT> 同上。
  • git revert <COMMIT>:创建一个新的提交,撤销某次提交的修改。

动手做:玩一玩

试着使用上面提到的命令,玩一玩 Git 吧!如果有什么问题,欢迎随时提问。

分支

分支(Branch)是一个非常重要的概念。它允许你在不影响主线的情况下进行开发,然后再将你的工作合并到主线上。

  • 创建分支
    • git branch <BRANCH>:基于当前的 HEAD
    • git branch <BRANCH> <COMMIT>:基于某个快照。
  • 查看分支
    • git branch:查看本地分支。
    • git show-branch(更加详细)
  • 切换分支检出
    • git checkout <BRANCH>:切换到某个分支。
    • git checkout -b <BRANCH>:创建并切换到某个分支。
  • 删除分支
    • git branch -d <BRANCH>:删除本地分支。

动手做:分支

假如说我想给我们的 hello.c 添加一个函数,但是由于这个函数很复杂,可能会断断续续修改很长时间,所以我不想影响到主线的开发。这时候就可以创建一个新的分支,然后在这个分支上与主线平行开发

试着创建一个 dev 分支,切换进去,然后在 hello.c 中添加一个 print_hello() 函数:

void print_hello() {
    printf("Hello from dev branch!");
}

然后提交这个修改。再运行 git log(你可以同时试一试它的各种参数,你看到了什么?在脑海里想一想当前的分支结构。

切换回主分支(master 或者 mainhello.c 里面有这个函数吗?为什么?

Answer

branch

Detached HEAD 问题

所谓 Detached HEAD,指的是 HEAD 指向的不是一个分支,而是一个具体的快照

如果在这种情况下进行修改并提交,新的提交不属于任何分支,它只能通过 Commit ID 来访问,相当于丢失了。

detached
图片来源:TonyCrane 的讲义

演示:Detached HEAD

使用 git log 查看历史,找到一个 Commit ID,然后使用 git checkout <ID> 切换到这个快照,你看到了什么?

试着修改这个快照,提交,然后使用 checkout 回到主分支,你还能找到这个修改吗?

如果我想保留这个修改,应该怎么做?

合并

git merge <BRANCH1> [<BRANCH2> ...] -m "<MESSAGE>"

将一个或多个分支合并到当前分支。合并本身也是一次提交,-m 参数后面是提交信息。

  • Already up-to-date:当前分支只比要合并的分支,不需要合并。
  • Fast-forward:要合并的分支只比当前分支,只需要挪动指针即可,不需要新的提交。
  • 如果都有新的提交,Git 会尝试自动合并,如果有冲突(Merge Conflict,需要手动解决(解决冲突后需要再次add + commit
merge_1
图片来源:TonyCrane 的讲义

动手做:合并冲突

在主分支的 hello.c 中也添加一个 print_hello() 函数(真实情况下,可能是你在 devmain 中同时修改了某个配置文件,或者两个人不经意间同时修改了同一个文件

void print_hello() {
    printf("Hello from main branch!");
}

提交这个修改,然后尝试合并 dev 分支。你成功了吗?如果没有,你应该怎么做?

试着解决这个冲突,然后提交 merge

实际上,Merge 还有很多其他的策略。

  • Squash Merge:将多个 Commit 合并为一个。
  • Rebase变基,将当前分支的历史平移到目标分支,可以使得历史更加线性(会篡改历史,不推荐多人协作时使用)
  • GitHub RebaseGitHub 提供的一种 Rebase 方式,将目标分支在当前分支上重放。
merge_2
图片来源:TonyCrane 的讲义

篡改历史

成为历史的罪人

Git 的历史一般情况下是不可篡改的,但实际上还是有一些方法可以对历史进行修改:

  • git commit --amend:修改最新的提交 message
  • git reset <COMMIT>回退到某个快照(你可能更应该使用更加温和的 git revert
  • git rebase -i <COMMIT>:交互式 Rebase,功能非常强大,不到万不得已不要使用!

edit_history

VSCode 中使用 Git

Visual Studio Code 默认集成了 Git,点击左侧源代码管理(Source Control)按钮即可打开图形化的 Git 界面。

动手做:玩一玩

VSCode 打开看看你刚刚创建的 Git 项目吧~

.gitignore 文件

在实际开发中,有一些文件是不需要纳入版本库的,例如编译生成的文件、日志文件、缓存文件等。这时候就需要在根目录下创建一个 .gitignore 文件。

.gitignore 文件是一个文本文件,每一行是一个匹配规则,例如:

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
cache

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

*.tsbuildinfo
.vercel

语法:

  • # 开头的行是注释
  • * 代表通配符,匹配多个字符。例如 *.c 匹配所有 .c 结尾的文件。
  • ** 通配多个目录。例如 a/**/b 匹配 a/ba/x/ba/x/y/b 等。
  • / 开头只匹配根目录,否则匹配任意目录
  • ! 开头是取消忽略

官方文档:git-scm.com/docs/gitignoreGitHub 也提供了一个模板

GitHub / ZJU Git 基础

设想多人协作的场景,每个人都在本地有自己的版本库,那如何实现同步呢?

答案是每个人都使用一个“权威”的远程版本库(Remote Repository

远程版本库

remote
图片来源:TonyCrane 的讲义

远程版本库也是一个普通的 Git 版本库,只不过由于不需要工作目录和暂存区,所以一般使用裸版本库

  • git clone <SRC> <DEST>克隆一个远程版本库到本地,会自动添加一个名为 origin 的远程版本库,可以通过 git remote 管理。
  • git push:将本地的提交推送到远程版本库。
  • git pull:将远程版本库的提交拉取到本地,等价于git fetch + git merge

远程版本库可以简单地理解成是本地的一个 origin/main 分支。

origin

ZJU Git

作为一个全球顶尖的三本,怎么能没有一个自己的 Git 服务器呢?

网址:git.zju.edu.cn,只能通过校网或 WebVPN 访问。

注意区分

ZJU GitGitHub 是平行的关系,它们提供类似的功能(非常类似,都可以作为 Git 远程版本库

不要把 ZJU Git / GitHub Git 混淆!

注册账号

使用统一身份认证登录。

设想一下,ZJU Git 如何确定你的身份呢?

  • 本地 Git 用户名邮箱要和 ZJU Git 匹配:你可能需要重新设置本地的用户名和邮箱。
  • 建立本地与远程的身份验证机制:使用 SSH 密钥

SSH 密钥

SSH 密钥采用非对称加密算法,它包括公钥私钥两部分。

  • 公钥:用于加密,可以公开。
  • 私钥:用于解密,绝对不能泄露!

通过 SSH 密钥,可以与远程服务器建立安全的连接。

动手做:设置 SSH 密钥

打开 Git Bash,输入以下命令:

ssh-keygen -t ed25519 -C "<你的邮箱>"

它会向你询问密钥的保存位置和密码,直接回车使用默认值即可。

这条指令会在 ~/.ssh 目录(对于 Windows,是 C:/Users/<用户名>/.ssh)下生成两个文件:id_ed25519id_ed25519.pub,前者是私钥,后者是公钥

用文本编辑器打开 id_ed25519.pub,复制里面的内容,然后在 ZJU Git 的设置中添加 SSH 密钥即可。

创建仓库

动手做:创建仓库

ZJU Git 上创建一个新的仓库,然后将本地的 Git 项目推送到这个仓库。

git remote add origin <你的仓库地址>
git branch -M main
git push -uf origin main

回到 ZJU Git 网站,看看你的仓库吧!

  • git remote add origin <地址>:添加一个名为 origin 的远程版本库。
  • git branch -M main:将当前分支重命名为 main
    • -M强制重命名,如果分支已经存在,会覆盖。
  • git push -uf origin main:将本地的 main 分支推送到远程 origin 版本库中。
    • -u设置上游分支,意味着之后可以直接使用 git pushgit pull
    • -f强制推送,会覆盖远程版本库的内容(其他情况下不要使用

github_meme

动手做:提交更新

在本地修改 hello.c,然后把它提交到 ZJU Git 上。

还记得要用哪些指令吗addcommitpush

多人协作

  • Fork:你可以复制一个仓库到你自己的账号下,这个操作叫做 Fork,你可以在这个仓库上自由地修改、提交。
  • Pull RequestPR:当你修改完一个仓库后,你可以向原仓库提交一个 Pull Request,请求原仓库的所有者合并你的修改。

动手做:年轻人的第一个 PR

Fork 这个仓库:lec3-git

然后 cd 到一个新的文件夹,使用 git clone 克隆你 Fork 的仓库。

随便改一些东西吧~然后把这些修改提交到你 Fork 的仓库。

最后,向原仓库提交一个 PR,请求合并你的修改。

GitHub 简介

所谓 GitHub,就是 Git Hub我在说什么

GitHub 是全球最大的代码托管平台,拥有数亿的开发者截至 2023 1 )和数以亿计的代码仓库。

课后:注册一个 GitHub 账号

注册一个自己的 GitHub 账号吧~(你可能需要魔法)

打开我们现在这个网站的 GitHub 仓库,看一看 Commit 记录,体会一下真实项目中的团队协作吧!如果你觉得不错,不妨给我们一个 Star ⭐。

同时推荐 TonyCrane 学长的 Slides,有更多关于 GitHub 的详细的介绍。

开源项目基础

许可证

公开源代码不代表你可以拿它做任何想做的事,这个时候就需要许可证(License

常用软件开源许可证

  • MIT:非常宽松的许可证,几乎没有限制。
  • GPL:GNU General Public License,有传染性,要求派生作品也必须开源。
  • Unlicense:放弃所有权利,代表进入公共领域

参考 choosealicense.com

license
图片来源:阮一峰《如何选择开源许可证?》

如果没有许可证呢?

原作者保留所有权利,不允许复制、分发、修改,使用的话需要联系原作者(注意和 Unlicense 区分)

侵权是非常严肃的事情!

原作者有权对你的项目进行 DMCA Takedown(DMCA《千禧年数字著作权法案,甚至通过法律途径追究责任!

案例:

在根目录下创建名为 LICENSE 的文件,然后在其中附上许可证的内容。如果不同部分采用了不同的许可证,那么需要在每个文件的开头注明。

GitHub 能够自动识别常见的许可证类型,并在仓库的主页上显示。

除此之外,还有非软件类许可证,最常使用的是 CCCreative Commons,知识共享)系列许可证,目前最广泛使用的版本是 4.0,它包含如下几种:

  • CC 0:放弃所有权,进入公共领域
  • CC BYBY 表示必须署名
  • CC BY-SASA 表示必须使用相同许可证(Share-Alike
  • CC BY-NCNC 表示禁止商业用途(Non-Commercial
  • CC BY-NC-SA:三个要求都有。
  • CC BY-NDND 表示禁止分发、修改(No-Derivatives,禁止演绎
  • CC BY-NC-ND:三个要求都有。

往往通过一段文字即可表示许可:

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

思考

这篇文章也是通过 CC 许可证发布的,你能找到它的许可证吗?

结语

通过今天的学习,想必你已经对开源有了更深的了解,欢迎你加入开源的大家庭!

一些学习资源:

下一次课,李英琦学长将为大家带来 Markdown LaTeX 等排版相关的内容(实际上这个讲义就是用 Markdown 写的,敬请期待~