Studying Father's luogu blog

Studying Father's luogu blog

从零至灵,由壹达意

[洛谷日报#199]Git 简单上手指南

posted on 2019-05-10 20:51:22 | under 未分类 |

Part 1 关于分布式版本控制

Git 是目前使用最广泛的分布式版本控制系统,在很多大型项目的背后都能见到 Git 的身影。

那么,什么是分布式版本控制系统?Git 又有什么用呢?

版本控制,顾名思义,就是对一个或一些文件的变化进行记录,并在以后查阅具体修订情况的系统。

最简单的版本控制就是存储同一个文件的不同版本,像这样:

document-2019-2-15.md
document-2019-2-16-1.md
document-2019-2-16-2.md
...

这样做的弊端是很明显的,进行修改的时候容易错误修改之前的版本,而且在查询具体的某一次更改的时候也非常麻烦。

而成熟的版本控制系统,则拥有了更完善的版本控制功能:你可以方便地查阅对代码库的某一次修改,比对任意两个版本之间的差别,并在出现 bug 的时候,及时找到 bug 出现的原因,或是回退到之前的版本。在版本控制系统的帮助下,这些操作都将变得非常容易。

那么,相比于集中式版本控制,分布式版本控制有什么优势呢?

集中式版本控制,顾名思义,就是在一个集中式的服务器上进行版本控制,这个服务器保存了所有版本信息,用户只需连接到服务器,就可以读取文件,推送更新。

对于管理员而言,集中式版本控制方便了项目的维护,他们只需控制好服务器就可以了;而对于开发者,他们可以从服务器里获得其他人的工作信息,大大提升了开发效率。

然而,正是因为它集中性,它也就变得脆弱了。一旦服务器宕机,因为所有开发者都没有版本信息,也就无法再继续协作,更糟糕的是,一旦数据丢失,诸如版本信息之类的,就再也找不到了——开发者所拥有的,只是项目的一个快照而已。

于是,分布式版本控制系统就应运而生了。

分布式版本控制系统之下,不仅仅服务器存储了版本信息,每个客户端也都存储了版本信息。

在这种情况下,即使服务器宕机,开发者也可以从其他开发者那里获得版本信息,继续协作。

你会发现我们甚至并不需要专门的服务器来维护版本信息,是的,每个客户端都可以视为一个服务器。

正是因为分布式版本控制的灵活性,我们可以实现很多在集中式管理系统里实现不了的功能,例如同时与多个小组的人协作,进行多分支开发。

我们接下来介绍的 Git ,正是一个非常易用的分布式版本管理系统。

Part 2 Git 的安装

对于 Debian/Ubuntu 用户,只需在命令行执行 sudo apt install git 即可安装 Git。

其他 Linux 发行版用户可以在 这里 查看安装方式。

对于 Windows 和 Mac 用户,请前往 Git 官网 下载安装包。

Git 在 Windows 下的安装过程比较繁琐,这里做些必要的说明:

  1. 在选择安装路径后,会提示您选择偏好的编辑器(用于编辑 commit 信息等),这时您可以根据喜好,选择您愿意使用的编辑器。(笔者采用的是 Notepad++)

  1. 剩下的选项,如果您对具体含义不太熟悉的话,建议按照默认选项安装。

在安装结束后,您就获得了 Git Bash, Git cmd, Git GUI 三个应用。

Git cmd 属于已经弃用的应用,这里不再叙述。

Git GUI 是 Git 的图形化版本,支持很多较为简单的操作,适合初学者使用。但它对很多高级操作并不支持。

Git bash 是 Git 的命令行模式,你可以执行所有 Git 的操作。当然,所有命令行操作你也可以在 Windows 下的 cmd 或是 Powershell 下进行,不过使用 Git bash 更为方便。

接下来的内容将基于 Git 的命令行模式(也就是 Windows 下的 Git bash )展开。

Part 3 配置信息

在安装 Git 后,你需要先设置用户名和邮箱,这些信息将作为提交者的身份标识。

$ git config --global user.name StudyingFather # 设置用户名
$ git config --global user.email qaq@qaq.com # 设置邮箱

命令中的 --global 代表了配置对系统上的所有仓库均有效。假如只是对单个仓库的配置,只需要在你想要配置的仓库下执行去掉 --global 的命令即可。

注意:如果用户名中间有空格,记得用引号将用户名包起来。

接下来是设置文本编辑器:

$ git config --global core.editor vim

当然,如果你愿意,也可以采用 Emacs 等编辑器。

Part 4 仓库操作基础

Part 4.1 新建 Git 仓库

新建一个 Git 仓库非常简单,只需在你想要建立仓库的文件夹输入如下命令:

$ git init

Git 将在当前文件夹新建一个 .git 文件夹,一个仓库就这样建好了。

如果别人已经建立了一个仓库,你想要把这个仓库拷贝到自己的电脑上,采用 git clone 命令即可。像这样:

$ git clone https://github.com/SFOI-Team/luogu-problem-list

这样在当前文件夹下,就会新建一个名为 luogu-problem-list 的仓库,里面存放着原来仓库的所有信息。

Part 4.2 跟踪文件

在我们对仓库做出了一些修改后,我们需要将这些修改纳入版本管理当中去。

使用 git status 命令可以查看当前仓库文件的状态。

假设我们在一个空仓库中新增了一个 README.md 文件,执行 git status 命令的效果如下:

$ mkdir test
$ cd test
$ git init # 在 test 文件夹下建立一个新仓库
$ vi README.md # 写点东西
$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        README.md

nothing added to commit but untracked files present (use "git add" to track)

这里的 Untracked files 指的是 Git 之前没有纳入版本跟踪的文件。

如果文件没有纳入版本跟踪,我们对该文件的修改不会被 Git 记录。

让我们把这个新文件纳入版本跟踪。

$ git add README.md
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   README.md

这时 README.md 已经纳入了版本跟踪,放入了暂存区。

我们接下来只需执行 git commit 命令就可以提交这次更改(也就是让 Git 把我们这一次更改的内容记录下来)。

但在我们干这个之前,先让我们再对 README.md 做点小修改。

$ vi README.md # 再随便修改点东西
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   README.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   README.md

你会发现 README.md 同时处于暂存区和非暂存区。这时如果我们提交更改,会发生什么呢?

我们可以试一试:

$ git commit # 接下来会弹出编辑器页面,你需要写下 commit 信息
[master (root-commit) b13c84e] test
 1 file changed, 2 insertions(+)
 create mode 100644 README.md
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

我们会发现:处于暂存区的修改,在运行 git commit 命令后会被提交,而非暂存区的修改,则不会被提交。

正如 Git 提示的那样,假如想要提交非暂存区的更改的话,只需输入 git add MEADME.md 就可以将这次更改放入暂存区,然后执行 git commit 就可以提交更改。

Part 4.3 提交更新

上文提到的 git commit 命令可以将暂存区内的更改提交。

让我们回顾一下刚才进行的那次 commit 操作。

[master (root-commit) b13c84e] test
 1 file changed, 2 insertions(+)
 create mode 100644 README.md

master 表示当前位于 master 分支(关于分支的问题,下文将会详细介绍),b13c84e 表示本次提交的 SHA-1 校验和的前几位。后面则是本次提交的信息。

接下来两行则详细说明了本次更新涉及的文件修改。

另外,在 commit 的时候采用 -m 选项可以在输入命令的时候直接输入本次提交的信息,从而避免了再打开编辑器的麻烦。

$ git add README.md # 先把之前还没有纳入暂存区的更改放入暂存区
$ git commit -m "update README.md" # 引号里的就是本次commit的信息
[master cf78ab3] update README.md
 1 file changed, 1 insertion(+)

现在我们再执行一下 git status 命令,查看一下仓库状态。

$ git status
On branch master
nothing to commit, working tree clean

这时候 Git 提醒我们,我们没有任何未提交的更改了。

Part 4.4 查看提交记录

使用 git log 命令可以查看我们的提交历史记录。

可以看到,提交历史里记录了每次提交时的 SHA-1 校验和,提交的作出者,提交时间和 commit 信息。

$ git log
commit cf78ab3c198337acf508d90924328d476c2e5783 (HEAD -> master)
Author: StudyingFather <qaq@qaq.com>
Date:   Fri May 10 22:39:27 2019 +0800

    update README.md

commit b13c84ee799c0fc4c3420814d95c03ea1823ebe3
Author: StudyingFather <qaq@qaq.com>
Date:   Fri May 10 22:25:56 2019 +0800

    test

当然,git log 命令有很多非常不错的用法,可以让我们看到更多详细的信息,因为篇幅原因这里不再赘述,想要了解的可以参考 这里

Part 4.5 分支管理

试想我们需要开发一个新功能,直接在主线上进行开发是非常危险的举动。这时我们就需要新开一个分支进行修改了。

让我们在原来的 test 仓库下开一个新的开发分支:

$ git branch dev # 创建一个叫做 dev 的新分支
$ git checkout dev # 切换当前分支到 dev
Switched to branch 'dev'
$ git branch # 查看所有分支信息
  master
* dev

dev 前面的星号代表我们当前的分支为 dev,我们接下来的修改都将记录在这个分支上。

我们可以在这个开发分支上做点小修改:

$ vi test.cpp
$ git add test.cpp
$ git commit -m "QAQ"
[dev 4fe4923] QAQ
 1 file changed, 8 insertions(+)
 create mode 100644 test.cpp

在新分支上修改似乎和在 master 分支上修改没什么太大区别,事实上也确实如此,每个分支都是平等的。

让我们回到 master 分支,再新建一个文件:

$ git checkout master
$ vi qwq.cpp
$ git add qwq.cpp
$ git commit -m "QwQ"
[master f793578] QwQ
 1 file changed, 6 insertions(+)
 create mode 100644 qwq.cpp

你也许会奇怪,我们作出的这两次更改究竟有什么区别呢?让我们结合下面这幅图来理解:

我们事实上是在两个不同的分支进行开发,分支之间互不干扰。

当另外一个分支已经完成工作时,我们可以通过git merge命令合并两个分支。

$ git merge dev
Merge made by the 'recursive' strategy.
 test.cpp | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 test.cpp

可以看到,我们在 dev 分支上的更改被完美地合并到了 master 分支上。

合并后,两个分支指向了同一个提交。

当然,合并工作也并不总是这么顺利,例如我们在两个分支上同时修改一个文件的时候。

$ git checkout dev # 切换到dev分支
Switched to branch 'dev'
$ vi test.cpp # 在dev分支上修改test.cpp文件
$ git add test.cpp
$ git commit -m "add notes"
[dev 45cbf3d] add notes
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git checkout master
$ vi test.cpp # 在master分支上修改test.cpp文件
$ git add test.cpp
$ git commit -m "some small changes"
[master 7beb56a] some small changes
 1 file changed, 2 insertions(+), 2 deletions(-)
$ git merge dev # 尝试合并master分支和dev分支
Auto-merging test.cpp
CONFLICT (content): Merge conflict in test.cpp
Automatic merge failed; fix conflicts and then commit the result.

这时 Git 发出警告,提示出现了冲突。

当两个分支对同一文件进行不同的修改的时候,自动合并就无法正常进行。

这时 git status 会告诉我们冲突文件的情况:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   test.cpp

接下来我们的工作就是解决冲突。

我们打开出现冲突的文件 test.cpp ,会发现一些奇怪的现象:

#include <cstdio>
int main()
{
 int a,b;
<<<<<<< HEAD
 scanf("%d%d",&a,&b);//input two nums
 printf("%d\n",a+b);//output the sum
 return 0;
=======
 scanf("%d%d",&a,&b);
 printf("%d\n",a+b);
 return 0;//end the program
>>>>>>> dev
}

=======上面的部分代表了 master 分支该文件的状态,下面的部分代表了 dev 分支该文件的状态,我们需要手动修改这个文件来解决冲突,像这样:

#include <cstdio>
int main()
{
 int a,b;
 scanf("%d%d",&a,&b);//input two nums
 printf("%d\n",a+b);//output the sum
 return 0;//end the program
}

保存后退出,将刚刚的修改纳入暂存区,并完成合并:

$ git add test.cpp
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

        modified:   test.cpp
$ git commit
[master 14fcf92] Merge branch 'dev'

好了,我们就这样解决了合并冲突的问题。

当一个分支的工作已经合并到其他分支上,不再需要该分支的时候,使用git branch -d就可以安全删除该分支了。

$ git branch -d dev # 删除已经没用的 dev 分支

Part 4.6 远程仓库

我们可以将我们对仓库的修改提交到远程仓库(例如托管在 Github 上的仓库)。

先让我们添加一个远程仓库:

$ git remote add origin https://github.com/StudyingFather/my-code
$ git remote # 显示当前所有远程仓库
origin

移除远程仓库也很容易:

$ git remote rm origin

当我们在远程仓库作出一些更改时,需要将这些更改拉取到本地时,请使用 git fetch 命令。

$ git fetch origin # 将 origin 上的修改拉取到本地

需要注意,执行完 git fetch 命令后,只会抓取数据而不会合并,合并工作需要自己完成。

要想在抓取的同时将远程仓库的进度和本地合并,可以使用 git pull 命令。

$ git pull origin master # 抓取 origin 的数据并自动和本地的 master 分支合并

当我们完成了修改,使用git push命令可以将修改推送至远程仓库。

$ git push origin master # 将 master 分支的数据推送至 origin

在推送更改时,需要注意下面几点:

  1. 如果是第一次向远程仓库推送修改,Git 会要求输入远程仓库的账号和密码。
  2. 和分支合并类似,当远程分支有新更改而当前分支没有时,推送操作会失败。这时请执行 git pull 命令完成合并再提交。

Part 5 Github 入门使用指南

Github 是全世界规模最大的 Git 仓库托管平台,因此学会使用 Github 非常重要。

Part 5.1 认识 Github 中的仓库

限于篇幅,这里只讲述一些 Github 上远程仓库的基本操作,更高级的操作可以在 Github 里的帮助页看到。

点击右上角的加号,选择 new repository 就可以新建一个仓库。

Github 上的仓库分为两种:公开仓库和私有仓库。私有仓库对外不可见,您可以根据需要自己设置(后期可以更改)。

如果本地已经建好仓库,需要导入到 Github ,下面的初始化步骤(包括创建 README.md 文件等)请跳过。

一个公开仓库大概是长这样的:

右上角三个按钮的含义如下:

  • watch:设置关于该仓库的更新通知。
  • star:给这个仓库加星。
  • fork:clone 一份该仓库的副本,你可以对该副本进行编辑。

如果您需要获取该仓库的源代码,点击 clone or download 按钮,可以获取该仓库的链接,您可以按照上文介绍的方法 clone ,当然也可以选择 Download zip 下载源代码压缩包。

在 Github 上可以直接对文件进行编辑,但过程相较于本地编辑更加繁琐,因此还是推荐大家在本地编辑后将更改提交至远程仓库。

Part 5.2 协作开发

如果你不拥有对一个仓库的所有权,则不能对该仓库进行直接编辑,在这种情况下,你可以通过如下几种方式参与协作开发。

  1. Issue:在使用中遇到的 bug 和想要提出的建议都可以在这里提出。
  2. Pull Request:你可以对 fork 后得到的仓库的副本进行更改,并通过 PR 的方式将该更改合并到主仓库上。

前者因为细节不多,这里不再赘述。

我们重点介绍一下 Pull Request(PR) 的操作方法。

首先,请点击 fork 按钮,在你的账户下创建一份当前仓库的副本。

接下来你可以对这个副本进行一些修改。(如果在本地修改,别忘了推送到远程仓库)

在修改完后,进入到你自己的仓库页面,会发现有一个 Pull request 按钮。点击它就可以创建一个合并请求了。

像这样:

为了方便他人了解你的 PR 的内容,请给你的 PR 起一个有意义的标题,并在内容框里填写你这一次 PR 的主要更改。

接下来项目的维护者会根据情况决定是否接受你的合并请求。在此期间,你可以利用评论区与开发者(以及其他协作者)进行交流。

如果没有合并冲突,而且你的更改确实有帮助的话,你的更改就有可能被合并到主仓库上,这样你就完成了一次协作开发。

如果在创建 PR 后,你又想作出一些更改,只需要在原来的分支上继续推送更改即可,这些更改会自动追加进合并请求当中。

Part 6 总结

Git 作为分布式代码管理的强力工具,能帮我们解决项目开发与版本管理的诸多问题,配合 Github 这一全球最大的代码仓库托管网站,更为开源软件社区的发展提供了无限可能。

限于篇幅,Git 的很多高级操作本文都没有介绍到,感兴趣的读者可以自行查找资料。

Reference