Git不完全使用指南

基本概念

四度空间:

工作区:Workspace

 就是你在电脑里能看到的目录

暂存区: Index/Stage/Cached

英文叫stage, 或index。一般存放在 “.git目录下” 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)

版本库:Repository

工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。

远程仓库: Remote Repository

远程仓库则被设置在一个能被所有团队成员访问到的远程服务器上,基本上都是在互联网上或者是本地局域网中

五种状态:

未修改(Unmodify):

记录在工作区,没有被修改的文件

未追踪(Untracked):

新增的文件的状态,未受Git管理,记录在工作区

已修改(Modified):

受Git管理过的文件的改动状态(包括改动内容、删除文件),记录在工作区

已暂存(Staged):

将记录在工作区的文件变动状态通知了Git,记录在暂存区

已提交(Commited):

已提交到本地仓库

已推送(pushed):

已推送到远程仓库

一张图概括:

新建一个仓库

git init #在当前目录新建一个git仓库

git clone #clone一个远程的仓库到当前目录

当你创建一个带有.git文件夹的目录后,这个目录中的所有操作都将会被git记录,这就故事开始的地方,让我们开启这个游戏!

加入暂存区

这个目录就是被git记录的工作区,在这个目录下的所有增删改都会被git纳入到版本控制.

可以使用git status查看当前目录的哪些文件的变化:

git status #查看当前工作区文件状态

git status -s #简短形式输出当前工作状态

修改完后使用git add 加入到缓存区.

git add . #将当前目录所有文件做修改都加入缓存区
git add <file name> #将某个文件的修改就如缓存区

如果有操作想被git忽略有3钟种方式:

  • .gitignore文件

.gitignore只会忽略尚未加入版本控制的文件,如果文件已被加入版本控制,写入.gitignore文件git仍会记录文件的变化.如果已经纳入版本控制的文件想忽略,例如:在远程服务器中的配置文件,想本地修改,又不想被git记录可以使用下面两种命令

  • git update-index --assume-unchanged
git update-index --assume-unchanged <file name> # 这个会关闭文件与远程仓库的跟踪,认为这个文件远程仓库是不会修改,所以每次pull都是本地的文件

反向操作

git update-index --no-assume-unchanged <file name>
  • git update-index --skip-worktree
git update-index --skip-worktree <file name> # 这个不会关闭文件与远程仓库的跟踪,只是告诉Git不要跟踪对本地文件/文件夹的更改,每次pull时会拉取最新的变化会提示冲突,但因为没有跟踪本地更改,所以需要no-skip-worktree再合并最新的变化 或者使用git stash将本地的更改保存起来,再git pull,然后再git stash pop,再合并冲突,比较麻烦

反向操作:

git update-index --no-skip-worktree <file name>

提交

改写提交

如果提交完发现自己漏提了文件,或者提交信息写错了可以使用--amend来改写提交

git commit --amend #如果有漏提的文件,add到缓存区即可,然后git会打开默认的编辑器让你重新编写提交信息

复制提交

cherry-pick可以将其它提交复制到当前再提交一遍

执行:

git cherry-pick c2 c4

恢复

文件已被修改尚未加入到缓存区

git restore <file name> #将文件恢复到和缓存区(index)一样的状态

文件被修改并提交到缓存区

git restore --staged <file name> #将文件从缓存区(index)移动出来    

新增的文件删除(untracked状态)

git clean -f <file name> #将untracked状态的文件删除

Git指针

指针是git的精髓,理解的指针的移动对于很多命令的理解和记忆更胜一筹.

Git中指针大致有五类:

  • HEAD
    指向当前正在操作的 commit。

  • 分支指针

    指向当前分支所在的commit

  • ORIG_HEAD
    当使用一些在 Git 看来比较危险的操作去移动 HEAD 指针的时候,ORIG_HEAD 就会被创建出来,记录危险操作之前的 HEAD,方便 HEAD 的恢复,有点像修改前的备份。

  • FETCH_HEAD
    记录从远程仓库拉取的记录。

  • MERGE_HEAD
    当运行 git merge 时,MERGE_HEAD 记录你正在合并到你的分支中的提交。MERGE_HEAD在合并的时候会出现,合并结束,就删除了这个文件。

  • CHERRY_PICK_HEAD
    记录您在运行 git cherry-pick时要合并的提交。同上,这个文件只在 cherry-pick 期间存在。

