git功能用法-全功能

工作就是玩

编写代码,或编辑文档,其实和玩游戏差不多。在你做出了很多进展之后,你最好保存一下。 要做到这点,点击编辑器的保存按钮就好了。

但这将覆盖老版本。就像那些学校里玩的老游戏,只有一个存档:你确实可以保存,但你不能回到更老的状态。这真让人扫兴,因为那个状态可能恰好保存了这个游戏特别有意思一关,说不定哪天你想再玩一下呢。或者更糟糕的事情,你当前的保存是个必败局, 这样你就不得不从头开始玩了。

版本控制

在编辑文档的时候,如果想保留旧版本,你可以将文件“另存为”一个不同的文件,或在保存之前将文件拷贝到别处。你可能会压缩这些文件以节省空间。这是一个初级的依赖手工进行的版本控制方式。游戏软件在这块早就做了很多提高,很多游戏都提供基于时间戳的多个存档。

让我们看看稍稍复杂的情况。比如你有很多放在一起的文件,比如项目源码或网站文件。现在如你想保留旧版本,你不得不把整个目录存档。手工保存多个版本很不方便, 而且很快会耗费巨大的存储空间。

在一些电脑游戏里,一个存档包含在一个充满文件的目录里。这些游戏为玩家屏蔽了一些细节,并提供一个方便易用的界面来管理该目录的不同版本。

版本控制系统也没有什么不同。提供友好的用户界面,来管理目录里的东西。你可以频繁保存,也可以在之后加载任一存档。不像大多数计算机游戏,版本控制系统通常精于节省存储空间。一般情况下,如果两个版本间只有少数文件的变更,每个文件的变更也不大,那就只存储差异的部分,而不是把全部拷贝都保存下来,以达到节省存储空间的目的。

分布控制

现在设想一个很难的游戏。太难打了,以至于世界各地很多骨灰级玩家决定组队,分享他们的游戏存档以攻克它。在同一个游戏里,玩家们分别攻克不同的等级,协同工作以创造惊人战绩。

你如何搭建一个系统,使得他们易于得到彼此的存档?并易于上传新的存档?

在过去,每个项目都使用中心式版本控制(如SVN)。在某个服务器上存放所有保存的游戏记录。其他人就不用再做备份了。每个玩家在他们的机器上最多保留几个游戏记录。当一个玩家想更新至最新进度的时候,他们需要把这个进度从主服务器下载下来,玩一会儿,保存并上传到主服务器以供其他人使用。

假如一个玩家由于某种原因,想得到一个较旧版本的游戏进度,该怎么办?或许当前保存的游戏是一个注定的败局,因为某人在第三级忘记捡某个物品;他们希望能找到最近一个可以完成的游戏记录。或者他们想比较两个旧版本间的差异,来估算某个特定玩家干了多少活。

查看旧版本的理由有很多,但检查的办法都是一样的。他们必须去中心服务器检索那个旧版本的记录。需要的旧版本越多,和服务器的交互就越多。

Git是新一代的版本控制系统中的一员,它的特点是分布式的,广义上也可以被看作是一 种中心式系统。从主服务器下载时,玩家会得到所有保存的记录,而不仅是最新版。这么看来,玩家们好像把中心服务器做了个镜像。最初的克隆操作可能比较费时,特别当存档有很长历史的时候,但从长远看这是值得的。一个显而易见的好处是,当查看一个旧版本时,就不再需要和中心服务器通讯了。

中心式版本控制:我就是仓库,我拥有全部

我们首先要明确一个git与先前的版本管理工具(主要是SVN)的不同。下面是使用SVN版本管理工具时,程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式:

众所周知,SVN是基于中心版本仓库进行版本管理协作的版本管理工具。就像上图中那样,所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录;而进行版本管理操作或者与他人进行协作的前提也是:中心版本仓库必须始终可用。这有点像以太网的“半双工的集线器(hub)模式”:SVN中心仓库就像集线器本身,每个程序员节点就像连接到集线器上的主机;当一个程序员提交(commit)代码到中心仓库时,其他程序员不能提交,否则会出现冲突;如果中心仓库挂掉了,那么整个版本管理过程也将停止,程序员节点间无法进行协作,这就像集线器(hub)挂掉后,所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。

分布式版本管理

