摸爬滚打 Git


生成SSH

用SSH(Secure Shell)干什么?

SSH以非对称加密实现身份验证。身份验证有多种途径,例如其中一种方法是使用自动生成的公钥-私钥对来简单地加密网络连接,随后使用密码认证进行登录;另一种方法是人工生成一对公钥和私钥,通过生成的密钥进行认证,这样就可以在不输入密码的情况下登录。任何人都可以自行生成密钥。公钥需要放在待访问的电脑之中,而对应的私钥需要由用户自行保管。认证过程基于生成出来的私钥,但整个认证过程中私钥本身不会传输到网络中。

ssh-keygen命令:生成一对SSH密钥

$ ssh-key -t ed25519 -C "your comment"
Generating public/private ed25519 key pair.

随后命令行会询问你存储密钥的位置:

Enter file in which to save the key (/home/tsum/.ssh/id_ed25519):

直接回车,密钥将存放在默认位置~/username/.ssh/文件夹下。

随后要求设置一个密码:Enter passphrase (empty for no passphrase):

连续敲两次回车,可以缺省不填。

常用Flags:

  • -t 是Type的缩写,用于指定密钥类型。常见的有rsa、dsa、ed25519(都是加密算法)
  • -C (comment)表示提供一个注释,用于区分这个密钥。
  • -b 是bits的缩写, 用于指定密钥的长度。对于RSA密钥,最小要求768位,默认是2048位。4096指的是RSA密钥长度为4096位。DSA密钥必须恰好是1024位(FIPS 186-2 标准的要求)。
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
用rsa算法生成一对4096位的密钥,注释是:your_email@example.com

最终生成的密钥:

img

带有.pub后缀名的是公钥(可以理解为门锁),没有后缀的是私钥(理解为钥匙)。

ssh-add命令:将私钥身份添加到 ssh-agent 身份验证代理

  1. 开启 ssh-agent。默认情况下操作系统是不开启ssh-agent的。
$ eval "$(ssh-agent -s)"
Agent pid 470
  1. 把私钥添加到ssh-agent中。
$ ssh-add ~/.ssh/id_ed25519
Identity added: /home/tsum/.ssh/id_ed25519 (2207428258@qq.com)

也可以直接使用

$ ssh-add
Identity added: /home/tsum/.ssh/id_rsa (2207428258@qq.com)
Identity added: /home/tsum/.ssh/id_ed25519 (2207428258@qq.com)

直接添加所有的密钥

常用Flags:

  1. -L :查看ssh-agent中所有身份的公钥 (可以cd去.ssh/文件夹对应着看看)
$ ssh-add -L
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDqSJWKpZQYJdCX2R+VtpH08wmhs/7/fwfe6cQ2Usq1j9ybeaS4Ic9RazAdZo50IM1PNf9cRSAPt3H1HZlCu71ti2eqdl+FKiHBjD/acswianDhA59voQK48vUML/3sXunfI5G3l5LhbVC7Z8h1tF/MqzuBansN4C3SZGNglt6u9zIFDRRilCQ5tWWs33y5PwEprbkyQsma63VRYXh5XELU/+n6iRTAVIQUG9e5LJmbvkmCO5EIsjsxIo5I20vvAtiRDQT+nzvs0hwVs2qqnWuuiEd081qA8vbuPv2gvC5thl2v3lgVQBJYjQqYCC5rkVcWg/nP0/E3yhwYEJuyejWR5jC5vgeygEnqm3OF5lxdC22xVhISOGk+DnqHV7S6IYNkadA5QoyLcQ5qxRIaGZ7iWmi04qT2es/Syifw2GY0jX2j8IPA+OuoKqdCccLzjjrPfcFukjR/DUuWbPwnFUeRrT7agO2G4UwYjjQbskmHbPiaiI+KbCbntMpX65cbw/c= 2207428258@qq.com
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP6Gvxv4/xKlG99/F5ISmLFrmu7BjkpX5O35tDkXSCbz 2207428258@qq.com
  1. -l:列出ssh-agent 中所有身份的指纹(私钥)
$ ssh-add -l
3072 SHA256:S3GsFBitbn3rcMX8ky/qK+bIuPZtY6N0ftns0NTaE1s 2207428258@qq.com (RSA)
256 SHA256:tJJmHXRVP+NQGpMx/7EyYCo8DgxTxkuhtt4xs/FEJKw 2207428258@qq.com (ED25519)

