版本控制工具

集中式版本控制工具

集中化的版本控制系统诸如 CVS,Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。多年以来,这已成为版本控制系统的标准做法

这种做法带来了许多好处,现在,每个人都可以在一定程度上看到项目中的其他人正在做些什么。而管理员也可以轻松掌控每个开发者的权限,并且管理一个集中化的版本控制系统;要远比在各个客户端上维护本地数据库来得轻松容易

事分两面,有好有坏。这么做最显而易见的缺点是中央服务器的单点故障

如果服务器宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作

并不是说服务器故障了就没有办法写代码了,只是在服务器故障的情况下写的代码是没有办法得到保障的。试想svn中央服务器挂机一天,你还拼命写了天代码,其中12点之前的代码都是高质量可靠的,而且有很多闪光点。而12点之后的代码由于你想尝试一个比较大胆的想法,将代码改的面目全非了。这样下来你12点之前做的工作也都白费了。有记录的版本只能是 svn 服务器挂掉时保存的版本!

要是中央服务器的磁盘发生故障,碰巧没做备份,或者备份不够及时,就会有丢失数据的风险。最坏的情况是彻底丢失整个项目的所有历史更改记录,而被客户端偶然提取出来的保存在本地的某些快照数据就成了恢复数据的希望。但这样的话依然是个问题,你不能保证所有的数据者都已经有人事先完整提取出来过。只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险

集中式总结

  • 优点:代码存放在单一的服务器上,便于项目管理
  • 缺点:服务器宕机、故障

分布式版本控制工具

于是分布式版本控制系统面世了。在这类系统中,像 Git、BitKeeper 等,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。因为每一次的提取操作,实际上都是一次对代码仓库的完整备份

分布式的版本控制系统在管理项目时存放的不是项目版本与版本之间的差异,它存的是索引(所需磁盘空间很少,所以每个客户端都可以放下整个项目的历史记录)

分布式的版本控制系统出现之后,解决了集中式版本控制系统的缺陷:

  • 1)断网的情况下也可以进行开发(因为版本控制是在本地进行的)
  • 2)使用 git 业进行团队协作,哪怕 github 挂了,每个客户端保存的也都是整个完整的项目(包含历史记录的!!)

Git 简介

  • Git 是 Linux 发明者 Linus 开发的一款新时代的版本控制系统 ,版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统,对于软件开发领域来说版本控制是最重要的一环,而 Git 毫无疑问是当下最流行、最好用的版本控制系统。

  1. 图中左侧为工作区,右侧为版本库。在版本库中标记为index的区域是暂存区(stage,index),标记为master的是master分支所代表的目录树。
  2. 图中我们可以看出此时HEAD指针实际是指向master分支的一个“游标”。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。
  3. 图中的objects标识的区域为Git的对象库,实际位于.git/objects目录下。
  4. 当对工作区修改(或新增)的文件执行git add命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。
  5. 当执行提交操作git commit时,暂存区的目录树写到版本库(对象库)中,master分支会做相应的更新。即master指向的目录树就是提交时暂存区的目录树。
  6. 当执行git reset HEAD命令时,暂存区的目录树会被重写,被master分支指向的目录树所替换,但是工作区不受影响。
  7. 当执行git rm --cached <file>命令时,会直接从暂存区删除文件,工作区则不做出改变。
  8. 当执行git checkout .或者git checkout -- <file>命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。
  9. 当执行git checkout HEAD .或者git checkout HEAD <file>命令时,会用HEAD指向的master分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

安装与设置

Git 安装

官网地址:https://git-scm.com/

查看 GNU 协议,可以直接点击下一步

选择 Git 安装位置,要求是非中文并且没有空格的目录,然后下一步

Git 选项配置,推荐默认设置,然后下一步

Git 安装目录名,不用修改,直接点击下一步

Git 的默认编辑器,建议使用默认的 Vim 编辑器,然后点击下一步

默认分支名设置,选择让 Git 决定,分支名默认为 master,下一步

修改 Git 的环境变量,选第一个,不修改环境变量,只在 Git Bash 里使用 Git

选择后台客户端连接协议,选默认值 OpenSSL,然后下一步

配置 Git 文件的行末换行符,Windows 使用 CRLF,Linux 使用 LF,选择第一个自动转换,然后继续下一步

选择 Git 终端类型,选择默认的 Git Bash 终端,然后继续下一步

选择 Git pull 合并的模式,选择默认,然后下一步

选择 Git 的凭据管理器,选择默认的跨平台的凭据管理器,然后下一步

其他配置,选择默认设置,然后下一步

实验室功能,技术还不成熟,有已知的 bug,不要勾选,然后点击右下角的 Install 按钮,开始安装 Git

点击 Finsh 按钮,Git 安装成功!

右键任意位置,在右键菜单里选择 Git Bash Here 即可打开 Git Bash 命令行终端

在 Git Bash 终端里输入 git --version 查看 git 版本,如图所示,说明 Git 安装成功

命令查询

  • 使用--global参数表示设置了全局的环境,如果想对与特定的项目使用不同的用户名和邮件地址,则可已在该项目目录下不使用--global参数设置不同的用户名和邮件地址。

  • git config --list命令可以列出当前Git所有的配置信息。

设置命令

  • 通常情况下,安装完Git后的第一件事就是设置用户名称邮件地址。每一个Git的提交都会使用这些信息,如果不设置则无法进行提交。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 用户名和邮箱也可以填写别的(只要是用户名和邮箱格式就OK)。
    git config --global user.name "fl"
    git config --global user.email "fl_6145@163.com"
    # 设置主分支名称
    git config --global init.defaultBranch main
    # 忽略ssl证书错误 禁用ssl校验
    git config --global http.sslVerify false
    # 保存密码
    git config --global credential.helper store
    # 全局过滤列表
    git config --global core.excludesfile /e/fulsun/Documents/.gitignore_global
    # 回车换行符input:Windows系统上的签出文件中保留CRLF,会在Mac和Linux系统上,包括仓库中保留LF。
    git config --global core.autocrlf input
    # 查看设置值
    git config --list
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #.gitignore_global
    target/
    !.mvn/wrapper/maven-wrapper.jar

    ## STS ##
    .apt_generated
    .classpath
    .factorypath
    .project
    .settings
    .springBeans

    ## IntelliJ IDEA ##
    .idea
    *.iws
    *.iml
    *.ipr

    ## JRebel ##
    rebel.xml
    .rebel.xml.bak

    ## MAC ##
    .DS_Store

    ## Other ##
    logs/
    temp/
    ——————

git别名

1
2
3
4
5
git config --global alias.lg1 "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all"

git config --global alias.lg2 "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n''%C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all"

git config --global alias.lg '!"git lg1"'

下载问题

1
2
3
4
git config --global http.postBuffer 104857600   # 100MB
# Git缓存值设置为5G 5242880000
git config --global http.postBuffer 5242880000
git config --global https.postBuffer 5242880000

status 乱码问题

1
git config --global core.quotepath false

Git 命令操作

  • 命令行(win + R),输入 cmd 后,输入 git可以判断是否成功安装。

  • 网上找了个图,别人整理的一张图,很全很好,借来用下。下面详细解释一些常用命令。

  • 在掌握具体命令前,先理解下HEADHEAD,它始终指向当前所处分支的最新的提交点。你所处的分支变化了,或者产生了新的提交点,HEAD就会跟着改变。

