Git的骚操作你都会嘛? 教会你远离Git噩梦

正式开始之前先讲个亲身经历经历的故事,在上一个公司工作有个女同事,晚上和男朋友提前说好了约会看电影,快五点了,就想着赶紧提交代码,好下班,可是她push了好几次,都被远端拒绝了,这时候她一狠心一跺脚用了-f,然后就下班去了,她倒是下班了,由于她对之前提交的代码用了rebase,而其他同事又基于她之前的代码开发,直接导致了其他同事的commit id出现了混乱,而导致当天的上线被搁置了,这是一个真实的案例,而其中的问题就在于这个女同事对rebase不熟悉,并采用了危险的git操作。因此,git的操作虽然简单,但在开发过程中却非常重要,如果用不好,自己的工作白费了是轻的,更糟糕的是有可能让自己成为同事厌恶的人。因此git用的好,将会是一个神器,用不好就是噩梦!

前言

首先问一下自己以下几个问题:

  • 如何合并几个历史commit?

  • 如何修改历史提交的message?

  • 如何将某个分支的某次commit移到另一个分支尼?

要是你都会,那恭喜你,可以不用阅读本文了,要是还有疑惑,那就跟着我一起来学习学习吧。

一、基本配置

配置用户信息

git config --global user.name "FrontDream"

git config --global user.email [email protected]

以上配置过程中用到了--global,其实还有--local--system,下面对此做一个对比,问题不大,可以不用着重考虑。

git config --local  // local只对某个仓库有效

git config --global // global对当前用户所有仓库有效

git config --system // 不常用,对系统所有登陆用户有效

如果想要显示 config 的配置,可以加上--list:

git config --list --local

git config --list --global

git config --list --system

配置文本编辑器

当用户信息设置完毕,你可以配置默认文本编辑器了,当 Git 需要你输入信息时会调用它。如果没有配置,Git 会使用操作系统默认的文本编辑器,通常是 Vim。如果你想使用不同的文本编辑器,例如 Emacs,可以这样做:

git config --global core.editor emacs

配置命令的别名

git config --global alias.ci commit

可以通过 git config 文件来轻松地为每一个命令设置一个别名,如上配置后,当要输入 git commit 时,只需要输入 git ci

二、建立仓库

主要有以下几种场景:

把已有的项目代码纳入到 Git 管理

$cd 项目代码所在文件夹
$git init

新建的项目(没有代码)直接用 Git 管理

$ cd 某个文件夹
$ git init your_project #会在当前路径下创建和项目名称同名的文件夹
$ cd your_project

克隆仓库

git clone https://github.com/libgit2/libgit2

这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹, 从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。如果你进入到这个新建的 libgit2 文件夹,你会发现所有的项目文件已经在里面了,准备就绪等待后续的开发和使用。

如果你想在克隆远程仓库的时候,自定义本地仓库的名字,你可以通过额外的参数指定新的目录名:

git clone https://github.com/libgit2/libgit2 mylibgit

这会执行与上一条命令相同的操作,但目标目录名变为了 mylibgit

添加远程仓库

当你本地已经有了代码,在 GitHub 上新建了一个仓库,这时候需要将远程的仓库与本地进行合并与关联。

首先你需要在仓库中获取到自己的 SSH:

通过命令:

git remote add origin [email protected]:FrontDream/FrontDream.github.io.git

这样就将本地的项目与远程的项目进行了关联,如果这时候远程是有文件的如 readme 文件,直接git push --all会失败,这个时候,需要将远程的代码拉下来git fetch,然后再git merge --allow-unrelated-histories origin/master,再git push或者git push -u origin master就好了。

如果想要在某个的仓库下配置local的用户名和邮箱,可以在当前仓库的路径下用以下设置:

git config --local user.name "FrontDream"

git config --local user.email [email protected]

这样就达到了只对某个仓库进行配置的目的。如下图所示

三、深入探索.git 文件

HEAD 文件

如上图所示,现在 master 分支上,通过cd .git进行git文件,然后通过cat命令将 HEAD 文件的内容输出,然后切换到 204-分支, 再次进入.git文件,并将 HEAD 文件的内容输入。通过对比,我们可以发现,HEAD 文件存放的是当前所在分支的引用。