所以ssh-agent就像一把钥匙串。登陆其他主机的时候,会自动将所有钥匙(私钥)往钥匙孔(公钥)里插一遍。

  1. -d:从ssh-agent 中删除密钥
$ ssh-add -d ~/.ssh/id_rsa.pub
Identity removed: /home/tsum/.ssh/id_rsa.pub (2207428258@qq.com)
  1. -D: 从ssh-agent中删除所有密钥

注意重启Ubuntu后也需要重新开启ssh-agent,并用ssh-add添加密钥。

将公钥添加到github账户中

首先查看公钥内容。

img

当然也可以在.ssh文件夹下直接看

img

复制到github上

img

测试是否连接成功

ssh -T git@github.com
The authenticity of host 'github.com (20.205.243.166)' can't be established.
ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com,20.205.243.166' (ECDSA) to the list of known hosts.
Hi Tsumgo! You've successfully authenticated, but GitHub does not provide shell access.

第一次连接可能会提示警告:

The authenticity of host '``github.com`` (20.205.243.166)' can't be established.

原因是,.ssh/文件夹下缺少了一个known_hosts文件。

如果当前主机为 SSH 客户端, ~/.ssh 目录下会有一个 known_hosts 文件,该文件用来记录连接过的远程主机。

此时选择yes即可,同时生成缺少的known_hosts文件

基本概念

Git是一个分布式的版本控制系统。

版本控制系统(VCSs)

是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照(snapshot)将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。

什么是工作区(working directory)、缓存区(staging area)和提交(commits)

工作区:在资源管理器中可见的所有文件。

对于Git来说,这部分文件又分为3种:

  • Untracked: 一些新建的文件。使用git add命令可以让Git跟踪他们,变为Tracked。
  • Tracked:跟踪到的文件。已跟踪的文件即使改动后,依然处于Tracked状态。
  • ignored:忽略的文件。

缓存区:保存更改的中间过程

git add命令后,文件会进入缓冲区,防止误操作。

提交:记录新版本到历史记录中。

git commit命令后,新的版本会被记录。用git checkout命令可以随时回到过去的版本中。

新建一个仓库(repository)

什么是git 仓库?

A Git repository is a virtual storage of your project. It allows you to save versions of your code, which you can access when needed.

Git init:初始化文件夹,作为一个Git仓库(repo)

首先,cd 至一个项目文件夹(我这里是TextEditor),这里用于存储整个项目所需要的代码。

使用以下命令初始化:

$ git init
Initialized empty Git repository in D:/Code/TextEditor/.git/

.git 文件夹内包括所有git操作需要的文件。具体可参考:https://blog.csdn.net/mayfla/article/details/78653396

当然,为了利用好开源共享性,我们可以直接拷贝网络上已有的开源仓库。

Git clone: 从远端下载仓库

用法:

git clone <repo URL>

由于 Git 支持不同的网络传输协议,因此这里的 repo URL 可以有很多不同的写法。

  • http(s)://

HTTP协议。例如: https://github.com/alibaba/MNN.git

  • ssh://

SSH协议。一个SSH协议地址通常长这样:

git@HOSTNAME:USERNAME/REPONAME.git

HOSTNAME 主机名

USERNAME 用户名

REPONAME 仓库名称

一个例子是:

git@github.com``:alibaba/MNN.git
  • 本地路径(略)

例:

$ git clone https://github.com/alibaba/MNN.git

就可以在当前文件夹下得到MNN源码文件。

Saving Changes

当我写好了代码,如何让git记录我做出的更改?

在git的版本控制流程中,保存更改有两个步骤

下面以一个TextEditor项目为例。MainTest.c是主程序,include/libgraphics/分别存储头文件及相应的.c文件

Git status:查看当前仓库的状态

单从它的功能就可以猜到,这将是我们使用git过程中使用最频繁的命令之一。

img

git告诉我们,在master分支上,还没有任何提交记录。文件夹下有好多新文件没有被追踪到。

同时提醒我们,可以用git add <file> 命令,告诉git我将要提交哪些文件。

因此下一步:

Git add: 添加被更改的文件到缓冲区(staging area)

用法:

$ git add <file> -- 添加单个文件
$ git add <directory> -- 添加整个文件夹下的新文件、有改动的文件

如果我希望让仓库记录我所做的更改,首先使用:

$ git add MainTest.c include libgraphics

命令添加MainTest.c include\ libgraphics\ 到git的缓冲区中。

img

此处warning暂时不用关注。详情:https://blog.csdn.net/Babylonxun/article/details/126598477