Init/Clone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 初始化本地库
git init

# 浅尝clone: depth的参数控制着clone最近多少次的提交。
git clone <版本库的网址> <本地目录名>
git clone --depth=1 http://xxx.git
git clone --depth=1 git@github.com:xxx.git

# 拉取完整当前分支
git fetch --unshallow

# 追踪所有远程分支
git remote set-branches origin '*'

# 拉取所有远程分支
git fetch -v
  • 以上2种clone 方式有如下区别:

    • https方式:不管是谁,只要拿到该项目的 url 可以随便 clone,但是在 push 到远程的时候需要验证用户名和密码;
    • ssh方式:需要现将你电脑的SSH key(SSH公钥)添加到GitHub(或者其他代码托管网站)上,这样在 clone 项目和 push 项目到远程时都不需要输入用户名和密码。
    • 通常来说,Git协议下载速度最快,SSH协议用于需要用户认证的场合。各种协议优劣的详细讨论请参考官方文档
  • git clone支持多种协议,除了HTTP(s)以外,还支持SSH、Git、本地文件协议等,下面是一些例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    git clone http[s]://example.com/path/to/repo.git
    git clone ssh://example.com/path/to/repo.git
    git clone git://example.com/path/to/repo.git
    git clone /opt/git/project.git
    git clone file:///opt/git/project.git
    git clone ftp[s]://example.com/path/to/repo.git
    git clone rsync://example.com/path/to/repo.git
    # SSH协议还有另一种写法
    git clone [user@]example.com:path/to/repo.git

git status

  • git status 这个命令顾名思义就是查看状态,这个命令可以算是使用最频繁的一个命令了,建议大家没事就输入下这个命令,来查看你当前 git 仓库的一些状态。一般会显示当前所处的分支,以及当前工作区是否干净,是否有文件要提交。

  • 当你克隆远程仓库到本地后,通过该命令查看当前状态时,显示信息如下图所示:

  • 当你修改了一个文件后,再用该命令查看当前状态,它会提示你把当前的变更添加到暂存区或者丢弃该变更,显示信息如下图所示:

  • 当你新增一个文件后,用该命令查看当前状态,它会显示新增的文件状态是未跟踪,并且提示用git add命令将其添加到暂存区,显示信息如下图所示:

git add

新建立一个文件,输入 git status 后查看 ,Untracked files ,就是说 a.md 这个文件还没有被跟踪输入 git add a.md,然后再输入 git status :提示以下文件 Changes to be committed , 意思就是 a.md 文件等待被提交,当然你可以使用 git rm --cached 这个命令去移除这个缓存.

1
2
3
4
5
6
7
8
# 添加当前目录的所有文件到暂存区 
git add .
# 添加指定目录到暂存区,包括子目录
git add [dir]
# 添加指定的文件到暂存区
git add [file1]
# 使用命令,删除暂存区该文件(只是删除暂存区,不影响工作区)
git rm --cached hello.txt

git commit

1
2
3
4
5
6
7
8
9
10
# -m 表示添加一个版本日志信息,不写此参数也会打开日志信息的文件框。一般带参数
# 提交暂存区到本地仓库,message代表说明消息
git commit -m [message]
# 提交暂存区的指定文件到本地仓库
git commit [file1] -m [message]
# 使用一次新的commit,替代上一次提交
git commit --amend -m [message]

# 保存后,git 里是按照行维护文件, 会将本次修改行数信息进行提示
# 1 insertion(+), 1 deletion(-)

git log

这个时候我们输入 git log 命令,查看所有产生的 commit 记录,所以可以看到已经产生了一条 commit 记录,而提交时候的附带信息叫 ‘first commit’ 。

1
2
3
4
5
6
# 查看精简版本信息
git reflog
# 查看详细版本信息
git log
# 以图形的方式查看历史(这个我比较常用,能很好的显示各个分支之间的关系)
$ git log --graph
  • git log

  • –graph 左边显示出了分支的图形

git diff

  • diff 显示变更内容, 当你对文件进行了修改,想查看进行了哪些修改时,可以通过该命令查看。

  • git diff命令会显示修改的文件中哪些内容进行了修改,包括新增了哪些内容,删除了哪些内容。

    1
    2
    3
    4
    5
    6
    # 后面不接参数,表示显示所有修改文件的变更
    git diff
    # 显示工作区与当前分支最新commit之间的差异
    git diff HEAD
    # 后面接文件名,表示只显示该文件的变更
    git diff README.md
  • 例如:我对 README.md 文件进行了修改,删除了1行,新增了2行,然后用该命令查看进行了哪些修改,如下图所示: (“+” 表示新增的内容,“-” 表示删除的内容)

git add & git commit

看到这里估计很多人会有疑问,我想要提交直接进行 commit 不就行了么,为什么先要再 add一次呢?

首先 git add 是先把改动添加到一个「暂存区」,你可以理解成是一个缓存区域,临时保存你的改动,而 git commit 才是最后真正的提交。这样做的好处就是防止误提交。

当然也有办法把这两步合并成一步,为了实现一次性进行add和commit,我们可以使用git commit -a命令。这个命令的含义是将当前Git跟踪的所有已修改的文件一并提交,并且会自动将这些文件添加到暂存区。

1
2
3
# -a 参数的作用是将所有已修改的文件添加到暂存区,
# -m 参数用来设置提交信息。
git commit -am "Initial commit"

reset

  • reset命令把当前分支指向另一个位置,并且相应的变动工作区和暂存区。

命令 说明
git reset –soft [commit] 只改变提交点,暂存区和工作目录的内容都不改变
git reset –mixed [commit] 只改变提交点,同事改变暂存区的内容
git reset –hard [commit] 暂存区和工作区的内容都会被修改到与提交点完全一致的状态
git reset –hard HEAD 让工作区回到上次提交的状态
  • 我们可以先用 git log 看一下当前历史版本,如下图所示:

  • 如果要回退到前一个版本,则只需要输入:git reset HEAD~1,执行完再看一下历史版本:

  • 我们已经回退到前一个版本了。如果需要回退到前2个版本,命令是:git reset HEAD~2,回退到前n个版本就是:git reset HEAD~n

  • 如果需要回退到任何一个版本,则需要替换成该版本的 commit id 就可以了,例如:git reset a8336834b50daafa0793370,执行完再看一下历史:

revert

  • git revert用一个新提交来消除一个历史提交所做的任何修改。

revert与reset的区别

  • git revert是用一次新的commit来回滚之前的commit

  • git reset是直接删除指定的commit。

  • 在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。

    • 因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,减少冲突。
    • 但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入,产生很多冲突。
  • git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。