Config 文件

如上图所示,通过在.git目录下,通过cat命令将 congig 文件的内容输出,我们可以看到,这个文件存储的就是我们之前配置的用户名和邮箱,我们可以通过文本编辑器打开这个文件,修改用户名和邮箱,这个修改的效果和我们用git config 的命令相同。

refs 文件

还记得上面 HEAD 文件中存放的是refs/heads/master嘛?指的就是 refs 文件夹下的 heads 中的 master, 如上图所示,通过在.git目录下,通过cd命令进入 refs,可以看到有 heads 文件夹和 tags 文件夹,继续进入 heads,我们可以看到有很多分支名称的文件,我们通过cat命令输出内容,可以发现,各个分支名的文件存储的其实是 commit 的哈希。可以通过命令git cat-file -t 跟上输出内容的前面一部分查看其是什么类型,输出是commit类型。

回到 refs 进入 tags,发现有很多的版本号的名称,我们通过cat输出其中某个版本号文件的内容,同样发现,tag 同样也是某个 commit 的哈希值。但是通过git cat-file -t 跟上输出内容的前面一部分查看其是什么类型,输出是tag类型。继续通过git cat-file -p跟上哈希值(一部分就行),发现其实他是一个object。不要停,继续通过git cat-file -t 跟上object后面的一部分哈希值,输出了commit类型。这里需要注意的是git cat-file后面跟-t是查看类型,-p是查看内容

结论: heads中存放的是各个分支名,而各个分支名其实是commit的哈希值,而tags我们也是在某个 commit 后打的标签,其是通过一个tag类型,其实是一个object对象,包裹着一个commit哈希值。

objects 文件

首先从.git目录下进入objects目录,我们发现目录下面有很多两位数的文件夹,进入其中一个如6b,我们发现有很多哈希文件,通过6b,与哈希值进行拼接,并用git cat-file -t 查看类型,发现为tree类型(tree类型是git中重要的类型,其他类型还有commitblob等);用git cat-file -p查看内容,我们发现又是一堆哈希,继续用-t查看类型,-p查看内容,我们发现这是我们的新增文件呀,为blob类型。

四、基本用法

我们将在此部分中用提问的方式,引出每一条命令。

Q1: 如何查看工作区、暂存区的状态?

git status

命令的输出十分详细,但其用语有些繁琐。 Git 有一个选项可以帮你缩短状态命令的输出,这样可以以简洁的方式查看更改。如果你使用 git status -s 命令或 git status --short 命令,你将得到一种格式更为紧凑的输出。

git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt

新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有A 标记,修改过的文件前面有 M 标记。输出中有两栏,左栏指明了暂存区的状态,右栏指明了工作区的状态。例如,上面的状态报告显示: README 文件在工作区已修改但尚未暂存,而 lib/simplegit.rb 文件已修改且已暂存。 Rakefile 文件已修,暂存后又作了修改,因此该文件的修改中既有已暂存的部分,又有未暂存的部分。

Q2: 如何查看工作区、暂存区的修改后的差异?

git diff

此比较的是工作目录中当前文件和暂存区域快照之间的差异。也就是修改之后还没有暂存起来的变化内容。

git diff --staged

这条命令将比对已暂存文件与最后一次提交的文件差异。

git diff 分支一 分支二 -- 要比较的文件

此命令对两个分支的某个文件进行比较差异。

git diff #hash1 #hash2

此命令对比 commit1 和 commit2 的差异。

git diff HEAD HEAD~2

对比 HEAD(其实指向的也是一个commit的哈希)与 HEAD 的父亲的父亲(上两次 commit)的差异。

Q3: 如何跳过添加到暂存区直接提交?

git commit -a -m 'added new benchmarks'
git commit -am 'added new benchmarks'  // 简化版

此命令将跳过使用暂存区域,尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。Git 提供了一个跳过使用暂存区域的方式, 只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤。

Q4: 如何将连续的多个commit合并成一个?

以下图为例,需要将前三个commit合并成一个,首先复制第三个commit后面的哈希值。

然后运行命令:

git rebase -i #复制的哈希

出现如下界面:

将第二、第三的pick修改为s(squash 简写),如下图所示,保存按esc,输入:wq!保存退出。

