第三讲:开源世界生存基础 ¶
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 就是为了解决这种问题而生的。通过高效的数据结构,它可以帮你记录文件的自创建以来的每一个版本,甚至允许你在不同版本之间自由切换。
远古时期的 Linux 代码
你甚至可以找到 Linux 内核在 GitHub 上的第一次提交
历史 ¶
Linus Torvalds 在开发 Linux 内核时,原本使用的版本管理系统叫 BitKeeper。但是由于当时 BitKeeper 的免费版本加入了限制,他决定开发一款自由、免费的版本管理系统,顺带解决一下历代 VCS 的缺陷,于是 Git 诞生了。
-
2005 年 4 月 8 日,Git 实现自托管。
-
10 天后,Linux 内核的开发就转向了 Git
。 (Fig. 梦开始的地方)
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。
Git 启动时,会在当前目录下创建一个名为 .git
的隐藏文件夹,这个文件夹就是 Git 的版本库(Repository
版本库中,保存着所有文件的历史(History
快照 ¶
为了记录文件的历史,Git 会给每一个版本创建一个快照(Snapshot,又称 Commit、提交
在上面的例子中,历史在 C 快照处产生了两个分支(Branch
在 Git 中,每一个快照都由一个十六进制数唯一标识,它相当于快照的 ID。通常只需要前几位就可以唯一确定一个快照,例如之前图片中的 1da177e
。但很显然,这种十六进制数不适合人类记忆。因此 Git 允许给快照创建引用(Referencemaster
、main
等名字来表示最新的快照。
Git 有一个特殊的指针叫做 HEAD
,它指向当前所在的分支或者快照,也就是当前正在工作的版本。
通过这种方式,你可以很方便地在不同快照之间切换。
暂存区 ¶
你可能会想象,每次提交都会对当前工作目录(Working Directory)打一个快照,然后保存到版本库中,但实际上并非如此。
设想这样一个场景:你同时修改了两个文件,但只想提交其中一个,这时候就需要一个中间状态来保存你的修改。这个中间状态就是所谓的暂存区。
基础用法 ¶
Let's get our hands dirty!
初始化版本库 ¶
快速回顾
在 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
动手做 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 ID、ID 简写、引用、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
或者 main
hello.c
里面有这个函数吗?为什么?
Detached HEAD 问题 ¶
所谓 Detached HEAD,指的是 HEAD
指向的不是一个分支,而是一个具体的快照。
如果在这种情况下进行修改并提交,新的提交不属于任何分支,它只能通过 Commit ID 来访问,相当于丢失了。
演示: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
) 。
动手做:合并冲突
在主分支的 hello.c
中也添加一个 print_hello()
函数(真实情况下,可能是你在 dev
和 main
中同时修改了某个配置文件,或者两个人不经意间同时修改了同一个文件
void print_hello() {
printf("Hello from main branch!");
}
提交这个修改,然后尝试合并 dev
分支。你成功了吗?如果没有,你应该怎么做?
试着解决这个冲突,然后提交 merge。
实际上,Merge 还有很多其他的策略。
- Squash Merge:将多个 Commit 合并为一个。
- Rebase:变基,将当前分支的历史平移到目标分支,可以使得历史更加线性
。 (会篡改历史,不推荐多人协作时使用) - GitHub Rebase,GitHub 提供的一种 Rebase 方式,将目标分支在当前分支上重放。
篡改历史 ¶
成为历史的罪人
Git 的历史一般情况下是不可篡改的,但实际上还是有一些方法可以对历史进行修改:
git commit --amend
:修改最新的提交 message。git reset <COMMIT>
:回退到某个快照。 (你可能更应该使用更加温和的git revert
)git rebase -i <COMMIT>
:交互式 Rebase,功能非常强大,不到万不得已不要使用!
在 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/b
、a/x/b
、a/x/y/b
等。/
开头只匹配根目录,否则匹配任意目录。!
开头是取消忽略。
官方文档:git-scm.com/docs/gitignore,GitHub 也提供了一个模板。
GitHub / ZJU Git 基础 ¶
设想多人协作的场景,每个人都在本地有自己的版本库,那如何实现同步呢?
答案是每个人都使用一个“权威”的远程版本库(Remote Repository
远程版本库 ¶
远程版本库也是一个普通的 Git 版本库,只不过由于不需要工作目录和暂存区,所以一般使用裸版本库。
git clone <SRC> <DEST>
:克隆一个远程版本库到本地,会自动添加一个名为origin
的远程版本库,可以通过git remote
管理。git push
:将本地的提交推送到远程版本库。git pull
:将远程版本库的提交拉取到本地,等价于git fetch
+git merge
。
远程版本库可以简单地理解成是本地的一个 origin/main
分支。
ZJU Git¶
作为一个全球顶尖的三本,怎么能没有一个自己的 Git 服务器呢?
网址:git.zju.edu.cn,只能通过校网或 WebVPN 访问。
注意区分
ZJU Git 和 GitHub 是平行的关系,它们提供类似的功能(非常类似
不要把 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_ed25519
和 id_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 push
和git pull
。-f
:强制推送,会覆盖远程版本库的内容(其他情况下不要使用) 。
动手做:提交更新
在本地修改 hello.c
,然后把它提交到 ZJU Git 上。
还记得要用哪些指令吗add
、commit
push
)
多人协作 ¶
- Fork:你可以复制一个仓库到你自己的账号下,这个操作叫做 Fork,你可以在这个仓库上自由地修改、提交。
- Pull Request(PR
) :当你修改完一个仓库后,你可以向原仓库提交一个 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:放弃所有权利,代表进入公共领域。
如果没有许可证呢?
原作者保留所有权利,不允许复制、分发、修改,使用的话需要联系原作者
侵权是非常严肃的事情!
原作者有权对你的项目进行 DMCA Takedown(DMCA
案例:
- 你的 Github 仓库被 DMCA Takedown 后怎么办? — Linux 中国
- 千万粉丝博主“何同学”抄袭他人项目代码?质疑者称非常恶劣,原作者称成果被窃取 — 观察者网
- GitHub 上的 DMCA 公示:github.com/github/dmca
- 我自己遇到的侵权行为
在根目录下创建名为 LICENSE
的文件,然后在其中附上许可证的内容。如果不同部分采用了不同的许可证,那么需要在每个文件的开头注明。
GitHub 能够自动识别常见的许可证类型,并在仓库的主页上显示。
除此之外,还有非软件类许可证,最常使用的是 CC(Creative Commons,知识共享)系列许可证,目前最广泛使用的版本是 4.0,它包含如下几种:
- CC 0:放弃所有权,进入公共领域。
- CC BY:BY 表示必须署名。
- CC BY-SA:SA 表示必须使用相同许可证(Share-Alike
) 。 - CC BY-NC:NC 表示禁止商业用途(Non-Commercial
) 。 - CC BY-NC-SA:三个要求都有。
- CC BY-ND:ND 表示禁止分发、修改(No-Derivatives,禁止演绎
) 。 - CC BY-NC-ND:三个要求都有。
往往通过一段文字即可表示许可:
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
思考
这篇文章也是通过 CC 许可证发布的,你能找到它的许可证吗?
结语 ¶
通过今天的学习,想必你已经对开源有了更深的了解,欢迎你加入开源的大家庭!
一些学习资源:
下一次课,李英琦学长将为大家带来 Markdown 和 LaTeX 等排版相关的内容(实际上这个讲义就是用 Markdown 写的