git实现了分布式版本管理系统,每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个分布式git版本管理网络。这样的一个分布式网络存在着与普通分布式系统的类似的问题:如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信),那么这个分布式网络还缺少一个“控制平面”

github恰恰给出了一份git分布式网络控制平面的实现:托管、发现、控制…。其名称中含有的“hub”字样让我们想起了上面的“hub模式”:

我们看到在github的git协作模式实践中,引入了“中心仓库”的概念,各个程序员的节点git仓库源于(clone于)中心仓库。但是它和SVN的“中心仓库”有着本质的不同,这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”,也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。

一个误区

一个很常见的错误观念是,分布式系统不需要中心仓库。这与事实并不相符。克隆主仓库并不会降低它的重要性。

一般来说,一个中心版本控制系统能做的任何事,一个良好设计的分布式系统都能做得更好。网络资源总要比本地资源耗费更昂贵。

一个小项目或许只需要分布式系统提供的一小部分功能,但是,在项目很小的时候,就理应使用规划并不好的系统?就好比说,在计算较小数目的时候应该使用罗马数字?

而且,你的项目的增长可能会超出你最初的预期。从一开始就使用Git好似带着一把瑞士军刀,尽管你很多时候只是用它来开开瓶盖。某天你迫切需要一把改锥,你就会庆幸你所有的不单单是一个启瓶器。

Git 工作流程

  • 在工作目录中新建、修改、删除文件;
  • 将需要进行版本管理的文件放入暂存区域;
  • 将暂存区域的文件commit到Git本地仓库;
  • 将Git本地仓库文件push到GIt的远程仓库;
git管理的文件有三种状态:modified(已修改),staged(已暂存),committed(已提交)