git tag

  • 我们在客户端开发的时候经常有版本的概念,比如v1.0、v1.1之类的,不同的版本肯定对应不同的代码,所以我一般要给我们的代码加上标签,这样假设v1.1版本出了一个新bug,但是又不晓得v1.0是不是有这个bug,有了标签就可以顺利切换到v1.0的代码,重新打个包测试了。

  • 所以如果想要新建一个标签很简单,比如 git tag v1.0就代表我在当前代码状态下新建了一个v1.0的标签,输入 git tag 可以查看历史 tag 记录。

  • 想要切换到某个tag怎么办?也很简单,执行 git checkout v1.0 ,这样就顺利的切换到 v1.0tag的代码状态了。

  • 截止到图中的最近一次提交,我们完成了 1.0 版本的开发,则可以通过以下命令为其打上版本的标签。

    • 命令:

      1
      2
      git tag v1.0 //为当前提交打上 v1.0 的标签
      git tag v1.0 ab1591eb4e06c1e93fdd50126b9fab8a88d89155 //为这个节点打上 v1.0 的标签

    • 图中可以看出 v1.0 标签已经打上了。 如果发现标签打错了,想删除某个标签,则可以通过如下命令来执行。 命令:git tag -d v1.0 删除 v1.0 标签

    • 如果想将标题推送到远程库,则可以使用如下命令来完成。 命令:git push origin --tags 将打的 tag 都推送到远程库

git show

  • 显示信息,可用于显示某次提交或者某个 tag 相关的信息。

  • 命令:git show commit_id 显示某次提交的详细信息

  • 命令:git show tag_name 显示某个 tag 的详细信息

git blame

  • 查看文件每行的提交历史(追责),可用于查看某个文件中的每一行是那次提交产生的,是谁提交的,什么时候提交的,提交的版本号是多少等等详细信息,在实际工作中方便对出问题的代码进行追责,找到产生 BUG 的责任人。

  • 命令:git blame file_name

  • 上图中可以看到 README.md 这个文件有 5 行,其中后 4 行都是我在 2018 年提交的,第 1 行是另外一个人在 2017 年提交的。

git remote

为了便于管理,Git要求每个远程主机都必须指定一个主机名。git remote命令就用于管理主机名。不带选项的时候,git remote命令列出所有远程主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 列出所有远程主机 -v选项,可以参看远程主机的网址
git remote -v

# 克隆版本库的时候,所使用的远程主机自动被Git命名为origin。
# 如果想用其他的主机名,需要用git clone命令的-o选项指定。


# 查看该主机的详细信息 git remote show origin
git remote show <主机名>

# 添加远程主机。
git remote add <主机名> <网址>

# 删除远程主机
git remote rm <主机名>

# 远程主机的改名
git remote rename <原主机名> <新主机名>

git fetch

一旦远程主机的版本库有了更新(Git术语叫做commit),需要将这些更新取回本地,这时就要用到git fetch命令。

1
$ git fetch <远程主机名>

上面命令将某个远程主机的更新,全部取回本地。

git fetch命令通常用来查看其他人的进程,因为它取回的代码对你本地的开发代码没有影响。

默认情况下,git fetch取回所有分支(branch)的更新。如果只想取回特定分支的更新,可以指定分支名。

1
$ git fetch <远程主机名> <分支名>

比如,取回origin主机的master分支。

1
$ git fetch origin master

所取回的更新,在本地主机上要用”远程主机名/分支名“的形式读取。比如origin主机的master,就要用origin/master读取。

git branch命令的-r选项,可以用来查看远程分支,-a选项查看所有分支。

1
2
3
4
5
6
$ git branch -r
origin/master

$ git branch -a
* master
remotes/origin/master

上面命令表示,本地主机的当前分支是master,远程分支是origin/master

取回远程主机的更新以后,可以在它的基础上,使用git checkout命令创建一个新的分支。

1
$ git checkout -b newBrach origin/master

上面命令表示,在origin/master的基础上,创建一个新分支。

此外,也可以使用git merge命令或者git rebase命令,在本地分支上合并远程分支。

1
2
3
$ git merge origin/master
# 或者
$ git rebase origin/master

上面命令表示在当前分支上,合并origin/master

Git 分支操作

命令 说明
git branch 列出本地所有分支
git branch -r 列出所有远程分支
git branch -a 列出所有本地分支和远程分支
git branch [branch-name] 新建一个分支,但是停留在当前分支
git branch -b [branch-name] 新建一个分支并切换到该分支
git branch –track [branch][remote-branch] 新建一个分支,与指定的分支建立追踪关系
git checkout [branch-name] 切换到指定的分区,并更新工作区
git branch -d [branch-name] 删除分支
git push origin –delete [branch-name] 删除远程分支,不要加 origin/

什么是分支

在版本控制过程中,同时推进多个任务,为每个任务,我们就可以创建每个任务的单独分支。使用分支意味着程序员可以把自己的工作从开发主线上分离开来,开发自己分支的时候,不会影响主线分支的运行。对于初学者而言,分支可以简单理解为副本,一个分支就是一个单独的副本(分支底层其实也是指针的引用)

分支的好处:同时并行推进多个功能开发,提高开发效率,各个分支在开发过程中,如果某一个分支开发失败,不会对其他分支有任何影响。失败的分支删除重新开始即可

创建/查看分支

执行git init 初始化git仓库之后会默认生成一个主分支 master ,也是你所在的默认分支,也基本是实际开发正式环境下的分支,一般情况下 master 分支不会轻易直接在上面操作的,你们可以输入 git branch查看下当前分支情况:

1
2
3
4
5
# 新建了一个名字叫 [name] 的分支
git branch [name]
# 查看的当前分支情况, * 号表示当前分支
git branch
git branch -v

checkout 分支

切换分支执行这个命令git checkout [branchName],那有人就说了,我要先新建再切换,未免有点麻烦,有没有一步到位的,聪明: git checkout -b [branchName] 这个命令的意思就是新建分支,并且自动切换到分支

1
2
3
4
# 切换分支 
git checkout 分支名
# 创建并切换分支
git checkout -b [branchName]

删除分支

有新建分支,那肯定有删除分支,假如这个分支新建错了,或者a分支的代码已经顺利合并到master 分支来了,那么a分支没用了, 就可以把a分支删除了。

有些时候可能会删除失败,比如如果a分支的代码还没有合并到master,你执行 git branch -d a 是删除不了的,它会智能的提示你a分支还有未合并的代码,但是如果你非要删除,那就执行 git branch -D a 就可以强制删除a分支。

1
2
3
4
5
6
# 删除分支
git branch -d [branch]

# 强制删除
git branch -D

如果远程主机删除了某个分支,默认情况下,git pull 不会在拉取远程分支的时候,删除对应的本地分支。这是为了防止,由于其他人操作了远程主机,导致git pull不知不觉删除了本地分支。但是,你可以改变这个行为,加上参数 -p 就会在本地删除远程已经删除的分支。

1
git pull -p

merge分支

  • merge命令把不同的分支合并起来。如图,在实际开放中,我们可能从master分支中切出一个分支,然后进行开发完成需求,中间经过R3,R4,R5的commit记录,最后开发完成需要合入master中,这便用到了merge。

    1
    2
    3
    4
    # merge之前先拉一下远程仓库最新代码
    git fetch [remote]
    # 合并指定分区到当前分区
    git merge [branch]

  • A同学在a分支代码写的不亦乐乎,终于他的功能完工了,并且测试也都ok了,准备要上线了,这个时候就需要把他的代码合并到主分支master上来,然后发布。git merge 就是合并分支用到的命令. 针对这个情况,需要先做两步,

    • 第一步是切换到 master 分支,如果你已经在了就不用切换了
    • 第二步执行 git merge a,意思就是把a分支的代码合并过来,不出意外, 这个时候a分支的代码就顺利合并到 master 分支来了。
    • 为什么说不出意外呢?因为这个时候可能会有冲突而合并失败。