这里我们主要理解HEAD指针和分支指针.

正常情况下HEAD指针是指向当前分支指针,如图:

git checkout <commit id>

git checkout 本质就是移动HEAD指针 (个人理解,待验证)

当执行git checkout <commit id>git checkout HEAD^时,进入指针分离模式,HEAD指针指向一条commit id,不再指向分支指针,此时如果提交代码,只有HEAD指针会指向新提交的commit id,而分支指针不会移动.

此时如果想让HEAD重新指向分支指针,执行git checkout <branch name>即可.

git reset

reset 的本质:移动 HEAD 以及它所指向的 branch

实质上,reset 这个指令虽然可以用来撤销 commit ,但它的实质行为并不是撤销,而是移动 HEAD ,并且「捎带」上 HEAD 所指向的 branch(如果有的话)。也就是说,reset 这个指令的行为其实和它的字面意思 “reset”(重置)十分相符:它是用来重置 HEAD 以及它所指向的 branch 的位置的。

reset --hard HEAD^ 之所以起到了撤销 commit 的效果,是因为它把 HEAD 和它所指向的 branch 一起移动到了当前 commit 的父 commit 上,从而起到了「撤销」的效果:

git reset 各个选项解释:

  • –soft – 缓存区和工作目录都不会被改变
  • –mixed – 默认选项。缓存区和你指定的提交同步,但工作目录不受影响
  • –hard – 缓存区和工作目录都同步到你指定的提交

把这些标记想成定义 git reset 操作的作用域就容易理解多了。

git branch -f <branch name>

这条命令是将分支指针移动到指定位置.运行git branch -f master c1:

匿名分支与头指针分离处理

  • 匿名分支

当git进入头指针分离状态,进行提交会产生一个匿名分支,如果一直匿名分支,就会被git清理掉,