注意,此时上述改动并没有被记录到仓库的历史记录中。这些文件被存放在了缓冲区。

我们可以再查看一次当前仓库的状态。

img

命令行提示我们,需要提交(commit)之后,这些改动才能生效。

附:可以用git add -all将所有的改动添加至缓冲区。

Git commit:记录并跟踪当前的改动。

$ git commit -m "add .c .h files to the repo"
  • -m 代表 message,用于描述此次提交的改动内容。

如何写一个好看的提交信息

img

此时,我们成功完成了一次提交。这次改动会被记录到仓库的历史记录中,方便我们随时查看。

commit操作还有更多可选flags:

  • 无参数:执行后会弹出文本编辑器,需要我们写本次的提交信息。
  • -a:可以省去一步git add操作,直接将当前文件夹状态存储为一个快照。

但是请注意,新增文件不会在此次commit范围内。它们仍然需要git add之后再提交。

​ option -a/-all: Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.

  • -am:综合-a-m的两个特性。

例:

$ git commit -am "add a new file"
  • --amend:追加更新上次commit。
$ git add hello.py
$ git commit --amend

此时会弹出文本编辑器,发现内容是上次编辑的commit message。因此我们的更改是基于上次快照的修改,并没有创建一个新的快照。

img

通过以上内容,不难发现:git addgit commit通常是一起使用的。

例如:

$ git add .
$ git commit

为当前文件夹的所有文件创建快照。

$ git add hello.py
$ git commit

为某个新文件/修改后的文件创建快照。

在实际合作过程中,我们通常将每一个改动的文件分别提交。这样虽然增加了操作繁琐度,但是有利于日后在庞大的更新中定位Bug。

Git Diff:查看差异

我们在MainText.c文件中做一些改动。

img

在命令行中输入git diff

img