正常合并

冲突合并

冲突产生的原因:合并分支时,两个分支在同一个文件的同一个位置有两套完全不同的修改。Git无法替我们决定使用哪一个。必须人为决定新代码内容

解决冲突

rebase分支

  • rebase又称为衍合,是合并的另外一种选择。

  • 在开始阶段,我们处于new分支上,执行git rebase dev,那么new分支上新的commit都在master分支上重演一遍,最后checkout切换回到new分支。这一点与merge是一样的,合并前后所处的分支并没有改变。

  • git rebase dev,通俗的解释就是new分支想站在dev的肩膀上继续下去。rebase也需要手动解决冲突

  • 使用说明:https://git-scm.com/docs/git-rebase

    • 不要通过rebase对任何已经提交到公共仓库中的commit进行修改,
    • 不要在master分支上rebase,在master分支上rebase就会出现游离分支。
    • 正常做法是比如在dev分支开发,然后需要的话在dev分支rebase,然后再merge到master

准备示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 我们初始化一个项目
git init
# 制造一些提交
touch base.txt
git add .
git commit -m "add base"

touch 1.txt
git add .
git commit -m "add 1"

touch 2.txt
git add .
git commit -m "add 2"

touch 3.txt
git add .
git commit -m "add 3"

touch 4.txt
git add .
git commit -m "add 4"

touch 5.txt
git add .
git commit -m "add 5"

合并commit

  • 命令 git rebase -i [startpoint] [endpoint]

    • -i 的意思是 --interactive,即弹出交互式的界面让用户编辑完成合并操作,
    • [startpoint] [endpoint] 则指定了一个编辑区间,如果不指定[endpoint],则该区间的终点默认是当前分支HEAD所指向的commit(注:该区间指定的是一个前开后闭的区间)。
  • 合并多个commit为一个完整commit

    1
    2
    3
    4
    5
    6
    7
    # 在查看到了log日志后,我们运行以下命令:
    * 09b85cb - (17 minutes ago) add 5 - XXX (HEAD -> main)
    * 0f89d77 - (17 minutes ago) add 4 - XXX
    * 6c82fb4 - (17 minutes ago) add 3 - XXX
    * 4c4e075 - (17 minutes ago) add 2 - XXX
    * eda0aa7 - (17 minutes ago) add 1 - XXX
    * e90bc6c - (17 minutes ago) add base - XXX
  • 把如下分支add 4/5二个提交记录合并为一个完整的提交,然后再push到公共仓库。

    1
    2
    3
    4
    # 合并最近二次提交 需要选择二次提交的前一个提交
    git rebase -i 6c82fb4
    # 或者
    git rebase -i HEAD~2
  • 出交互式的界面,我们可以编辑说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    pick 0f89d77 add 4
    pick 09b85cb add 5

    # Rebase 6c82fb4..09b85cb onto 6c82fb4 (2 commands)
    #
    # Commands:
    每一个commit id 前面的pick表示指令类型,git 为我们提供了以下几个命令:
    pick:保留该commit(缩写:p)
    reword:保留该commit,但我需要修改该commit的注释(缩写:r)
    edit:保留该commit, 但我要停下来修改该提交(不仅仅修改注释)(缩写:e)
    squash:将该commit和前一个commit合并(缩写:s)
    fixup:将该commit和前一个commit合并,但我不要保留该提交的注释信息(缩写:f)
    exec:执行shell命令(缩写:x)
    drop:我要丢弃该commit(缩写:d)

    ## 合并add 5到add 4
    pick 0f89d77 add 4
    s 09b85cb add 5
  • 保存后是注释修改界面,修改保存后查看日志

    1
    2
    3
    4
    5
    6
    PS C:\Users\sfuli\Desktop\test> git lg
    * 7290f75 - (31 minutes ago) add 4 5 - XXX (HEAD -> main)
    * 6c82fb4 - (31 minutes ago) add 3 - XXX
    * 4c4e075 - (31 minutes ago) add 2 - XXX
    * eda0aa7 - (31 minutes ago) add 1 - XXX
    * e90bc6c - (31 minutes ago) add base - XXX

commit粘到其他分支

  • 当我们项目中存在多个分支,有时候我们需要将某一个分支中的一段commit记录 同时应用到其他分支中

  • 我们希望将develop分支中的C~E部分复制到master分支中,这时我们就可以通过rebase命令来实现(如果只是复制某一两个提交到其他分支,建议使用更简单的命令:git cherry-pick)。

  • 命令: git rebase [startpoint] [endpoint] --onto [branchName]

    • [startpoint] [endpoint] 仍然和上一个命令一样指定了一个编辑区间(前开后闭)
    • --onto 的意思是要将该指定的提交复制到哪个分支上。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 将develop分支中的C~E部分复制到master分支中
    # 为了让这个区间包含C(90bc0045b)提交,我们将区间起始点向后退了一步。C(90bc0045b)和E(5de0da9f2)
    git rebase 90bc0045b^ 5de0da9f2 --onto master

    # 执行完成后虽然此时HEAD所指向的内容正是我们所需要的,但是master分支是没有任何变化的
    # git只是将C~E部分的提交内容复制一份粘贴到了master所指向的提交后面
    # 我们需要做的就是将master所指向的提交id设置为当前HEAD所指向的提交id
    # 即
    git checkout master
    git reset --hard 0c72e64

rebase与merge的区别

  • merge操作会生成一个新的节点,之前的提交分开显示。

  • rebase操作不会生成新的节点,是将两个分支融合成一个线性的提交。

  • 现在我们有这样的两个分支,test和master,提交如下:

    1
    2
    3
          D---E test
    /
    A---B---C---F master
  • 在master执行 git merge test,然后得到如下结果:

    1
    2
    3
          D--------E
    /
    A---B---C---F----G test, master
  • 在master执行 git rebase test,然后得到如下结果:

    1
    A---B---D---E---C'---F'   test, master
  • 如果你想要一个干净的,没有merge commit的线性历史树,那么你应该选择git rebase

  • 如果你想保留完整的历史记录,并且想要避免重写commit history的风险,你应该选择使用git merge

git cherry-pick

  • 挑拣节点合并到当前分支上,该命令一般用于从其他分支上挑拣某些节点到当前分支。

  • 命令:git cherry-pick commit_id

  • 上图中我想把 ruby_client 分支上的 e43a6 这个节点合并到 master 分支上,但不需要 5ddae 这个节点,那么我们就可以使用下面的命令:

    1
    2
    $ git checkout master // 先切换到 master 分支
    $ git cherry-pick e43a6 //将 e43a6 节点挑拣合并到当前分支
  • 完成后如下图所示: 注意:该节点被挑拣合并到 master 上后会产生一个新的节点a0a41

创建分支和切换分支图解

master、hot-fix 其实都是指向具体版本记录的指针。当前所在的分支,其实是由 HEAD 决定的。所以创建分支的本质就是多创建一个指针

  • HEAD 如果指向 master,那么我们现在就在 master 分支上
  • HEAD 如果指向 hotfix,那么我们现在就在 hotfix 分支上

所以切换分支的本质就是移动HEAD指针