可以使用`git branch ``起个名字,来避免这种情况.

  • 强制回退丢失 commit id

如果意外使用如:git reset --hard HEAD^时,可能会使commit消失,这时候可以使用git reflog来找回命令

分支

几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。 在很多版本控制系统中,这是一个略微低效的过程——常常需要完全创建一个源代码目录的副本。对于大项目来说,这样的过程会耗费很多时间。

有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。 为何 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。 理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,并且从此真正改变你的开发方式。

命令

git branch #列出仓库中所有分支。
git branch <branch> #创建一个名为 <branch> 的分支。不会 自动切换到那个分支去。
git branch -d <branch> #删除指定分支。这是一个安全的操作,Git 会阻止你删除包含未合并更改的分支。
git branch -D <branch> #强制删除指定分支,即使包含未合并更改。如果你希望永远删除某条开发线的所有提交,你应该用这个命令。
git branch -m <branch> #将当前分支命名为 <branch>。


git checkout <existing-branch> #查看特定分支,分支应该已经通过 git branch 创建。这使得 <existing-branch> 成为当前的分支,并更新工作目录的版本。
git checkout -b <new-branch> #创建并查看 <new-branch>,-b 选项是一个方便的标记,告诉Git在运行 git checkout <new-branch> 之前运行 git branch <new-branch>。
git checkout -b <new-branch> <existing-branch> #和上一条相同,但将 <existing-branch> 作为新分支的基,而不是当前分支。

分支合并的两种姿势

git merge

merge的三种特殊情况
  • 冲突

    merge 在做合并的时候,是有一定的自动合并能力的:如果一个分支改了 A 文件,另一个分支改了 B 文件,那么合并后就是既改 A 也改 B,这个动作会自动完成;如果两个分支都改了同一个文件,但一个改的是第 1 行,另一个改的是第 2 行,那么合并后就是第 1 行和第 2 行都改,也是自动完成。

但,如果两个分支修改了同一部分内容,merge 的自动算法就搞不定了。这种情况 Git 称之为:冲突(Conflict)。

解决方法:

  1. 解决掉冲突
  2. 手动 commit 一下

或者放弃合并:

git merge --abort
  • HEAD 领先于目标 commit

    如果 merge 时的目标 commitHEAD 处的 commit 并不存在分叉,而是 HEAD 领先于目标 commit

那么 merge 就没必要再创建一个新的 commit 来进行合并操作,因为并没有什么需要合并的。在这种情况下, Git 什么也不会做,merge 是一个空操作。

  • HEAD 落后于 目标 commit——fast-forward

    而另一种情况:如果 HEAD 和目标 commit 依然是不存在分叉,但 HEAD 不是领先于目标 commit,而是落后于目标 commit.

    那么 Git 会直接把 HEAD(以及它所指向的 branch,如果有的话)移动到目标 commit

git rebase

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

git merge branch1

如果把 merge 换成 rebase,可以这样操作:

git checkout branch1
git rebase master

另外,在 rebase 之后,记得切回 mastermerge 一下,把 master 移到最新的 commit

git checkout master
git merge branch1

为什么要从 branch1rebase,然后再切回 mastermerge 一下这么麻烦,而不是直接在 master 上执行 rebase

从图中可以看出,rebase 后的 commit 虽然内容和 rebase 之前相同,但它们已经是不同的 commits 了。如果直接从 master 执行 rebase 的话,就会是下面这样:

这就导致 master 上之前的两个最新 commit 被剔除了。如果这两个 commit 之前已经在中央仓库存在,这就会导致没法 push 了:

它会把整个 branch1分支移动到 master 分支的后面,有效地把所有 master 分支上新的提交并入过来。但是,rebase 为原分支上每一个提交创建一个新的提交,重写了项目历史,并且不会带来合并提交。

rebase最大的好处是你的项目历史会非常整洁。首先,它不像 git merge 那样引入不必要的合并提交。其次,如上图所示,rebase 导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的 fork。这让你更容易使用 git loggit bisectgitk 来查看项目历史。

不过,这种简单的提交历史会带来两个后果:安全性和可跟踪性。如果你违反了 rebase 黄金法则,重写项目历史可能会给你的协作工作流带来灾难性的影响。此外,rebase 不会有合并提交中附带的信息——你看不到 branch分支中并入了上游的哪些更改。

交互式rebase

交互式的 rebase 允许你更改并入新分支的提交。这比自动的 rebase 更加强大,因为它提供了对分支上提交历史完整的控制。一般来说,这被用于将 feature 分支并入 master 分支之前,清理混乱的历史。

-i 传入 git rebase 选项来开始一个交互式的rebase过程:

git checkout feature
git rebase -i master

它会打开一个文本编辑器,显示所有将被移动的提交:

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

这个列表定义了 rebase 将被执行后分支会是什么样的。更改 pick 命令或者重新排序,这个分支的历史就能如你所愿了。比如说,如果第二个提交修复了第一个提交中的小问题,你可以用 fixup 命令把它们合到一个提交中:

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

忽略不重要的提交会让你的 feature 分支的历史更清晰易读。这是 git merge 做不到的。

Rebase的黄金法则

当你理解 rebase 是什么的时候,最重要的就是什么时候 不能 用 rebase。git rebase 的黄金法则便是,绝不要在公共的分支上使用它。

这次 rebase 将 master 分支上的所有提交都移到了 feature 分支后面。问题是它只发生在你的代码仓库中,其他所有的开发者还在原来的 master 上工作。因为 rebase 引起了新的提交,Git 会认为你的 master 分支和其他人的 master 已经分叉了。

同步两个 master 分支的唯一办法是把它们 merge 到一起,导致一个额外的合并提交和两堆包含同样更改的提交。不用说,这会让人非常困惑。

所以,在你运行 git rebase 之前,一定要问问你自己「有没有别人正在这个分支上工作?」。如果答案是肯定的,那么把你的爪子放回去,重新找到一个无害的方式(如 git revert)来提交你的更改。不然的话,你可以随心所欲地重写历史。

强制推送

如果你想把 rebase 之后的 master 分支推送到远程仓库,Git 会阻止你这么做,因为两个分支包含冲突。但你可以传入 --force 标记来强行推送。就像下面一样:

# 小心使用这个命令!
git push --force

它会重写远程的 master 分支来匹配你仓库中 rebase 之后的 master 分支,对于团队中其他成员来说这看上去很诡异。所以,务必小心这个命令,只有当你知道你在做什么的时候再使用。

仅有的几个强制推送的使用场景之一是,当你在想向远程仓库推送了一个私有分支之后,执行了一个本地的清理(比如说为了回滚)。这就像是在说「哦,其实我并不想推送之前那个 feature 分支的。用我现在的版本替换掉吧。」同样,你要注意没有别人正在这个 feature 分支上工作。

本地清理

在你工作流中使用 rebase 最好的用法之一就是清理本地正在开发的分支。隔一段时间执行一次交互式 rebase,你可以保证你 feature 分支中的每一个提交都是专注和有意义的。你在写代码时不用担心造成孤立的提交——因为你后面一定能修复。

调用 git rebase 的时候,你有两个基(base)可以选择:上游分支(比如 master)或者你 feature 分支中早先的一个提交。我们在「交互式 rebase」一节看到了第一种的例子。后一种在当你只需要修改最新几次提交时也很有用。比如说,下面的命令对最新的 3 次提交进行了交互式 rebase:

git checkout feature
git rebase -i HEAD~3

通过指定 HEAD~3 作为新的基提交,你实际上没有移动分支——你只是将之后的 3 次提交重写了。注意它不会把上游分支的更改并入到 feature 分支中。

如果你想用这个方法重写整个 feature 分支,git merge-base 命令非常方便地找出 feature 分支开始分叉的基。下面这段命令返回基提交的 ID,你可以接下来将它传给 git rebase

git merge-base feature master

交互式 rebase 是在你工作流中引入 git rebase 的的好办法,因为它只影响本地分支。其他开发者只能看到你已经完成的结果,那就是一个非常整洁、易于追踪的分支历史。

但同样的,这只能用在私有分支上。如果你在同一个 feature 分支和其他开发者合作的话,这个分支是公开的,你不能重写这个历史。

用带有交互式的 rebase 清理本地提交,这是无法用 git merge 命令代替的。

保存当前工作状态

此时我在 feature_666 分支,非常聚精会神加持高专注地实现一个功能 666 模块,简直键盘如飞的编写代码~~~ 然后这时,客户反馈出一个 bug , 非常严重,必须立马解决,优先级为 0 !!! 于是,我需要去到 release 分支去 checkout 新的分支去工作了,但是 666 功能还没完成怎么办? 此时我面临着一个选择题:

A:提交后切换,代码保存到分支 feature_666,却产生一个无意义的提交

B:不提交直接切换,然而这个选项根本没人会选。(因为不提交没办法切换分支)

是不是很难选,此时,别忘记还有 C 选项!

C:使用 git stash , 将当前修改(未提交的代码)存入缓存区,切换分支修改 bug ,回来再通过 git stash pop 取出来。

———————————————— 版权声明:本文为CSDN博主「DRPrincess」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_32452623/article/details/76100140

命令:

git stash #将修改储存到暂存区,工作区会删除修改

git stash show #查看刚才暂存的修改

git stash pop #取出修改并删除记录列表中对应记录

git stash list #查看暂存区的所有暂存修改记录

git stash save <message> #给stash 存储的修改起个名字并保存

git stash apply stash@{X} #取出相应的暂存

git stash drop stash@{X} #将记录列表中取出的对应暂存记录删除

标签

命令:

git tag <tag name> #打一个标签

git checkout <tag name> #检出一个标签

git tag -d <tag name> #删除一个标签

远程交互

添加远程仓库(git init方式)

  • 初始化
git init
  • 添加远程仓库

git remote add <repository name> <url>

git remote add origin xxx.git
  • 拉取代码
git fetch
  • 将当前分支关联远程分支
git branch -u origin/master
  • 关联关系
git merge origin/master --allwo-unrelated-histories
  • 合并当前与远程分支
git merge origin/dev

克隆方式(git clone)

  • 克隆仓库
git clone url
  • 检出分支
git checkout -b <local branch name> <remote branch name> #拉取本地分支与远程分支关联

有关远程分支的命令

git branch -a #查看远程分支

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

本地分支与远程分支名称问题

本地分支与远程对应的分支尽量同名,如果不同命会出现问题:

    fatal: The upstream branch of your current branch does not match
the name of your current branch.  To push to the upstream branch
on the remote, use

    git push origin HEAD:dev

To push to the branch of the same name on the remote, use

    git push origin HEAD

To choose either option permanently, see push.default in 'git help config'.

可以使用命令提交:

git push origin dev2:dev

或者更改git 配置:

git config --global push.default upstream

操纵远程分支

  • 将本地分支推送到远程
git checkout -b feature-branch    #创建并切换到分支feature-branch  

git push origin feature-branch:feature-branch    #推送本地的feature-branch(冒号前面的)分支到远程origin的feature-branch(冒号后面的)分支(没有会自动创建)
  • 删除远程分支
git push origin --delete <branch name> 
# 或者
git branch -r -d origin/<branch  name>

git push origin :<branch name>