继续出现如下界面:

添加一条message,如下图所示:

保存按esc输入:wq!退出。成功界面如下图所示:

squash是指将该commit合并到相邻的pickcommit中。

Q5: 如何重命名或者移动文件?

重命名文件或者移动文件。如果是移动根目录下的文件,要有相对路径。

git mv file_from file_to

Q6: 如何删除指定文件?

git rm 待删除文件

Q7: 如何保存文件,不提交到暂存区?

这个问题可以用一个场景来解释,如果在开发过程中,工作区已经有了修改内容,这时需要立马修复一个 bug,这时是不可能直接跳到某个分支去修改的,我们需要先提交到暂存区,但是我其实不满意与现在的修改,后面还需要再大改,我不想提交到暂存区。这时就可以通过命令:

git stash

此命令将工作区的内容存放在堆栈中,当我们修改好了 bug,就重新用:

git stash apply
or
git stash pop

恢复工作区的内容,需要注意的是,apply会在堆栈中保存原有的stash信息,而pop将会把stash中的内容清空。

Q8: 如何查看历史提交记录?

git log

我相信小孩都知道,在不传入任何参数的默认情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。

其中一个比较有用的选项是 -p 或 --patch ,它会显示每次提交所引入的差异(按 补丁 的格式输出)。你也可以限制显示的日志条目数量,例如使用 -2 选项来只显示最近的两次提交。

git log -p -2

如果我希望在一行显示需要的信息,可以进行相应的配置如:

git log --pretty=oneline
git log --oneline //简化版

oneline 会将每个提交放在一行显示,在浏览大量的提交时非常有用。另外还有 shortfull 和 fuller 选项,它们展示信息的格式基本一致,但是详尽程度不一。

git log --pretty=format:"%h - %an, %ar : %s"

可以定制记录的显示格式:

当 oneline 或 format 与另一个 log 选项 --graph 结合使用时尤其有用。这个选项添加了一些 ASCII 字符串来形象地展示你的分支、合并历史。

git log --pretty=format:"%h %s" --graph

限制输出长度

示例如下:

$ git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" \
   --before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:\$current_branch" into an unborn branch

以上是通过命令行git log查看版本历史信息,当然也可以通过gitk命令调出图形化界面,查看历史信息。在命令行中输入gitk,调出如下界面:

Q9: 如何修改最近的提交信息?

有时候我们提交完了才发现漏掉了几个文件没有添加,或者说上一次的提交信息写错了,我们想修改当前分支最近的 commit 的 message。此时,可以运行带有 --amend 选项的提交命令来重新提交,第二次的提交会替代掉第一次的提交。

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

两次提交只有一次提交信息。

Q10: 如何修改历史的提交信息尼?

这里需要用到rebase了。如下图,我们想修改历史的提交信息“ant4.0 升级”为“升级 ant4.0”。

首先需要找到需要修改的提交信息的上一个(父级)的哈希值,记住是需要修改的上一次提交的哈希值,并复制,重要的事情说三遍!

然后通过命令:

git rebase -i #你复制的哈希值

然后将出现如下界面:

如下图所示,修改你需要修改的提交信息前面的pickr(reward 的简写),保存,然后点击一下esc,输入:wq!回车退出。

接着出现如下界面,供你修改提交信息。

编辑修改,并保存,在英文输入法下点击一下esc,输入:wq!回车退出。出现如下界面,即成功修改了历史的提交信息。

Q11: 如何取消暂存中的文件?

可以通过如下命令实现取消暂存中的文件:

git reset HEAD <file>

Q12: 如何取消工作区中的文件?

如果我们现在正在工作区中工作,发现现在的写法还不如已经提交的写法,想把还没添加到暂存区的文件取消修改,可以通过如下命令:

git checkout -- <file>

Q13: 如何回退到某个 commit?

有时候我们提交了一些改动,后来又不想要了。有可能是WIP提交,也可能是某个引入了 bug 的提交。这种情况,我们可以执行git resetgit reset会丢弃当前所有暂存的文件,并让我们决定 HEAD 应该指向哪里。

git reset --soft #哈希