stash分支

  • git stash命令:将当前未提交的修改(即,工作区的修改和暂存区的修改)先暂时储藏起来,这样工作区干净了后,就可以切换切换到master分支下拉一个fix分支。在完成线上bug的修复工作后,重新切换到dev分支下通过git stash pop命令将之前储藏的修改取出来,继续进行新功能的开发工作

  • 没有在git 版本控制中的文件,是不能被git stash 存起来的

  • 常规git stash的一个限制是它会一下暂存所有的文件。有时,只备份某些文件更为方便,让另外一些与代码库保持一致。一个非常有用的技巧,用来备份部分文件

    1. add 那些你不想备份的文件(例如: git add file1.js, file2.js
    2. 调用 git stash –keep-index。只会备份那些没有被add的文件。
    3. 调用 git reset 取消已经add的文件的备份,继续自己的工作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    # 查看储藏记录列表
    git stash list
    stash@{index}: WIP on [分支名]: [最近一次的commitID] [最近一次的提交信息]

    # 标识储藏记录
    git stash save [stashMessage]

    # 取出最近一次储藏的修改到工作区
    # 命令恢复之前缓存的工作目录,将缓存堆栈中的对应stash删除,并将对应修改应用到当前的工作目录下,默认为第一个stash,即stash@{0}
    # 如果要应用并删除其他stash 比如应用并删除第二个:git stash pop stash@{1}
    git stash pop
    git stash pop stash@{$num}

    # 取出指定index的储藏的修改到工作区中,但不会把存储从存储列表中删除,默认使用第一个存储,即stash@{0}
    # 取出第二个:git stash apply stash@{1}
    git stash apply stash@{index}

    # 将指定index的储藏从储藏记录列表中删除
    git stash drop stash@{index}

    # 清空所有缓存的stash
    git stash clear

    # 显示做了哪些改动,默认show第一个存储,如果要显示其他存贮,后面加`stash@{$num}`,比如第二个` git stash show stash@{1}`
    git stash show
    # 显示指定的 stash 中保存的所有文件的列表
    git stash show stash@{0} --name-only
    # 显示第一个存储的改动,如果想显示其他存存储,命令:git stash show stash@{$num} -p
    git stash show -p

push推送

  • 上传本地仓库分支到远程仓库分支,实现同步。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 上传本地指定分支到远程仓库
    git push <远程主机名> <本地分支名>:<远程分支名>
    # 如果省略远程分支名,则表示将本地分支推送与之存在"追踪关系"的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。
    # 将本地的`master`分支推送到`origin`主机的`master`分支。如果后者不存在,则会被新建。
    git push origin master
    # 强行推送当前分支到远程仓库,即使有冲突
    git push [remote] –force
    # 推送所有分支到远程仓库
    git push [remote] -all

    # 如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。
    git push origin :master
    # 等同于
    git push origin --delete master

    # 如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。
    git push origin
    # 如果当前分支只有一个追踪分支,那么主机名都可以省略。
    git push

    # 如果当前分支与多个主机存在追踪关系,则可以使用`-u`选项指定一个默认主机,这样后面就可以不加任何参数使用`git push`。
    # 将本地的`master`分支推送到`origin`主机,同时指定`origin`为默认主机,后面就可以不加任何参数使用`git push`了
    git push -u origin master
  • 不带任何参数的git push,默认只推送当前分支,这叫做simple方式。此外,还有一种matching方式,会推送所有有对应的远程分支的本地分支。Git 2.0版本之前,默认采用matching方法,现在改为默认采用simple方式。如果要修改这个设置,可以采用git config命令。

    1
    2
    3
    git config --global push.default matching
    # 或者
    git config --global push.default simple
  • 还有一种情况,就是不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用--all选项。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    git push --all origin
    # 上面命令表示,将所有本地分支都推送到`origin`主机。

    # 如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做`git pull`合并差异,然后再推送到远程主机。这时,如果你一定要推送,可以使用`--force`选项。
    git push --force origin

    # 上面命令使用`--force`选项,结果导致远程主机上更新的版本被覆盖。除非你很确定要这样做,否则应该尽量避免使用`--force`选项。

    最后,`git push`不会推送标签(tag),除非使用`--tags`选项。
    git push origin --tags
  • 新建仓库 推送

    1
    2
    3
    4
    5
    6
    7
    8
    echo "# README" >> README.md
    git init
    git add README.md
    git commit -m "first commit"
    git branch -M main
    git remote add origin git@github.com:xxx/xxx.git

    git push -u origin main
  • 已经存在的仓库

    1
    2
    3
    4
    5
    # 删除关联的origin的远程库
    git remote rm origin
    git remote add origin https://gitee.com/xxxxxx.git
    git branch -M main
    git push -u origin main # git push --set-upstream origin main

pull 分支

  • git pull命令的作用是,取回远程主机某个分支的更新,再与本地的指定分支合并。它的完整格式稍稍有点复杂。一般是本地分支的进度落后于远程分支时,需要使用该命令:git pull <远程主机名> <远程分支名>:<本地分支名>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 取回 origin 主机的 next 分支,与本地的 master 分支合并,需要写成下面这样。
    git pull origin next:master
    # 如果远程分支是与当前分支合并,则冒号后面的部分可以省略。
    git pull origin next
    # 上面命令表示,取回`origin/next`分支,再与当前分支合并。实质上,这等同于先做`git fetch`,再做`git merge`
    $ git fetch origin
    $ git merge origin/next

    # 在本地删除远程已经删除的分支 如果远程主机删除了某个分支,默认情况下,`git pull` 不会在拉取远程分支的时候,删除对应的本地分支。
    $ git pull -p
    # 等同于下面的命令
    $ git fetch --prune origin
    $ git fetch -p
  • 在某些场合,Git会自动在本地分支与远程分支之间,建立一种追踪关系(tracking)。比如,在git clone的时候,所有本地分支默认与远程主机的同名分支,建立追踪关系,也就是说,本地的master分支自动”追踪”origin/master分支。Git也允许手动建立追踪关系。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 指定`master`分支追踪`origin/next`分支。
    git branch --set-upstream master origin/next

    # 如果当前分支与远程分支存在追踪关系,`git pull`就可以省略远程分支名。
    # 下面命令表示本地的当前分支自动与对应的`origin`主机"追踪分支"(remote-tracking branch)进行合并。
    $ git pull origin

    # 如果当前分支只有一个追踪分支,连远程主机名都可以省略。表示当前分支自动与唯一一个追踪分支进行合并。
    $ git pull
  • 常用 git pull --rebase origin master 用 rebase 的方式进行,不会产生 merge 保持分支干净、整洁

  • pull 分支报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $ git pull origin
    提示:您有不同的分支,需要指定如何协调它们。
    hint: You have divergent branches and need to specify how to reconcile them.
    提示:您可以通过在之前某个时间运行以下命令之一来做到这一点
    hint: You can do so by running one of the following commands sometime before
    hint: your next pull:
    hint:
    hint: git config pull.rebase false # merge
    hint: git config pull.rebase true # rebase
    hint: git config pull.ff only # fast-forward only 仅快进
    hint:
    提示:可以将“git config”替换为“git config——global”来设置默认值
    hint: You can replace "git config" with "git config --global" to set a default
    提示:首选所有存储库。你也可以传递——rebase,——no-rebase,
    hint: preference for all repositories. You can also pass --rebase, --no-rebase,
    提示:或命令行上的——ff-only,以覆盖配置的默认per
    hint: or --ff-only on the command line to override the configured default per
    hint: invocation.
    fatal:需要指定如何协调不同的分支。
    fatal: Need to specify how to reconcile divergent branches.
  • 解决

    1
    2
    3
    4
    5
    6
    # 默认将pull下来的代码与现有改动的代码进行合并
    git config --global pull.rebase false

    # 回退到合并之前的代码,在进行pull拉取最新代码
    git reset --hard c129513
    git pull origin

pull rebase 模式

  1. 命令 git pull --rebase <远程主机名> <远程分支名>:<本地分支名>

  2. 多人使用同一个远程分支合作开发,在 push 代码的时候很可能出现以下问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ git push origin master

    # 结果如下
    To github.com:hello/demo.git
    ! [rejected] master -> master (fetch first)
    error: failed to push some refs to 'git@github.com:hello/demo.git'
    hint: Updates were rejected because the remote contains work that you do
    hint: not have locally. This is usually caused by another repository pushing
    hint: to the same ref. You may want to first integrate the remote changes
    hint: (e.g., 'git pull ...') before pushing again.
    hint: See the 'Note about fast-forwards' in 'git push --help' for details.
  3. 从输出结果中可以明显看出此时远程分支有新的 commit 未同步到本地,无法推送。此时我们一般会执行以下操作:

    1
    2
    $ git pull origin master
    $ git push origin master
  4. 确实 push 成功了,但是此时用 git log 查看提交记录:多出了一条 merge commit,这个 commit 就是在执行 git pull origin master 的时候自动生成的。如果多人多次如此操作,那么提交记录就会出现很多条这种自动生成的 merge commit,非常难看。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 结果如下
    commit 1aefef1a2bedbd3ebd82db8dcf802011a35a9888 (HEAD -> master, origin/master, origin/HEAD)
    Merge: 24cfa5c f318a05
    Author: hello <hello@qq.com>
    Date: Tue Jul 23 09:53:47 2019 +0800

    Merge branch 'master' of github.com:hello/demo

    commit 24cfa5c3ad271e85ff0e64793bf2bcc9d700c233
    Author: hello <hello@qq.com>
    Date: Tue Jul 23 09:50:06 2019 +0800

    feat: 新功能提交

    commit f318a05b1a4cbc0a6cf8d7dc7d3fb99cbafb0363
    Author: world <world@qq.com>
    Date: Tue Jul 23 09:48:20 2019 +0800

    feat: 其他功能提交
  5. 解决方案:要解决以上问题,不再出现自动生成的 merge commit,那么只要在执行 git pull origin master 的时候带上 --rebase 即可:

    1
    2
    # 等价于:git fetch --all && git rebase branch
    $ git push origin master
  6. 注意事项: 执行 git pull --rebase 的时候必须保持本地目录干净。即:不能存在状态为 modified 的文件。(存在Untracked files是没关系的),如果出现冲突,可以选择手动解决冲突后继续 rebase,也可以放弃本次 rebase

  7. 如果 A、B 同学修改了同一个文件。那么很有可能会出现冲突,当 B 同学来执行命令的时候会出现如下状况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    $ git pull --rebase

    # 结果如下
    remote: Counting objects: 6, done.
    remote: Compressing objects: 100% (6/6), done.
    remote: Total 6 (delta 1), reused 0 (delta 0)
    Unpacking objects: 100% (6/6), done.
    From gitlab.lrts.me:fed/gitlab-merge
    93a1a93..960b5fc master -> origin/master
    First, rewinding head to replay your work on top of it...
    Applying: feat: 其他功能提交
    Using index info to reconstruct a base tree...
    M one.md
    Falling back to patching base and 3-way merge...
    Auto-merging one.md
    CONFLICT (content): Merge conflict in one.md
    error: Failed to merge in the changes.
    Patch failed at 0001 feat:其他功能提交
    hint: Use 'git am --show-current-patch' to see the failed patch

    Resolve all conflicts manually, mark them as resolved with
    "git add/rm <conflicted_files>", then run "git rebase --continue".
    You can instead skip this commit: run "git rebase --skip".
    To abort and get back to the state before "git rebase", run "git rebase --abort".

    # 这种情况下,可以手动打开 one.md 文件解决冲突,然后再执行:
    $ git add one.md
    $ git rebase --continue

    # 也可以用 git rebase --abort 放弃本次 rebase 操作。
  8. 总结:多人基于同一个远程分支开发的时候,如果想要顺利 push 又不自动生成 merge commit,建议在每次提交都按照如下顺序操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 把本地发生改动的文件贮藏一下
    $ git stash

    # 把远程最新的 commit 以变基的方式同步到本地
    $ git pull --rebase

    # 把本地的 commit 推送到远程
    $ git push

    # 把本地贮藏的文件弹出,继续修改
    $ git stash pop

SSH 密钥

SSH是一种网络协议,用于计算机之间的加密登录。而大多数 Git 服务器都会选择使用 SSH 公钥来进行授权,所以想要在 GitHub 提交代码的第一步就是要先添加 SSH key 配置。

生成SSH key

Linux 与 Mac 都是默认安装了 SSH ,而 Windows 系统安装了 Git Bash 应该也是带了 SSH的。大家可以在终端(win下在 Git Bash 里)输入 ssh 如果出现提示证明你本机已经安装 SSH, 否则请搜索自行安装下。

紧接着输入 ssh-keygen -t rsa ,什么意思呢?就是指定 rsa 算法生成密钥,接着连续三个回车键(不需要输入密码),然后就会生成两个文件 id_rsa 和 id_rsa.pub ,而id_rsa 是密钥id_rsa.pub 就是公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[sun@CMCC#1 ~]$ ssh-keygen -t rsa -b 4096 -C "your_mail@gmail.com"
Generating public/private rsa key pair.

Enter file in which to save the key (/Users/sun/.ssh/id_rsa): Created directory '/Users/sun/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/sun/.ssh/id_rsa.
Your public key has been saved in /Users/sun/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:BQZcuTpPMQ2L/xYPFQoF8jhKsWrSZU4S3iIU7QiUxyU fl_6145@163.com
The key's randomart image is:
+---[RSA 4096]----+
|.+*Eoooo=+. |
|oo *.o.=+. . |
|o B B o..*.. . |
| + @ ...=.o . |
|. + o oSo . |
| o o o o |
| + . + |
| . o . |
| . |
+----[SHA256]-----+
[sun@CMCC#1 ~]$

这两文件默认分别在如下目录里生成: Linux/Mac 系统 在 ~/.ssh 下,win系统在 /c/Documents and Settings/username/.ssh 下,都是隐藏文件,相信你们有办法查看的。

接下来要做的是把 id_rsa.pub 的内容添加到 GitHub 上,这样你本地的 id_rsa 密钥跟 GitHub上的 id_rsa.pub 公钥进行配对,授权成功才可以提交代码。

ed25519 算法

如果有多个平台,可以生产多份 ssh密钥, RSA 经典且可靠,但性能不够理想。 OpenSSH 版本大于 6.5(2014 年的古早版本),就可以利用 Ed25519 算法生成的密钥对,减少你的登录时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ssh-keygen -t ed25519
ssh-keygen -t ed25519 -C "fl_6145@163.com"
# 生成目录
mkdir -p ~/.ssh && cd ~/.ssh
# 我在 GitHub
ssh-keygen -t ed25519 -f my_github_ed25519 -C "me@github"
# 我在 Gitee
ssh-keygen -t ed25519 -f my_gitee_ed25519 -C "me@gitee"
# 我在 GitLab
ssh-keygen -t ed25519 -f my_gitlab_ed25519 -C "me@gitlab"
# 我在企业
ssh-keygen -t ed25519 -f my_company_ed25519 -C "email@example.com"

# RSA
ssh-keygen -t rsa -b 4096 -f my_id -C "email@example.com"
# [-t rsa] 表示使用 RSA 算法。
# [-b 4096] 表示 RSA 密钥长度 4096 bits (默认 2048 bits)。Ed25519 算法不需要指定。
# [-f my_id] 表示在【当前工作目录】下生成一个私钥文件 my_id (同时也会生成一个公钥文件 my_id.pub)。
# [-C "email@example.com"] 表示在公钥文件中添加注释,即为这个公钥“起个别名”(不是 id,可以更改)。

多个ssh秘钥

ssh-add 这个命令不是用来永久性的记住你所使用的私钥的。实际上,它的作用只是把你指定的私钥添加到 ssh-agent 所管理的一个session 当中。而 ssh-agent 是一个用于存储私钥的临时性的 session 服务,也就是说当你重启之后,ssh-agent服务也就重置了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 把专用密钥添加到 ssh-agent 的高速缓存中
ssh-add ~/.ssh/id_dsa
# 从ssh-agent中删除密钥:
ssh-add -d ~/.ssh/id_xxx.pub
# 查看ssh-agent中的密钥:
ssh-add -l


# ~/.ssh/config中配置
# 关于别名
# Host 是别名,HostName 是真正的域名。
# 得益于别名,你可以直接以别名访问地址。例如:
# 无别名: git clone git@github.com:torvalds/linux.git
# 有别名: git clone github:torvalds/linux.git
# 本例中使用与域名一致的别名,以免错误的配置导致登录不上。

# 关于代理
# SOCKS 代理格式: ProxyCommand connect -S localhost:1080 %h %p
# HTTP 代理格式: ProxyCommand connect -H localhost:1080 %h %p
## SSH 代理依赖外部程序,这里使用了 Git for Windows 同捆的 connect.exe。
## Linux 下使用该代理方式需要额外安装 connect-proxy。


# 我在 GitHub
Host github.com
Hostname github.com
# ProxyCommand connect -H localhost:1080 %h %p
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/my_github_ed25519

# 我在 GitLab
Host gitlab.com
Hostname gitlab.com
# ProxyCommand connect -H localhost:1080 %h %p
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/my_gitlab_ed25519

# 我在 Gitee
Host gitee.com
Hostname gitee.com
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/my_gitee_ed25519

# 我在企业
Host example.com
Hostname example.com
Port 22
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/my_company_ed25519
  • ssh config 就是一个配置文件,描述不同的秘钥对应的设置——包括主机名、用户名、访问策略等等,本地配置内容放在文件/Users/you/.ssh/config, Host 是别名,HostName 是真正的域名。Hostname 是域名则最好保持一致。

    • 当我 git clone https://github.com/user/repo 的时候,~/.ssh/my_github_ed25519 秘钥会被使用

    • 当我 git clone https://gitlab.com/user/repo 的时候,很显然 ~/.ssh/my_gitlab_ed25519 秘钥会被使用。

  • 如果同一域名下有两个不同的配置怎么办?

    • 以 Github 为例,如果我有两个账户,一个个人的,一个组织的,并且要使用不同的秘钥,那么我可以这么写:这里 Host 后面对应的是 Github 的两个用户名 github.com/user1 github.com/user2

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      Host github.com-user1
      Hostname github.com
      User git
      PreferredAuthentications publickey
      IdentityFile ~/.ssh/id_rsa

      Host github.com-user2
      Hostname github.com
      User git
      PreferredAuthentications publickey
      IdentityFile ~/.ssh/my_github_ed25519
  • 如果域名是数字 IP,是否可以简化呢?Host 可以帮助你把对应的 IP 变成好记的名字。

1
2
3
4
5
6
7
# 比如说我在公司内部配置了 Git Server(基于 gitolite 或 Gitlab 或任何工具)
# 正常的访问地址是:git://xxx.xxx.xxx.xxx:repo.git,如下的配置则可以帮你把它简化成:git.work:repo.git
Host git.work
Hostname 192.168.61.11
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/work
1

添加 SSH key

先在 GitHub 上的设置页面,点击最左侧 SSH and GPG keys : 然后点击右上角的 New SSH key 按钮: 需要做的只是在 Key 那栏把id_rsa.pub公钥文件里的内容复制粘贴进去就可以了,Title 那栏不需要填写,点击 Add SSH key 按钮就ok了。

1
2
3
4
5
#  Linux/Mac 用户执行以下命令:
cd ~/.ssh
cat id_rsa.pub
# 将 SSH 密钥复制到剪贴板
pbcopy < ~/.ssh/id_rsa.pub

SSH key 添加成功之后,输入 ssh -T git@github.com 进行测试,如果出现以下提示证明添加成功了。

1
2
3
4
# 验证ssh key是否设置成功
# ssh -T git@gitee.com
ssh -T git@github.com
Hi fulsun! You've successfully authenticated, but GitHub does not provide shell access.

大文件清理

斩草除根式

  • 把根目录下面的.git文件夹删除,然后重建,简单粗暴,但如果需要git log历史记录的,请不要这样做

    1
    2
    3
    4
    rm -rf .git
    # 删除之后,该目录就不是一个git仓库了,因此需要重建
    # 输入rm -rf + github仓库地址 在 github 的对应的库中到 setting 删除库
    rm -rf https://github.com/xxx/xxx.git

逐个攻破式

  • 对仓库进行gc操作

    1
    git gc
  • 查看空间使用,size-pack 是以千字节为单位表示的 packfiles 的

    1
    2
    git count-objects -v
    du -ah .git/objects
  • 运行底层命令 git verify-pack 以识别大对象,对输出的第三列信息即文件大小进行排序.

    1
    2
    3
    4
    5
    6
    7
    8
    # 占用空间最多的五个文件
    git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5
    # 787d86fa5890fa037cca876b7b67d581d424b357 blob 1259085 1168273 16059429
    # 第一行的字母其实相当于文件的id,用以下命令可以找出id 对应的文件名:
    git rev-list --objects --all | grep 787d86fa5890fa037cca876b7b67d581d424b357

    # 使用 rev-list 命令,传入 --objects选项,它会列出所有 commit SHA 值,blob SHA 值及相应的文件路径,这样查看 blob 的文件名。
    git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5 | awk '{print$1}')"
  • 查找文件

    1
    2
    3
    4
    # 查出文件提交commit记录
    git log --pretty=oneline --branches -- ${FILE_PATH}
    # 想要知道这条commit id所在的分支,可以使用以下命令
    git branch -a --contains <COMMIT ID>
  • 删除文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 遍历所有提交: commit多了会比较慢
    git filter-branch -f --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch <file>' --tag-name-filter cat -- --all

    filter-branch命令可以用来重写Git仓库中的提交
    -f , --force 假如遇到冲突也让git强制执行
    --prune-empty 选项告诉git,如果因为重写导致某些commit变成了空(比如修改的文件全部被删除),那么忽略掉这个commit。
    --index-filter参数用来指定一条Bash命令,然后Git会检出(checkout)所有的提交, 执行该命令,然后重新提交。
    git rm --cached --ignore-unmatch file 让git删除掉缓存的文件,如果有匹配的话
    --tag-name-filter 表示对每一个tag如何重命名,重命名的命令紧跟在后面,当前的tag名会从标注输入送给后面的命令,用cat就表示保持tag名不变。
    -- 紧跟着的-- 表示分割符,
    --all 表示对所有的文件都考虑在内

    # 指定commit修改
    git filter-branch -f --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch <file>' -- <commit_id>
  • 回收内存

    1
    2
    3
    4
    5
    rm -rf .git/refs/original/
    git reflog expire --expire=now --all
    git fsck --full --unreachable
    git repack -A -d # 将所有未被包含在一个pack的松散对象连结成一个pack
    git gc --aggressive --prune=now
  • 提交到远程仓库

    1
    2
    3
    git push --force [remote] master
    # 让远程仓库变小
    git remote prune origin

脚本-更新目录的多个仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# vim ~/git_pull_Batch.sh
#!/bin/bash

function showGreen(){ echo -e "\033[32m $1 \033[0m"}
function showBlue(){ echo -e "\033[36m $1 \033[0m"}
function showYellow(){ echo -e "\033[33m $1 \033[0m"}
function showWhite(){ echo -e "\033[37m $1 \033[0m"}

function traversal_dir(){
for sub_dir in `ls $1` #通过 ls root_dir 遍历出子目录,装入子目录 sub_dir 中
do
dir=$1"/"$sub_dir #将根目录 $1 与子目录 sub_dir 拼接成完整的目录
if [ -d $dir ] #判断:是目录的继续下一步
then
cd $dir
showBlue $dir
showGreen 'git pull '$sub_dir
git pull
echo #打印空行
else
showYellow $dir
echo #打印空行
fi
done
}

root_dir="N:\Desktop\qnit" #定义根目录,即项目 project 的上级目录。例如:root_dir/project/.git
traversal_dir $root_dir

代码行数统计

1
2
3
4
5
6
7
# 个人提交的代码行数统计
git log --author="username" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -

# 查看项目每个人提交的代码行数统计
git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --author="$name" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -; done
# 查询所有用户的提交总次数
git log --pretty='%aN' | sort | uniq -c | sort -k1 -n -r

Git在工作中的使用

Git 工作流分类

  • GItFlow是在项目开发过程中使用 Git 的方式

  • 集中式工作流 :像 SVN 一样,集中式工作流以中央仓库作为项目所有修改的单点实体。所有修改都提交到Master 这个分支上。 这种方式与 SVN的主要区别就是开发人员有本地库。Git 很多特性并没有用到。

  • GitFlow工作流:为功能开发、发布准备和维护设立了独立的分支,让发布迭代过程更流畅。严格的分支模型也为大型项目提供了一些非常必要的结构

  • Forking 工作流:在 GitFlow 基础上,充分利用了 Git 的 Fork 和 pull request 的功能以达到代码审核的目的。更适合安全可靠地管理大团队的开发者,而且能接受不信任贡献者的提交。

分支种类详解

  • 主干分支 master: 主要负责管理正在运行的生产环境代码。永远保持与正在运行的生产环境完全一致。
  • 开发分支 develop: 主要负责管理正在开发过程中的代码。一般情况下应该是最新的代码。
  • bug 修理分支 hotfix: 主要负责管理生产环境下出现的紧急修复的代码。 从主干分支分出,修理完毕并测试上线后,并回主干分支。并回后,视情况可以删除该分支。
  • 准生产分支(预发布分支) release: 较大的版本上线前,会从开发分支中分出准生产分支,进行最后阶段的集成测试。该版本上线后,会合并到主干分支。生产环境运行一段阶段较稳定后 可以视情况删除。
  • 功能分支 feature: 为了不影响较短周期的开发工作,一般把中长期开发模块,会从开发分支 中独立出来。 开发完成后会合并到开发分支。

Github跨团队协作

  1. 假设zs团队在开发,要请ls团队协作。
  2. ls登录Github账户,forkzs的项目后
  3. 修改本地代码后,推送到远程仓库中,访问Github后,点击Pull Request,新建一个pull请求。
  4. 这时候zs登录github后可以在pull request 中看到请求, 可以进行交流和审核提交的代码。
  5. 确认无误后,合并代码,选择merge pull request,填写操作的日志信息后, confirm merge即可。
  6. zs团队后续继续开发,ls要获取代码,同样取zs项目和自己项目的diff,创建pull request ,然后merge即可。

普通开发人员的操作

普通开发人员,一般按照如下几个步骤来进行开发、测试工作就可以了:

  1. 将远程 dev 分支 clone 到本地,例如:git clone git@github.com:goto456/test.git
  2. 从 dev 分支拉出(新建)自己的 feature 分支用于开发,例如:git checkout -b feature_login
  3. 在自己的 feature 分支上进行开发工作;
  4. 开发完了用 add、commit 等操作提交到当前分支;
  5. 如果需要在测试环境进行测试,则将远程 test 分支拉到本地,例如:git branch test origin/test;
  6. 将自己的 feature 分支合并到 test 分支,并将 test 分支 push 到远程,例如:git rebase test, git checkout test, git merge feature_login, git push origin test(注意:我们推荐用 rebase 来合并,以保证分支的整洁、美观)
  7. 通过公司的发布平台将远程 test 分支发布到测试环境进行测试;
  8. 如果测试没问题或者开始就不需要测试,这可以直接将当前 feature 分支合并到 dev 分支,并 push 到远程库,例如:git rebase dev, git checkout dev, git merge feature_login, git push origin dev(注意:我们推荐用 rebase 来合并,以保证分支的整洁、美观)
  9. 这时表示该功能已经开发完成了,代码的 review 以及发布,需要团队 leader 在合并到 master 操作时进行;这时可以删除了自己的 feature 分支,例如:git branch -d feature_login
  10. 如果在 push 到远程的时候提示需要先 pull 时,我们推荐使用 rebase 的方式:git pull --rebase 以保持分支的整洁、美观。

团队 leader 的操作

因为只有 leader 有操作 master 分支的权限,所以需要完成 dev 分支到 master 分支的合并,以及后续打 tag 和正式上线发布的工作:

  1. 先切换到 dev 分支,并拉取最新的状态,例如:git checkout dev, git pull --rebase origin dev
  2. 进行代码 review 等过程后,合并到 master 分支,例如:git rebase master, git checkout master, git merge dev;(注意:我们推荐用 rebase 来合并,以保证分支的整洁、美观)
  3. 为本次完成的版本打上标签,例如:git tag v1.0 -m "release version 1.0"
  4. 将本地合并后的 master 分支以及标签 push 到远程库,例如:git push orgin master --tags

结束语

以上就是我从自己平时的应用中整理出的一个比较简洁的教程,以及我们团队在实际工作中是如何使用的。希望对大家有所帮助!