从命令行给出的反馈可以看出,git diff 比较了工作区(working directory**上一次提交(commit)**的差别。

git add命令将它存储到缓冲区(staging area)。

$ git add MainTest.c

随后在工作区添加注释:

img

在命令行中输入git diff

img

不难发现,这里git diff比较了工作区(working directory**缓冲区(staging area)**的差别。

我们首先解读上述内容。

  • diff --git a/MainTest.c b/MainTest.c

可以看出传给diff命令的两个文件。分别是 a/MainTest.cb/MainTest.c 分别是缓冲区(staging area)的文件、工作区(working directory)下的MainTest.c

  • index b68cee0..f1f0870 100644

这里是一个哈希值,用于代表git中的一个对象。用处不大。

  • 图例

img

这部分表示:在下面的语句块中, '-'表示a/MainTest.c的改动,'+'表示 b/MainTest.c的改动。

  • 比较语句块

img

绿色部分以'+'开头,表示b/MainTest.c 中改动的内容

红色部分以'-'开头,表示a/MainTest.c 中改动的内容。

为什么两次git diff比较的内容不同?

当工作区有改动,缓冲区为空,**diff的对比是“工作区最后一次commit提交的仓库**的共同文件”;

当工作区有改动,缓冲区不为空,**diff对比的是“工作区缓冲区**的共同文件”。

其他用法

  • --stat:显示简略信息。

img

在上述情况下,添加--stat参数,会告诉我们有1个文件被修改,文件中有两处插入,一处删除。

  • --cached or --staged:显示缓冲区与最后一次commit(HEAD)之间的增删改。

在上述条件下继续实验,首先进行一次add操作。

img

第一次git diff:由于缓冲区不为空,比较工作区与缓冲区的文件差异。不输出。

第二次git diff --cached:比较缓冲区与上一次commit(HEAD)的文件差异。有输出。

更多用法详见博客:https://blog.csdn.net/wq6ylg08/article/details/88798254

.gitignore文件

实际的项目中,我们不希望某些文件被跟踪到(例如.o文件,占空间)。

我们只需要写一个.gitignore文件,就能实现这个需求。

我们需要用到类似正则表达式的通配符。

看下面例子很好懂。

Pattern Example matches Explanation*
**/logs logs/debug.log logs/monday/foo.bar build/logs/debug.log You can prepend a pattern with a double asterisk to match directories anywhere in the repository.
**/logs/debug.log logs/debug.log build/logs/debug.log but not logs/build/debug.log You can also use a double asterisk to match files based on their name and the name of their parent directory.
*.log debug.log foo.log .log logs/debug.log An asterisk is a wildcard that matches zero or more characters.
*.log !important.log debug.log trace.log but not important.log logs/important.log Prepending an exclamation mark to a pattern negates it. If a file matches a pattern, but also matches a negating pattern defined later in the file, it will not be ignored.
.log !important/.log trace.* debug.log important/trace.log but not important/debug.log Patterns defined after a negating pattern will re-ignore any previously negated files.
/debug.log debug.log but not logs/debug.log Prepending a slash matches files only in the repository root.
debug.log debug.log logs/debug.log By default, patterns match files in any directory
debug?.log debug0.log debugg.log but not debug10.log A question mark matches exactly one character.
debug[0-9].log debug0.log debug1.log but not debug10.log Square brackets can also be used to match a single character from a specified range.
debug[01].log debug0.log debug1.log but not debug2.log debug01.log Square brackets match a single character form the specified set.
debug[!01].log debug2.log but not debug0.log debug1.log debug01.log An exclamation mark can be used to match any character except one from the specified set.
debug[a-z].log debuga.log debugb.log but not debug1.log Ranges can be numeric or alphabetic.
logs logs logs/debug.log logs/latest/foo.bar build/logs build/logs/debug.log If you don’t append a slash, the pattern will match both files and the contents of directories with that name. In the example matches on the left, both directories and files named logs are ignored
logs/ logs/debug.log logs/latest/foo.bar build/logs/foo.bar build/logs/latest/debug.log Appending a slash indicates the pattern is a directory. The entire contents of any directory in the repository matching that name – including all of its files and subdirectories – will be ignored
logs/ !logs/important.log logs/debug.log logs/important.log Wait a minute! Shouldn’t logs/important.log be negated in the example on the left Nope! Due to a performance-related quirk in Git, you can not negate a file that is ignored due to a pattern matching a directory
logs/**/debug.log logs/debug.log logs/monday/debug.log logs/monday/pm/debug.log A double asterisk matches zero or more directories.
logs/*day/debug.log logs/monday/debug.log logs/tuesday/debug.log but not logs/latest/debug.log Wildcards can be used in directory names as well.
logs/debug.log logs/debug.log but not debug.log build/logs/debug.log Patterns specifying a file in a particular directory are relative to the repository root. (You can prepend a slash if you like, but it doesn’t do anything special.)

例如,我希望忽略文件夹中由Visual Studio自动生成的文件。

img

即此处.vs/ x64/ .sln .vcxproj等等文件。

在.gitignore文件中保存如下内容:

img

可以发现资源管理器中相应文件变成灰色。

再用git status 查看当前工作区状态。

img

原来一长串的Untracked files不见了。 最后我们只需要将.gitignore 提交即可。

Undoing Changes

git revert

如果我们想要回退去修改版本二(有bug),但又不想删除版本三,可以使用revert命令。

img

Git revert会新建一个版本四,其中包含了修改后的版本二与版本三。

git reset

git reset的作用是:修改HEAD的位置,即将HEAD指向的位置改变为之前存在的某个版本,如下图所示,假设我们要回退到版本一:

img

适用场景: 如果想恢复到之前某个提交的版本,且那个版本之后提交的版本我们都不要了,就可以用这种方法。

Inspecting a repository

Git log:显示历史日志

img

在上面的项目中我们进行了两次commit。此处列出了commit message、修改人及其电子邮箱。

commit 31de595cf3461e6bf01ca984fd8881ce849c627a (HEAD -> master)

黄色部分为一串Hash值,git以此来区分不同的commits。

蓝色的HEAD是一个游标,表示当前工作区内展现的是master分支的内容。

这样子的日志显得很杂乱。我们有办法让日志更好看一点。

$ git log --all --graph --oneline --decorate

该命令使用有向无环图展现一个仓库的更新日志。与上面的命令比起来更加赏心悦目。

其他选项:

$ git log -n <limit>  只显示最新的 <limit> 次提交
$ git log --author="<pattern>"   查看特定作者的提交,使用正则表达式筛选
$ git log --grep="<pattern>"   用正则表达式筛选commit message
$ git log <file>  筛选包含特定文件的提交

Syncing

Git remote:与远程仓库联系

  • git remote :列出远端。

git会为通过git clone 命令下载到本地的仓库建立与源仓库的联系。

MNN是从网上克隆到本地的仓库。

PS D:\Code\MNN> git remote -v
origin  https://github.com/Tsumgo/MNN.git (fetch)
origin  https://github.com/Tsumgo/MNN.git (push)

-v表示verbose。git把源仓库的地址 https://github.com/Tsumgo/MNN.git 称为origin

git remote add <name> <url>:添加一个远端。

我们在github上新建一个项目,并复制该项目的SSH地址。(不用HTTP协议,是因为SSH能免密码)

PS D:\Code\TextEditor> git remote add origin git@github.com:Tsumgo/TextEditor.git
PS D:\Code\TextEditor> git remote
origin

那么当前仓库就与一个叫origin的仓库建立了联系。

接下来我们将本地项目上传至远端仓库。

Git push:将对象传送至远端仓库

git push <remote> <local branch>:<remote branch>

表示远端仓库

表示本地更新好的分支。

希望更新的表示远端分支。

$ git push origin master:master

img

在github上,我们就能看到本地文件在远端主机上更新了。

需要注意的是,每次在分支上**push**本地文件时应当首先拉去远程仓库的最新版本。

Git branch & git checkout

一个项目可以有多个分支同时开发。下面使用git branchgit checkout 命令模拟对不同分支的管理。

img

git branch

$ git branch <branch>     # 创建名为<branch>的分支
$ git branch -d <branch>  # 删除<branch>分支。如果当前分支有未合并的更改则不会删除。
$ git branch -D <branch>  # 强制删除<branch>分支。
$ git branch -m <branch>  # 重命名当前分支

Git checkout: 切换分支

$ git checkout <commit> # 切换到其他分支

其中可以使用master等已命名的名字替换,或者用commit哈希值的前7位。

建立两个分支

$ git branch cat  # 创建cat 分支
$ git branch dog  # 创建dog 分支
# 在文件中做一些修改。增加cat函数
$ git checkout cat 切换到cat分支
$ git add MainTest.c
$ git commit -m "add function cat"
$ git log --all --graph --oneline --decorate

我们可以看到此时仓库的每一个历史提交。

img

游标HEAD指向cat分支。同时我们在工作区查看MainTest.c,发现存储的确实是cat分支的内容。

img

而master、dog分支停留在上一个commit中,还未更改。使用

$ git checkout dog

切换到dog 分支。

img

并在工作区中查看MainTest.c,已经回到了上一次提交的状态。

img

Git pull <remote>:拉取远端仓库特定分支的最新版本。

Git pull (git fetch + git pull) 与远端仓库同步

img

我希望下载远端仓库的某些分支到本地进行开发,需要经历以上过程。

Git fetch 命令下载远端仓库的指定文件,但它们并没有进入本地仓库,只是在本地有个副本

Git merge 命令可以将远程仓库的副本加入到本地仓库中,其间需要程序员主动考虑合并细节。

Git pull相当于上述两个步骤的合并。

$ git pull <remote> <remote branch>:<local branch> # 将远程主机的分支拉过来 与<local branch>合并。
$ git pull <remote> <remote branch> # 如果与当前分支合并,可以省略本地分支。

Work Flows

Feature Branch Workflow

  1. 首先,从主分支(master)创建一个新分支,命名为“feature/xyz”,其中“xyz”代表正在开发的特定功能。
  2. 接下来,在该分支上进行所有与特性相关的更改和提交。
  3. 当特性开发完成时,将其合并回主分支(master)。在合并之前,应该尽可能确保代码的稳定性,并通过测试确保它的质量。
  4. 如果需要对特性进行更改或修复,可以在feature分支上继续进行更改和提交,然后将其再次合并到主分支。

    这种工作流程的好处在于,它允许开发人员独立地处理不同的功能开发,而不会影响其他特性或主分支的稳定性。它也使得跟踪特性开发进度变得更加容易,并且有助于团队成员更好地协作。

Forking Workflow

  1. 开发人员首先从公共代码库中fork一个副本到自己的GitHub账户下。
  2. 然后,在其个人副本上创建一个新的分支,以便在其中进行所需的更改和提交。
  3. 当对代码进行了更改并已准备好合并到主代码库时,开发人员将新分支推送到他们的个人副本,并创建一个“pull request”(PR)以请求将更改合并回主代码库。
  4. 在进行代码审查、测试和合并之前,其他团队成员或贡献者可以在PR中提出评论或建议修改。
  5. 一旦所有必要的更改都已审核并通过测试,开发人员就可以将更改合并回主代码库。

这种工作流程的优点在于,它允许开发人员在不影响公共代码库的情况下进行独立的开发工作,并且保持原始代码库的稳定性。它也使得跨多个开发团队协作变得更加容易,因为每个人都可以在自己的个人副本上工作,并通过pull request将更改合并回主代码库。


Author: Tsum
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Tsum !
  TOC