Git文件的四种状态

  • Untracked: 未跟踪, 此文件在文件夹中, 但并没有加入到git仓库, 不参与版本控制. 通过 git add filename 状态变为Staged.
  • Unmodify: 文件已经入库, 未修改, 即版本库中的文件快照内容与文件夹中完全一致. 这种类型的文件有两种去处:
      • 如果它被修改, 而变为Modified.
      • 如果使用 git rm filename, 删除text1.txt文件,并把它从git的仓库管理系统中移除(
      • Modified: 文件已修改, 仅仅是修改, 并没有进行其他的操作. 这个文件也有两个去处:
        • 通过 git add filename可进入暂存staged状态,
        • 使用 git checkout filename 则丢弃修改, 返回到 unmodify 状态, 这个 git checkout filename 即从本地仓库中取出文件, 覆盖当前修改 !
      • Staged: 暂存状态. 执行 git commit 则将修改同步到库中, 这时库中的文件和本地文件又变为一致, 文件为Unmodify状态. 执行 git reset HEAD filename 取消暂存, 文件状态为 Modified;

Git 中 ~和^的区别

      • ~ 的作用是查找 祖先提交, 例如:想要 HEAD 的第10 个祖先提交,直接使用 HEAD~10 就可以了。<rev>~<n> 用来表示一个提交的第 n 个祖先提交,如果不指定 n,那么默认为 1。 另外,HEAD~~~HEAD~3 是等价的。
      • ^ 的作用是查找 父提交,<rev>^<n> 用来表示一个提交的第 n 个父提交,如果不指定 n,那么默认为 1。 和~ 不同的是,HEAD^^^ 并不等价于 HEAD^3,而是等价与 HEAD^1^1^1。 。因为,很多情况下一个提交并不是只有一个父提交。 就如图表示,7a3fb3d (C) 就有两个父提交:07b920c (B)71bd2cf (A)
      • 具体区别

查看命令详情

$ git ${command} --help

创建仓库

$ git init
$ git add .
$ git commit -m "Initial commit"

克隆仓库

$ git clone <repo> <directory>

<directory>省略,说明克隆仓库到当前所在文件夹;

查看Git文件状态

$ git status

添加文件到暂存区

# add change form all tracked and untracked files
$ git add -all
$ git add -A
$ git add .

# add specified folder
$ git add filename

提交变更到本地仓库

$ git commit -m ${changeLog}

# vim 编辑提交记录(i、esc、wq)
$ git commit 

查看commit信息

# 查看当前的提交信息
$ git show

$ git show ${commitId}
$ git show ${tag}

merge

从目标 branch 和当前 branch (即 HEAD 所指向的 commit)分叉的位置起,把目标 branch 的路径上的所有 commit 的内容一并应用到当前 branch,然后自动生成一个新的 commit

merge原理

例如:提交记录如下,当前分支为master

执行: git merge topic 之后,提交计入如下:

      1. 查找查找 特性分支当前分支
      2. 对比 特性分支topic 相对于 共同祖先E 的历次提交,提取相应的变更作为 临时文件H;
      3. 临时文件H 指向当前分支master的 G提交记录 master指针 指向 临时提交E
      1. topic 分支 不受影响
$ git merge ${featureBranch}
$ git merge (--continue | --abort | --quit)

merge 冲突解决

这里的线条要么与共同
祖先没有变化,要么因为只有一侧发生了变化而被彻底解决。
<<<<<<< yours:sample.txt
冲突解决很难;
我们去买东西吧。
======== 
Git 使冲突解决变得容易。
>>>>>> theirs:sample.txt
这是另一行已完全解析或未修改的行。

其中一对相互矛盾的变化发生的区域标有标记 之前的部分通常是当前分支一方,之后的部分通常是特性分支一方。

如何解决merge冲突

看到冲突后,你可以做两件事:

      • 决定不合并:git merge --abort 可以用于此。
      • 解决冲突:
        1. 解决冲突文件;
        2. 将冲突文件添加到索引区,git add filename;
        1. 使用 git commit 执行最后的收尾工作;

rebase

的意思是,给你的 序列重新设置基础点(也就是父 )。展开来说就是,把你指定的 以及它所在的 串,以指定的目标 为基础,依次重新提交一次

# branchName 若没有指定,默认为当前分支
$ git rebase ${upstream} ${branchName} 

rebase 原理

例如: 当前提交记录如下,当前分支为topic:

执行

# 如下两条指令等价
$ git rebase -i master 
$ git rebase -i master topic

之后,分支记录如下:

      1. 因为 branchName 指定为 topic,所以git rebase首先自动执行 git switch topic,HEAD切换到topic;
      2. 查找topic和master的 共同祖先E;
      1. 提取 topic 相对 E 的历次提交记录 A、B、C ,并保存为临时文件
      2. 最终将topic 指针指向 C`;

 

rebase解决冲突

这里的线条要么与共同
祖先没有变化,要么因为只有一侧发生了变化而被彻底解决。
<<<<<<< theirs:sample.txt
冲突解决很难;
我们去买东西吧。
======== 
Git 使冲突解决变得容易。
>>>>>> yours:sample.txt
这是另一行已完全解析或未修改的行。

其中一对相互矛盾的变化发生的区域标有标记 之前的部分通常是upstream分支一方,之后的部分通常是当前分支一方。rebase 之前,自动执行了git switch ${upstream}

如何解决rebase冲突

 

看到冲突后,你可以做两件事:

      • 决定不在继续rebase:git rebase --abort 可以用于此。
      • 解决冲突:
        1. 解决冲突文件;
        2. 将冲突文件添加到索引区,git add filename;
        1. git rebase --continue
        2. 还没好,淡定,重复前面步骤,直到返回正常分支。

rebase的黄金法则

只能在自己的私有分支上使用,绝对不能在公共的分支上使用。

branch

# 创建分支
$ git branch ${branchName}

# 切换分支
$ git checkout ${branchName}

# 创建并切换分支
$ git checkout -b ${branchName}

# 列出所有本地分支
$ git branch
# 列出所有远程分支
$ git branch -r
# 列出所有本地分支和远程分支
$ git branch -a

# 删除分支
$ git branch -d ${branchName}
$ git branch -D ${branchName}

# 查看各个本地分支的最后一次提交
$ git branch -v

# 查看本地分支与远程分支的关联情况
$ git branch -vv

# 重命名分支
$ git branch -m|-M ${oldBranch} ${newBranch}

# 为 local_branch_name 设置远程关联信息
$ git branch --set-upstream-to=<upstream> <local_branch_name>

# 取消 local_branch_name 的远程关联信息
$ git branch --unset-upstream [<local_branch_name>]

# 提交所属分支
$ git branch  --contains commitId          # 本地分支
$ git branch -r --contains commitId     # 远程分支
$ git branch -a --contains commitId     # 本地和远程分支

stash

# 暂存 untracked 和 modified 的文件
$ git stash push  -u  -m <message>

# 查看暂存列表
$ git stash list

# 显示具体的改动信息,num 默认为0
$ git stash show stash@{num}

# 应用暂存记录,但不会删除已应用的暂存记录,num 默认为0
$ git stash apply stash@{num}

# 应用暂存记录,同时删除已应用的暂存记录,num 默认为0
$ git stash pop stash@{num}

# 从暂存列表中,删除指定的暂存记录,num 默认为0
$ git stash drop stash@{num}

# 清空暂存列表
$ git stash clear

上传本地分支(push)

# 上传本地分支
$ git push <远程主机名> <本地分支名>:<远程分支名>

# 从远程分支删除与本地分支同名的远程分支
$ git push -d origin master  # push方式1
$ git push --delete origin master # push方式1

# 等同于 push方式1、2,等同于推送了一个空的本地分支到指定的远程分支
$ git push origin <省略本地分支名>:master

# 将本地仓库上传到远程
$ git remote add origin ${ssh@url}
$ git push -u origin master

如果 省略 远程分支名,则表示将本地分支推送到与之存在 追踪关系的 远程分支,如果对应的远程分支不存在,则对应的远程分支被创建。

如:

git push origin master

表示将本地的master分支推送到origin主机的master分支,如果origin主机的master分支不存在,则被创建;

获取远程分支(pull)

获取远程分支资源,再与指定的本地分支合并

# 本地分支名可省略,如果省略本地分支表示的是当前的本地分支
$ git pull <远程主机名> <远程分支名>
$ git pull <远程主机> <远程分支名>:<本地分支名>
# 等价与
$ git fetch <远程主机名> <远程分支名>
$ git merge ${<远程主机名>/<远程分支名>} <本地分支名>
 
# 拉取所有的远程分支
$ git fetch <remote_origin>

# 拉取指定的远程分支
$ git fetch <远程主机名> <远程分支名>

checkout

的本质,其实是把 指向指定的 ,然后签出这个 所对应的 的工作目录。所以同样的, 的目标也可以不是 ,而直接指定某个

git checkout -b <new-branch> [<start_point>]

另外,如果你留心的话可能会发现,在 的提示语中,Git 会告诉你可以用 的格式,通过「签出」的方式来撤销指定文件的修改:

reset

众所周知 这个指令虽然可以用来撤销 ,但它的实质行为并不是撤销,而是移动 ,并且「捎带」上 所指向的 (如果有的话);也就是说,它是用来重置 以及它所指向的 的位置的。

后面总是跟着的那个 是什么意思呢?

指令可以重置 的位置,不过在重置它们的同时,对工作目录可以选择不同的操作,而对工作目录的操作的不同,就是通过 后面跟的参数来确定的。

 

      1. 工作区有代码(未执行add)
      2. 暂存区有代码(未执行commit)

reset --hard:重置工作目录

撤销commit, 撤销add, 删除工作区改动;

这个参数慎用,会直接恢复到某次的 commit 状态,同时删除工作区和暂存区的代码。

reset --soft:保留工作目录

撤销 commit,不撤销 add,还原工作区改动代码。

会在重置 时,保留工作目录和暂存区中的内容,并把重置 所带来的新的差异放进暂存区。

由于 移动到了 ,而且在 的过程中工作目录的内容没有被清理掉,所以 中的改动在 后就也成了工作目录新增的「工作目录和 的差异」。这就是上面一段中所说的「重置 所带来的差异」。

reset --mixed 不加参数:保留工作目录,并清空暂存区

撤销 commit,撤销 add,还原工作区改动代码。

如果不加参数,那么默认使用 参数。它的行为是:保留工作目录,并且清空暂存区。也就是说,工作目录的修改、暂存区的内容以及由 所导致的新的文件差异,都会被放进工作目录。简而言之,就是「把所有差异都混合(mixed)放在工作目录中」。

revert

撤销之前的一个指定的提交这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 :一个原始 ,一个对它的反转

git revert -n master~5..master~2

将提交所做的更改从 master 中的倒数第五次提交(包括)恢复到 master 中的倒数第三次提交(包括),但不要使用恢复的更改创建任何提交。

diff

# 比对 **工作目录和暂存区** 的不同  
$ git diff (不加参数)
$  git diff <file>

# 比对 **暂存区和最后一次提交(HEAD)** 之间的不同
$ git diff --staged 
$ git diff --cached
$ git diff --cached <file>

# 比对 **工作目录和最后一次提交(HEAD)** 之间的不同
$ git diff HEAD
$ git diff HEAD <file>

cherry-pick

功能: 将指定的提交应用于其它分支

git cherry-pick -x ([commitId] | [first-commitId]..[last-commitId])
# 其中,[first-commitId]..[last-commitId] 是前开后闭,
# 即不包括first-commitId,但包括last-commitId

# -x cherry picked from commit

上面命令就会将指定的提交commitId,应用于当前分支。这会在当前分支产生一个新的提交,当然它们的哈希值会不一样

git cherry-pick 命令的参数,不一定是提交的哈希值,分支名也是可以的,表示转移该分支的最新提交

a - b - c - d   Master
         \
           e - f - g - i - j Feature

现在将提交f应用到master分支

# 切换到 master 分支
$ git checkout master

# 将commit f 转移到master分支,则运行
$ git cherry-pick f

# 操作之后的代码库样式

a - b - c - d - f   Master
         \
           e - f - g Feature

# 可以看到,master分支的末尾增加了一个提交f;

# 将commit f、commit  g 转移到master分支,则运行
$ git cherry-pick f g

# 将commit f 到 commit i 转移到master分支,则运行
$ git cherry-pick e..i

git rm

touch txt.txt

# 将txt.txt 文件添加到索引库
git add txt.txt

# 将 txt.txt 文件从索引库中移除,但是对txt.txt文件本身没有进行任何操作
git rm --cached txt.txt

远程分支已经被删除,删除本地同名的远程分支

# 远程分支已经被删除,删除本地同名的远程分支
$ git fetch -p 

恢复提交信息

刚刚提交的代码,发现写错了怎么办?

此方式用于解决:刚刚提交的代码,发现写错了,但还没有push到代码仓库;

$ git commit --amend 

在提交时,如果加上 参数,Git 不会在当前 上增加 ,而是会把当前 里的内容和暂存区(stageing area)里的内容合并起来后创建一个新的 用这个新的 commit 把当前 commit 替换掉

写错的不是最新的提交,而是倒数第n个?

此方式用于解决:提交的代码不是最新的,而是倒数第n个,发现写错了,但还没有push到代码仓库;

$ git rebase -i head~n 

比错还错,想直接丢弃刚写的提交?

此方式用于解决:刚刚提交的代码是最新的,但是不想要了,但还没有push到代码仓库;

有的时候,刚写完的 写得实在太烂,连自己的都看不下去,与其修改它还不如丢掉重写。这种情况,就可以用 来丢弃最新的提交。

git reset --hard head~

代码已经push上去了,才发现写错了?

分两种情况进行处理

      1. 出错的内容在自己的独立开发分支,和他人无关。可以使用本地内容强行覆盖中央仓库的内容,修改的内容是刻意的、可预料的;
$ git push -f origin <branch_name>
      1. 出错的内容在公共分支,使用 revert 操作;
$ git revert <commit_id>

完成之后,把新的 上去,这个 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 :一个原始 ,一个对它的反转 修改了gitignore文件,刷新Git暂存区

$ git rm -rf --cached .
$ git add .
$ git commit -m "update .gitignore"
$ git push origin master

branch 删过了才想起来有用?

用完就删是好习惯,但有的时候,不小心手残删了一个还有用的 ,或者把一个 删掉了才想起来它还有用,怎么办?

$ git reflog ${branch_name}

是 "reference log" 的缩写,使用它可以查看 Git 仓库中的引用的移动记录。如果不指定引用,它会显示 的移动记录。假如你误删了 这个 ,那么你可以查看一下 的移动历史:

git reflog

从图中可以看出, 的最后一次移动行为是「从 移动到 」。而在这之后, 就被删除了。所以它之前的那个 就是 被删除之前的位置了,也就是第二行的

所以现在就可以切换回 ,然后重新创建

$ git checkout c03ec77
$ git checkout -b dev4

这样,你刚删除的 就找回来了。

不确定那个分支有自己提交的commit

Git 提供了一种能够直接通过 commit-id 查找出包含该内容分支的命令。

$ git branch --contains <commit-id>
$ git branch --contains 700920

查看其他引用的 reflog

默认查看 的移动历史,除此之外,也可以手动加上名称来查看其他引用的移动历史,例如查看

git reflog main

 

posted @ 2022-02-22 21:28  扰扰  阅读(203)  评论(0编辑  收藏  举报