77. Git 使用
连接Github
- 在
terminal
输入ssh-keygen
- 进入
~/.ssh
id_rsa
是 私钥id_rsa.pub
是 公钥- 将公钥复制到
Github
下- 点击头像,选择
Setting
- 选择
SSH and GPG keys
- 选择
New SSH key
Title
可以自己写;Key
中复制你的公钥- 选择
Add SSH key
- 下面就可以访问了
- 点击头像,选择
git简单使用
将git下载到本地
git clone <你的仓库地址>
cd 进入 对应的文件夹
git pull
更新你的仓库git status
以查看在你上次提交之后是否有修改- 该命令加了
-s
参数,以获得简短的结果输出。如果没加该参数会详细输出内容。
- 该命令加了
git add
将该文件添加到缓存git add *
将所有更改的文件,添加进暂存git add <文件名>
将对应的文件添加进暂存
git commit -m "备注"
使用 git add 命令将想要快照的内容写入缓存区, 而执行 git commit 将缓存区内容添加到仓库中。git push
将本地库中的最新信息发送给远程库。
Notes
若已经有文件:
- 创建完git连接之后,先
git pull
一下。
commit
和push
的区别
git作为支持分布式版本管理的工具,它管理的库(repository)分为本地库、远程库。
git commit操作的是本地库,git push操作的是远程库。
git commit是将本地修改过的文件提交到本地库中。
git push是将本地库中的最新信息发送给远程库。
那有人就会问,为什么要分本地commit和服务器的push呢?
因为如果本地不commit的话,修改的纪录可能会丢失。而有些修改当前是不需要同步至服务器的,所以什么时候同步过去由用户自己选择。什么时候需要同步再push到服务器
设置vimdiff
1 | git config --global diff.tool nvimdiff |
gitdiff 在打开每一个文件的 diff 时都会进行输入确认, 设置
difftool.prompt
可以禁用这一行为.
给 difftool 起个别名 vd
此后使用 git difftool 时 Git 便会调用 vimdiff 逐一打开每个文件的 Diff. 查看完成一个文件的 diff 后使用 :qa
关闭该文件 diff.
这时 Git 会自动打开下一个文件的 diff. 如果中止本次 Diff 呢?首先需要让 Git 信任 difftool 的返回码:
1 | git config --global difftool.trustExitCode true |
然后让 vimdiff 返回 1::cq
(:help cquit
) 退出 Vim.
创建版本库
init
git init
命令把这个目录变成Git可以管理的仓库
也不一定必须在空目录下创建Git仓库,选择一个已经有东西的目录也是可以的。
status
运行git status
命令查看仓库当前状态
git status
命令可以让我们时刻掌握仓库当前的状态
add
命令git add
告诉Git,把文件添加到仓库
commit
命令git commit
告诉Git,把文件提交到仓库
-m
后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
diff
git diff
顾名思义就是查看difference,显示的格式正是Unix通用的diff格式,看具体修改了什么内容
时光穿梭机
版本回退
你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit
。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit
恢复,然后继续工作,而不是把几个月的工作成果全部丢失。
查看历史记录
在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么。版本控制系统肯定有某个命令可以告诉我们历史记录,在Git中,我们用git log
命令查看
1 | $ git log |
git log
命令显示从最近到最远的提交日志,我们可以看到3次提交,最近的一次是append GPL
,上一次是add distributed
,最早的一次是wrote a readme file
。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline
参数:
1 | $ git log --pretty=oneline |
需要友情提示的是,你看到的一大串类似1094adb...
的是commit id
(版本号),和SVN不一样,Git的commit id
不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且你看到的commit id
和我的肯定不一样,以你自己的为准。为什么commit id
需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。
每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线
版本回退
准备把readme.txt
回退到上一个版本,也就是add distributed
的那个版本
首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD
表示当前版本,也就是最新的提交1094adb...
(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^
,上上一个版本就是HEAD^^
,当然往上100个版本写100个^
比较容易数不过来,所以写成HEAD~100
。
现在,我们要把当前版本append GPL
回退到上一个版本add distributed
,就可以使用git reset
命令:
1 | $ git reset --hard HEAD^ |
--hard
参数有啥意义?这个后面再讲,现在你先放心使用。
还可以继续回退到上一个版本wrote a readme file
,不过且慢,让我们用git log
再看看现在版本库的状态:
1 | $ git log |
最新的那个版本append GPL
已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个append GPL
的commit id
是1094adb...
,于是就可以指定回到未来的某个版本:
1 | $ git reset --hard 1094a |
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
再小心翼翼地看看readme.txt
的内容:
1 | $ cat readme.txt |
果然,我胡汉三又回来了。
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD
指针,当你回退版本的时候,Git仅仅是把HEAD从指向append GPL
:
1 | ┌────┐ |
改为指向add distributed
:
1 | ┌────┐ |
然后顺便把工作区的文件更新了。所以你让HEAD
指向哪个版本号,你就把当前版本定位在哪。
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id
怎么办?
在Git中,总是有后悔药可以吃的。当你用$ git reset --hard HEAD^
回退到add distributed
版本时,再想恢复到append GPL
,就必须找到append GPL
的commit id。Git提供了一个命令git reflog
用来记录你的每一次命令:
1 | $ git reflog |
终于舒了口气,从输出可知,append GPL
的commit id是1094adb
,现在,你又可以乘坐时光机回到未来了。
总结
HEAD
指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id
。- 穿梭前,用
git log
可以查看提交历史,以便确定要回退到哪个版本。 - 要重返未来,用
git reflog
查看命令历史,以便确定要回到未来的哪个版本。
工作区与暂存区
工作区(Working Directory)
就是你在电脑里能看到的目录,比如我的learngit
文件夹就是一个工作区:
版本库(Repository)
工作区有一个隐藏目录.git
,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master
,以及指向master
的一个指针叫HEAD
。

分支和HEAD
的概念我们以后再讲。
前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add
把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个master
分支,所以,现在,git commit
就是往master
分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。
实例
现在,使用两次命令git add
,把readme.txt
和LICENSE
都添加后,用git status
再查看一下:
1 | $ git status |
所以, git add
命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit
就可以一次性把暂存区的所有修改提交到分支。

一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:
1 | $ git status |
现在版本库变成了这样,暂存区就没有任何内容了:

管理修改
什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。
现有操作过程:
第一次修改 -> git add
-> 第二次修改 -> git commit
Git管理的是修改,当你用git add
命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit
只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
git diff HEAD -- readme.txt
命令可以查看工作区和版本库里面最新版本的区别
你可以继续git add
再git commit
,也可以别着急提交第一次修改,先git add
第二次修改,再git commit
,就相当于把两次修改合并后一块提交了:
第一次修改 -> git add
-> 第二次修改 -> git add
-> git commit
撤销修改
add前撤销
在准备提交前,发现了错误。既然错误发现得很及时,就可以很容易地纠正它。你可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用git status
查看一下:
1 | $ git status |
你可以发现,Git会告诉你,git checkout -- file
可以丢弃工作区的修改:
1 | $ git checkout -- readme.txt |
命令git checkout -- readme.txt
意思就是,把readme.txt
文件在工作区的修改全部撤销,这里有两种情况:
一种是readme.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是readme.txt
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit
或git add
时的状态。
git checkout -- file
命令中的--
很重要,没有--
,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout
命令。
add后撤销
将文件git add
到暂存区了
庆幸的是,在commit
之前,你发现了这个问题。用git status
查看一下,修改只是添加到了暂存区,还没有提交:
1 | $ git status |
Git同样告诉我们,用命令git reset HEAD <file>
可以把暂存区的修改撤销掉(unstage),重新放回工作区:
1 | $ git reset HEAD readme.txt |
git reset
命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD
时,表示最新的版本。
再用git status
查看一下,现在暂存区是干净的,工作区有修改:
1 | $ git status |
comit后撤销
假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得版本回退一节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。还记得Git是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把stupid boss
提交推送到远程版本库,你就真的惨了……
小结
- 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令
git checkout -- file
。 - 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令
git reset HEAD <file>
,就回到了场景1,第二步按场景1操作。 - 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。
删除文件
case1: 删除文件
确实要从版本库中删除该文件,那就用命令git rm
删掉,并且git commit
:
1 | $ git rm test.txt |
case2: 误删文件
另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本
1 | $ git checkout -- test.txt |
git checkout
其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
远程仓库
添加远程仓库
创建GitHub仓库
create a new repo
连接仓库
1 | $ git remote add origin git@github.com:<github账户名>/<仓库名>.git |
添加后,远程库的名字就是origin
,这是Git默认的叫法,也可以改成别的,但是origin
这个名字一看就知道是远程库。
关联一个远程库时必须给远程库指定一个名字,origin
是默认习惯命名。
推送内容
就可以把本地库的所有内容推送到远程库上
1 | $ git push -u origin master |
把本地库的内容推送到远程,用git push
命令,实际上是把当前分支master
推送到远程。
由于远程库是空的,我们第一次推送master
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令。
推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一模一样
从现在起,只要本地作了提交,就可以通过命令:
1 | $ git push origin master |
把本地master
分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!
删除远程库
如果添加的时候地址写错了,或者就是想删除远程库,可以用git remote rm <name>
命令。使用前,建议先用git remote -v
查看远程库信息:
1 | $ git remote -v |
然后,根据名字删除,比如删除origin
:
1 | $ git remote rm origin |
此处的“删除”其实是解除了本地和远程的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。要真正删除远程库,需要登录到GitHub,在后台页面找到删除按钮再删除。
分支管理
分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。
如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!

创建与合并分支
形象化描述
在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master
分支。HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
一开始的时候,master
分支是一条线,Git用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:
1 | HEAD |
每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master
分支的线也越来越长。
当我们创建新的分支,例如dev
时,Git新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上:
1 | master |
你看,Git创建一个分支很快,因为除了增加一个dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变:
1 | master |
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
1 | HEAD |
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支:
1 | HEAD |
实战
创建并切换到dev分支
创建dev
分支,然后切换到dev
分支:
1 | $ git checkout -b dev |
git checkout
命令加上-b
参数表示创建并切换,相当于以下两条命令:
1 | $ git branch dev |
然后,用git branch
命令查看当前分支:
1 | $ git branch |
git branch
命令会列出所有分支,当前分支前面会标一个*
号。
然后,我们就可以在dev
分支上正常提交,比如对readme.txt
做个修改,加上一行:
1 | Creating a new branch is quick. |
然后提交:
1 | $ git add readme.txt |
现在,dev
分支的工作完成,我们就可以切换回master
分支:
1 | $ git checkout master |
切换回master
分支后,再查看一个readme.txt
文件,刚才添加的内容不见了!因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变:

现在,我们把dev
分支的工作成果合并到master
分支上:
1 | $ git merge dev |
git merge
命令用于合并指定分支到当前分支。合并后,再查看readme.txt
的内容,就可以看到,和dev
分支的最新提交是完全一样的。
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
1 | $ git branch -d dev |
删除后,查看branch
,就只剩下master
分支了:
1 | $ git branch |
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master
分支上工作效果是一样的,但过程更安全。
switch
我们注意到切换分支使用git checkout <branch>
,而前面讲过的撤销修改则是git checkout -- <file>
,同一个命令,有两种作用,确实有点令人迷惑。
实际上,切换分支这个动作,用switch
更科学。因此,最新版本的Git提供了新的git switch
命令来切换分支:
创建并切换到新的dev
分支,可以使用:
1 | $ git switch -c dev |
直接切换到已有的master
分支,可以使用:
1 | $ git switch master |
使用新的git switch
命令,比git checkout
要更容易理解。
小结
- 查看分支:
git branch
- 创建分支:
git branch <name>
- 切换分支:
git checkout <name>
或者git switch <name>
- 创建+切换分支:
git checkout -b <name>
或者git switch -c <name>
- 合并某分支到当前分支:
git merge <name>
- 删除分支:
git branch -d <name>
解决冲突
准备新的feature1
分支,继续我们的新分支开发:
1 | $ git switch -c feature1 |
修改readme.txt
最后一行,改为:
1 | Creating a new branch is quick AND simple. |
在feature1
分支上提交:
1 | $ git add readme.txt |
切换到master
分支:
1 | $ git switch master |
Git还会自动提示我们当前master
分支比远程的master
分支要超前1个提交。
在master
分支上把readme.txt
文件的最后一行改为:
1 | Creating a new branch is quick & simple. |
提交:
1 | $ git add readme.txt |
现在,master
分支和feature1
分支各自都分别有新的提交,变成了这样:
1 | HEAD |
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
1 | $ git merge feature1 |
果然冲突了!Git告诉我们,readme.txt
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
1 | $ git status |
我们可以直接查看readme.txt的内容:
1 | Git is a distributed version control system. |
Git用<<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
1 | Creating a new branch is quick and simple. |
再提交:
1 | $ git add readme.txt |
现在,master
分支和feature1
分支变成了下图所示:
1 | HEAD |
用带参数的git log
也可以看到分支的合并情况:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
最后,删除feature1
分支:
1 | $ git branch -d feature1 |
小结
当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。
解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。
用 git log --graph
命令可以看到分支合并图。
分支管理策略
通常,合并分支时,如果可能,Git会用Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward
模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。
下面我们实战一下--no-ff
方式的git merge
:
首先,仍然创建并切换dev
分支:
1 | $ git switch -c dev |
修改readme.txt文件,并提交一个新的commit:
1 | $ git add readme.txt |
现在,我们切换回master
:
1 | $ git switch master |
准备合并dev
分支,请注意--no-ff
参数,表示禁用Fast forward
:
1 | $ git merge --no-ff -m "merge with no-ff" dev |
因为本次合并要创建一个新的commit,所以加上-m
参数,把commit描述写进去。
合并后,我们用git log
看看分支历史:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
可以看到,不使用Fast forward
模式,merge后就像这样:

分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master
分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev
分支上,也就是说,dev
分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev
分支合并到master
上,在master
分支发布1.0版本;
你和你的小伙伴们每个人都在dev
分支上干活,每个人都有自己的分支,时不时地往dev
分支上合并就可以了。
所以,团队合作的分支看起来就像这样:

小结
Git分支十分强大,在团队开发中应该充分应用。
合并分支时,加上--no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward
合并就看不出来曾经做过合并。
API的使用
获取最新Releases的版本号
代码
1 | wget -qO- -t1 -T2 "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g' |
可以搭配 xargs
进行自定义命令
代码解释
主字段
https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest
这里用的是 GitHub 的官方 API,格式为https://api.github.com/repos/{项目名}/releases/latest
打开上述链接后,可见包含下述字段的内容:
1
2
3
4
5
6"html_url": "https://github.com/ryanoasis/nerd-fonts/releases/tag/v3.1.1",
"id": 131468221,
"node_id": "RE_kwDOAaTAks4H1gu9",
"tag_name": "v3.1.1",
"target_commitish": "master",
"name": "v3.1.1",那么这里的
tag_name
就是我们所需要的东西啦wget 参数
1
wget -qO- -t1 -T2`,在这里,我们使用了 4 个参数,分别是`q,O-,t1,T2
-q
: q 就是 quiet 的意思了,没有该参数将会显示从请求到输出全过程的所有内容,这肯定不是我们想要的。-O-
:-O
是指把文档写入文件中,而-O-
是将内容写入标准输出,而不保存为文件。(注:这里是大写英文字母 O (Out),不是数字 0)-t1,-T2
: 前者是设定最大尝试链接次数为 1 次,后者是设定响应超时的秒数为 2 秒,两者可以防止失败后反复获取,导致后续脚本无法执行。
筛选参数
grep "tag_name"
: grep 是 Linux 一个强大的文本搜索工具,在本代码中输出 tag_name 所在行,即输出"tag_name": "v3.1.1",
head -n 1
:head -n
用于显示输出的行数,考虑到某些项目可能存在多个不同版本的 tag_name,这里我们只要第一个。awk -F ":" '{print $2}'
: awk 主要用于文本分析,在这里指定:
为分隔符,将该行切分成多列,并输出第二列。于是我们得到了(空格)"v3.1.1",
sed 's/\"//g;s/,//g;s/ //g'
: 在这里 sed 用于数据查找替换,如sed 's/要被取代的字串/新的字串/g'
,因此本段命令可分为 3 个,以分号分隔。s/\"//g
即将引号删除(反斜杠是为了防止引号被转义),以此类推,最终留下我们需要的内容:v3.1.1
。
忽略特殊文件
有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status
都会显示Untracked files ...
,有强迫症的童鞋心里肯定不爽。
好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore
文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
[!NOTE]
.gitignore
文件本身应该提交给Git管理,这样可以确保所有人在同一项目下都使用相同的.gitignore
文件。
不需要从头写.gitignore
文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:GitHub/gitignore
忽略文件的原则是:
- 忽略操作系统自动生成的文件,比如缩略图等;
- 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的
.class
文件; - 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。
举个例子:
假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini
文件,因此你需要忽略Windows自动生成的垃圾文件:
1 | # Windows: |
然后,继续忽略Python编译产生的.pyc
、.pyo
、dist
等文件或目录:
1 | # Python: |
加上你自己定义的文件,最终得到一个完整的.gitignore
文件,内容如下:
1 | # Windows: |
最后一步就是把.gitignore
也提交到Git,就完成了!当然检验.gitignore
的标准是git status
命令是不是说working directory clean
。
使用Windows的童鞋注意了,如果你在资源管理器里新建一个.gitignore
文件,它会非常弱智地提示你必须输入文件名,但是在文本编辑器里“保存”或者“另存为”就可以把文件保存为.gitignore
了。
有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore
忽略了:
1 | $ git add App.class |
如果你确实想添加该文件,可以用-f
强制添加到Git:
1 | $ git add -f App.class |
或者你发现,可能是.gitignore
写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore
命令检查:
1 | $ git check-ignore -v App.class |
Git会告诉我们,.gitignore
的第3行规则忽略了该文件,于是我们就可以知道应该修订哪个规则。
还有些时候,当我们编写了规则排除了部分文件时:
1 | # 排除所有.开头的隐藏文件: |
但是我们发现.*
这个规则把.gitignore
也排除了,并且App.class
需要被添加到版本库,但是被*.class
规则排除了。
虽然可以用git add -f
强制添加进去,但有强迫症的童鞋还是希望不要破坏.gitignore
规则,这个时候,可以添加两条例外规则:
1 | # 排除所有.开头的隐藏文件: |
把指定文件排除在.gitignore
规则外的写法就是!
+文件名,所以,只需把例外文件添加进去即可。
可以通过GitIgnore Online Generator在线生成.gitignore
文件并直接下载。
最后一个问题:.gitignore
文件放哪?答案是放Git仓库根目录下,但其实一个Git仓库也可以有多个.gitignore
文件,.gitignore
文件放在哪个目录下,就对哪个目录(包括子目录)起作用。
1 | myproject <- Git仓库根目录 |
配置多个 SSH Key
背景
同时使用两个 GitHub 帐号,需要为两个帐号配置不同的 SSH Key:
- 帐号 A 用于公司;
- 帐号 B 用于个人。
创建ssh-key
生成帐号 A 的 SSH Key,并在帐号 A 的 GitHub 设置页面添加 SSH 公钥:
1 | ssh-keygen -t ed25519 -C "GitHub User A" -f ~/.ssh/github_user_a_ed25519 |
生成帐号 B 的 SSH-Key,并在帐号 B 的 GitHub 设置页面添加 SSH 公钥:
1 | ssh-keygen -t ed25519 -C "GitHub User B" -f ~/.ssh/github_user_b_ed25519 |
配置ssh代理
创建好了上面的多个ssh key就可以开始管理他们了。 在终端中输入如下命令,查询系统ssh key的代理:
1 | $ ssh-add -l |
如果系统已经设置了代理,需要删除:
1 | $ ssh-add -D |
如果提示:
1 | Could not open a connection to your authentication agent. |
执行:
1 | $ exec ssh-agent bash |
接下来添加刚才创建的ssh key的私钥:
1 | # 第一个 |
添加公钥
其实就是将对应的.pub中的内容,复制到对应平台的ssh key管理栏目中,不同的平台,位置不同,可以去对应的个人中心的设置中查看,很容易找到。
配置config
创建或者修改文件 ~/.ssh/config
,添加如下内容:
1 | Host gt_a |
验证ssh-key
用 ssh 命令分别测试两个 SSH Key:
1 | $ ssh -T gt_a |
使用
拉取代码:
将 git@github.com
替换为 SSH 配置文件中对应的 Host
,如原仓库 SSH 链接为:
1 | git@github.com:owner/repo.git |
使用帐号 A 推拉仓库时,需要将连接修改为:
1 | gt_a:owner/repo.git |
多个git账户的提交问题
我们大多数人都会使用第三方工具进行git提交,比如source tree之类的,这些工具在提交时,如果不对对应的git仓库进行专门的配置,会默认走git的全局配置,也就是会用默认的全局配置的账户进行git提交。一不小心,就会用我们私人的账户,进行了公司项目的git提交,生成了对应的提交记录,也有可能因为权限问题,导致直接提交失败。
这时,我们需要对不同的仓库,进行对应的配置。
检查全局配置
在终端中,分别输入如下命令,可以检查目前电脑中的git的全局配置信息,如果没有返回,说明没有全局配置,如果有,就可以看到对应的默认的账户是那个了。
1
2$ git config --global user.name
$ git config --global user.email为了避免麻烦,我们可以取消全局配置:
1
2$ git config --global --unset user.name
$ git config --global --unset user.email全局配置和局部配置
此时已经取消了电脑中默认的git全局配置信息,此时进行git提交,会报对应的找不到账户信息的错误。
我们可以cd到对应的git仓库的根目录下,执行局部git配置命令。比如~/github/DemoProject
是一个在github平台托管的本地git仓库的根目录,我们可以执行如下命令:1
2
3$ cd ~/github/DemoProject
$ git config user.name
$ git config user.email如果返回均为空,说明没有进行过局部配置,可以分别配置github的账户名和邮箱:
1
2$ git config user.name "github账户名"
$ git config user.email "github@example.com"同理,在不同的git仓库下,可以分别配置不同平台的git账户名和git邮箱。这虽然看起来麻烦,不过,只要设置完成,之后只要不再更改对应的git仓库的路径,就不需要再更换配置了。
而且,即便我们没有取消默认的全局git配置,在进行了局部配置后,后者的优先级会更高。 执行:1
$ git config --list
可以查看查看当前仓库的具体配置信息,在当前仓库目录下查看的配置是全局配置+当前项目的局部配置,使用的时候会优先使用当前仓库的局部配置,如果没有,才会去读取全局配置。