soft reset 将HEAD 移动到指定的提交(或者相对于HEAD 的位置索引),同时不会丢弃这些提交带来的改动。执行git status,你会看到我们依然能够查看之前提交所做的改动。这很有用,因为这样我们就能继续修改文件内容,后续再次提交了。

git reset --hard #哈希

有时候,我们不想保留某些提交带来的改动。跟 soft reset 不一样,我们不再需要访问这些变动了。Git 应该简单地重置到指定的提交,并且会重置工作区和暂存区的文件。

Q13: 如何删除不需要的分支?

git branch -d you-branch

Q14: 如何查看分支?

git branch -v
or
git branch -av

-v表示的是查看本地有哪些分支,而-av查看的是本地和远程的分支。

还有一种情况是为了查看设置的所有跟踪分支可以用-vv。将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。

git branch -vv

Q15: 如何查看哪些分支已经合并到当前分支?

git branch --merged
git branch --no-merged

--merged查看有哪些分支已经合并到当前分支,而--no-merged查看所有包含未合并工作的分支,因为它包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败(可以使用 -D 选项强制删除它)。

Q16: 如何跟远程分支关联?

如果远程已经有了feature-branch,通过git fetch拉到本地,并通过以下命令在本地新建了feature-branch,并同远程分支关联。

git checkout -b feature-branch origin/feature-branch

Q17: 如何跟将本地代码推送到远程的某个分支?

git push origin local-branch:feature-branch

推送本地的 local-branch(冒号前面的)分支到远程 origin 的 feature-branch(冒号后面的)分支(没有会自动创建)

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/feature-branch,指向服务器的 feature-branch 分支的引用。本地不会自动生成一份可编辑的副本(拷贝)。换一句话说,这种情况下,不会有一个新的 feature-branch 分支,只有一个不可以修改的 origin/feature-branch 指针。

可以运行 git merge origin/feature-branch 将这些工作合并到当前所在的分支。

如果想要在自己的 my_branch 分支上工作,可以将其建立在远程跟踪分支之上:

git checkout -b my_branch origin/feature-branch

这样就新开了一个本地分支 my_branch 用户跟踪远程的 feature-branch。看起来是挺复杂的,但是可以简化:

git checkout --track origin/feature-branch

这样的本地的 feature-branch 分支就可以跟踪远程的 feature-branch 分支了。还可以继续简化:

git checkout feature-branch

当然简化的化就无法重命名了,如果需要重命名还是得使用:

git checkout -b my_branch origin/feature-branch

Q18: 如何获取某个分支的某次提交内容?

当活动分支需要某个分支的某个提交包含的改动时,我们可以用cherry-pick命令。通过cherry-pick某个提交,在当前活动分支上会创建一个新提交,包含了前者带来的改动。

如下图所示(盗来的),假设 dev 分支上的提交76d12改动了index.js文件,我们在master分支上也需要。我们不需要整个分支上的改动,只要这个提交。

git cherry-pick 76d12

Q19: pull 与 fetch 有什么区别?

用 git fetch 把这些改动获取到本地。这不会影响本地分支,fetch只是下载数据。git pull实际上是两个命令合而为一:git fetchgit merge。当我们从 origin 拉取改动时,先是像git fetch一样获取所有数据,然后最新改动会自动合并到本地分支。

Q20: 如何变基?

如上图所示,我们从C2拉出了特性分支experiment并进行了开发到C4,同时master继续开发到C3,现在我们需要将C3,与C4进行合并,通过git merge是其中一种策略:

git checkout master

git merge experiment

另外一种策略就是通过变基:

git checkout experiment

git rebase master

git checkout master

git merge experiment

这两种整合方法的最终结果都是将C3C4进行合并到主分支,结果没有任何区别,主要是历史记录的区别,rebase是一种直线型的,提交历史非常清晰整洁,而merge相对来说分支比较复杂。

尽管变基会使得我们的提交历史变得更加简洁,但是变基是有风险的,但是当rebase出现冲突时,处理过程比较麻烦,同时当对已经push到远端,同事也基于你的push进行开发,然后你又对之前的push执行变基操作,就会出问题,本文开始的故事就是真实的案例。总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作

总结

git操作总的来说并不是很难,难点在于对命令的理解和记住,好好掌握,让git成为我们的利器而不是我们的噩梦!

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

点个在看支持我吧,转发就更好了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章