本文由云 + 社区发表
作者:工程师小熊
摘要:上一集我们一起入门学习了 git 的基本概念和 git 常用的操作,包括提交和同步代码、使用分支、出现代码冲突的解决办法、紧急保存现场和恢复现场的操作。学会以后已经足够我们使用 Git 参加协作开发了,但是在开发的过程中难免会出错,本文主要介绍版本控制的过程中出错了的场景,以及 Git 开发的一些技巧,让我们用的更流畅。
上集回顾:
本文核心:
如果你发现刚刚的操作一不小心 commit 了,所幸你还没有推送到远程仓库,你可以用reset
命令来撤消你的这次提交。
reset
命令的作用:重置 HEAD(当前分支的版本顶端)到另外一个 commit。
我们的撤消当前提交的时候往往不希望我们此次提交的代码发生任何丢失,只是撤消掉 commit 的操作,以便我们继续修改文件。如果我们是想直接不要了这次 commit 的全部内容的任何修改我们将在下一小节讨论。
来,我们先说一句蠢话来 diss 老板
$ touch to_boss.txt
$ echo 'my boss is a bad guy!' > to_boss.txt
$ git add to_boss.txt
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: to_boss.txt
$ git commit -m "[+]骂了我的boss"
[master 3d113a7] [+]骂了我的boss
1 file changed, 1 insertion(+)
create mode 100644 to_boss.txt
my boss is a bad guy!
add
然后status
查看新文件已经加入跟踪commit
提交了这次的修改好了,刚刚我们 “不小心” diss 了我们的老板,要是被发现就完了,所幸还没有push
,要快点撤消这些提交,再换成一些好话才行。
我们使用以下命令:
$ git reset --soft head^
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: to_boss.txt
$ cat to_boss.txt
my boss is a bad guy!
$ echo 'my boss is a good boy!'
my boss is a good boy!
$ echo 'my boss is a good boy!' > to_boss.txt
$ cat to_boss.txt
my boss is a good boy!
$ git add to_boss.txt
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: to_boss.txt
$ git commit -m "[*]夸了我的boss"
[master 8be46aa] [*]夸了我的boss
1 file changed, 1 insertion(+)
create mode 100644 to_boss.txt
git reset --soft head^
撤消了本次提交,将工作区恢复到了提交前但是已经add
的状态to_boss.txt
的内容改成了my boss is a good boy!
add
然后commit
提交好了,有惊无险,这就是撤消 commit 的操作。另一种情况是如果你想撤消 commit 的时候支持舍弃这次全部的修改就把git reset --soft head^
改成git reset --hard head^
,这样你本地修改就彻底丢掉了 (慎用),如果真用了想找回来怎么办?见救命的后悔药。
当然了,你只要开心不加soft
或hard
参数也是安全的 (相当于使用了--mixed
参数),只不过是撤消以后你的本次修改就会回到add
之前的状态,你可以重新检视然后再做修改和commit
。
要是我们做的更过分一点,直接把这次commit
直接给push
怎么办?要是被发现就全完了,我们来看看 github 上的远程仓库。
upload successful
完了,真的提交了(我刚刚 push 的)让我们冷静下来,用撤消当前 commit 的方法先撤消本地的commit
,这次我们来试试用hard
参数来撤消
$ git reset --hard head^
HEAD is now at 3f22a06 [+]add file time.txt
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
$ git push origin master --force
Total 0 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
+ 3d113a7...3f22a06 master -> master (forced update)
git reset --hard head^
回滚到上一个commit
git status
查看现在的工作区情况,提示Your branch is behind 'origin/master' by 1 commit
,代表成功表了上一次的提示状态,nothing to commit, working tree clean
代表这次的修改全没了,清理的算是一个彻底。如果还想找回来怎么办,我们还真是有办法让你找回来的,见救命的后悔药。git push origin master --force
命令强制提交到远程仓库 (注意,如果是在团队合作的情况下,不到迫不得已不要给命令加--force 参数) 让我们看看github
upload successful
真的撤消了远程仓库,长舒一口气。
如果我们刚刚执行了git reset --soft
或者add
等的操作,把一些东西加到了我们的暂存区,比如日志文件,我们就要把他们从暂存区拿出来。
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: mysql.log
$ git reset -- mysql.log
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
mysql.log
nothing added to commit but untracked files present (use "git add" to track)
status
查看暂存区,里面有一个 mysql.log 被放进去了git reset -- mysql.log
把mysql.log
取出来status
可以看到真的取出来了 然后如果不要想这个文件的话再 rm 掉就好啦,但是如果这些文件每次自动生成都要用这种方式取出暂存区真的好累,我们可以用 git 忽略不想提交的文件当我们想要把某个文件任意的回滚到某次提交上,而不改变其他文件的状态我们要怎么做呢?
我们有两种情况,一种是,只是想在工作区有修改的文件,直接丢弃掉他现在的修改;第二种是想把这个文件回滚到以前的某一次提交。我们先来说第一种:
$ cat time.txt
10:41
$ echo 18:51 > time.txt
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: time.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ cat time.txt
18:51
$ git checkout -- time.txt
$ cat time.txt
10:41
time.txt
的内容,可以status
看到他发生了变化git checkout -- time.txt
, 取消这次在工作区的修改,如果他已经被add
加到了暂存区,那么这个命令就没有用了,他的意思是取消本次在工作区的修改,去上一次保存的地方。如果没有add
就回到和版本库一样的状态;如果已经加到了暂存区,又做了修改,那么就回加到暂存区后的状态将文件回滚到任意的版本我们这里说的把文件回滚到以前的某个版本的状态,完整的含义是保持其他文件的内容不变,改变这个文件到以前的某个版本,然后修改到自己满意的样子和做下一次的提交。核心命令
git checkout [<options>] [<branch>] -- <file>...
我们还是用time.txt
这个文件来做试验,先搞三个版本出来,在这里我已经搞好了,来看看:
版本 1,time.txt 内容 00:50
commit 35b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4 (HEAD -> master, origin/master, origin/HEAD)
Author: pzqu <pzqu@example.com>
Date: Sun Dec 23 00:51:54 2018 +0800
[*]update time to 00:50
版本 2,time.txt 内容 18:51
commit 856a74084bbf9b678467b2615b6c1f6bd686ecff
Author: pzqu <pzqu@example.com>
Date: Sat Dec 22 19:39:19 2018 +0800
[*]update time to 18:51
版本 3,time.txt 内容 10:41
commit 3f22a0639f8d79bd4e329442f181342465dbf0b6
Author: pzqu <pzqu@example.com>
Date: Tue Dec 18 10:42:29 2018 +0800
[+]add file time.txt
现在的是版本 1,我们把版本 3 检出试试。
$ git checkout 3f22a0639f8d -- time.txt
$ cat time.txt
10:41
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: time.txt
checkout
+commit id
+-- filename
的组合,横跨版本 2 把历史版本 3 的time.txt
搞出来了我们来把 time.txt 恢复到版本 1,同样的方法,因为版本 1 是上一次提交我们可以省略掉版本号
$ git checkout -- time.txt
$ cat time.txt
00:50
看到了吧!只要用git checkout commit_id -- filename
的组合,想搞出哪个文件历史版本就搞出哪个。
到了这里,你可能会很懵比,reset
和checkout
命令真的好像啊!都可以用来做撤消
checkout
语义上是把什么东西取出来,所以此命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。reset
语义上是重新设置,所以此命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。还想不通可以给我发邮件:pzqu@qq.com
来到这里我已经很清楚的你的现况了,你的代码丢了现在一定非常的着急,不要慌,总是有办法找回他们的。但是前提是要保证你的项目根目录下.git 文件夹是完整的,要是手动删除了里面的一些东西那就真完了。还要保证一点,你的代码以前是有过 git 追踪的,最少add
过
Git 提供了一个命令git reflog
用来记录你的每一次命令,贴个图吧直观点:
upload successful
git reflog
里的全部都是和改变目录树有关的,比如commit rebase reset merge
,也就是说一定要有改变目录树的操作才恢复的回来git log
是一样的,也可以看到所有分支的历史提交,不一样的是看不到已经被删除的 commit
记录和 reset rebase merge
的操作 我们可以看到git reflog
前面的就是commit id
,现在我们就可以用之前介绍过的方法来回滚版本了,撤消当前 commit$ git reset --hard 856a740
HEAD is now at 856a740 [*]update time to 18:51
$ git log -1
commit 856a74084bbf9b678467b2615b6c1f6bd686ecff (HEAD -> master)
Author: pzqu <pzqu@example.com>
Date: Sat Dec 22 19:39:19 2018 +0800
[*]update time to 18:51
$ git reset --hard 35b66ed
HEAD is now at 35b66ed [*]update time to 00:50
$ git log -2
commit 35b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4 (HEAD -> master, origin/master, origin/HEAD)
Author: pzqu <pzqu@example.com>
Date: Sun Dec 23 00:51:54 2018 +0800
[*]update time to 00:50
commit 856a74084bbf9b678467b2615b6c1f6bd686ecff
Author: pzqu <pzqu@example.com>
Date: Sat Dec 22 19:39:19 2018 +0800
[*]update time to 18:51
git reflog
返回的结果,用git reset --hard commit_id
回退到856a740
这个版本git log -1
看近一行的日志,可以看到目前就在这了git reflog
的结果,用git reset --hard 35b66ed
跑到这次提交git log -2
看到两次提交的日志,我们就这么再穿梭过来了,就是这么爽 但是我们如果只是想把此提交给找回来,恢复他,那还是不要用reset
的方式,可以用cherry-pick
或者merge
来做合并你之前没有 commit 过的文件,被删除掉了,或者被reset --hard
的时候搞没了,这种情况可以说是相当的难搞了,所幸你以前做过add
的操作把他放到过暂存区,那我们来试试找回来,先来创建一个灾难现场
$ echo 'my lose message' > lose_file.txt
$ git add lose_file.txt
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: lose_file.txt
$ git reset --hard 35b66ed8
HEAD is now at 35b66ed [*]update time to 00:50
$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
$ ls
README.md need_stash.txt share_file.txt time.txt
lose_file.txt
的文件并写入内容my lose message
,并把他加到暂存区git reset --hard 35b66ed8
用丢弃一切修改的方式来使现在的工作区恢复到35b66ed8
版本,因为还没提交所以也就是恢复到当前的(head
)版本。status
和ls
再看,这个叫lose_file.txt
的文件真的没了,完蛋了,第一反应用刚刚学到的命令git reflow
会发现根本就不好使核心命令:git fsck --lost-found
,他会通过一些神奇的方式把历史操作过的文件以某种算法算出来加到.git/lost-found
文件夹里
$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
Checking objects: 100% (3/3), done.
dangling blob 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
dangling commit fdbb19cf4c5177003ea6610afd35cda117a41109
dangling commit 8be46aa83f0fe90317b0c6b9c201ad994f8caeaf
dangling blob 11400c1d56142615deba941a7577d18f830f4d85
dangling tree 3bd4c055afedc51df0326def49cf85af15994323
dangling commit 3d113a773771c09b7c3bf34b9e974a697e04210a
dangling commit bfdc065df8adc44c8b69fa6826e75c5991e6cad0
dangling tree c96ff73cb25b57ac49666a3e1e45e0abb8913296
dangling blob d6d03143986adf15c806df227389947cf46bc6de
dangling commit 7aa21bc382cdebe6371278d1af1041028b8a2b09
这里涉及到 git 的一些低层的知识,我们可以看到这里有blob、commit、tree
类型的数据,还有tag
等类型的。他们是什么含义呢?
upload successful
blob
组件并不会对文件信息进行存储,而是对文件的内容进行记录commit
组件在每次提交之后都会生成,当我们进行commit
之后,首先会创建一个commit
组件,之后把所有的文件信息创建一个tree
组件,所以哪个blob
代表什么文件都可以在tree
里找到 我们来看看怎么恢复刚刚不见了的lose_file.txt
文件,在上面执行完git fsck --lost-found
命令,返回的第一行blob
我们看看他的内容git show 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
my lose message
git show 7f5965523d2b9e850b39eb46e8e0f7c5755f6719 > lose_file.txt
$ ls
README.md lose_file.txt need_stash.txt share_file.txt time.txt
commit tree
的内容 $ git cat-file -p fdbb19cf4c5177003ea6610afd35cda117a41109 tree 673f696143eb74ac5e82a46ca61438b2b2d3bbf4 parent e278392ccbf4361f27dc338c854c8a03daab8c49 parent 7b54a8ae74be7192586568c6e36dc5a813ff47cf author pzqu pzqu@example.com 1544951197 +0800 committer pzqu pzqu@example.com 1544951197 +0800 Merge branch 'master' of github.com:pzqu/git_test $ git ls-tree 3bd4c055afedc51df0326def49cf85af15994323 100644 blob c44be63b27a3ef835a0386a62ed168c91e680e87 share_file.txtgit cat-file -p
可以看到 commit 的内容,可以选择把这个 commit 合并到我们的分支里,还是reset merge rebase cherry-pick
这些命令来合commit
git ls-tree
列出 tree 下面的文件名和id
的记录信息,然后就可以根据这些来恢复文件了后记:
如果你发现执行git fsck --lost-found
的输出找不到你想要的,那么在执行完git fsck --lost-found
后会出现一堆文件 在 .git/lost-found 文件夹里,我们不管他。可以用以下命令来输出近期修改的文件
$ find .git/objects -type f | xargs ls -lt | sed 3q
-r--r--r-- 1 pzqu staff 32 12 23 12:19 .git/objects/7f/5965523d2b9e850b39eb46e8e0f7c5755f6719
-r--r--r-- 1 pzqu staff 15 12 23 01:51 .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
-r--r--r-- 1 pzqu staff 162 12 23 00:51 .git/objects/35/b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4
$ git cat-file -t 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
blob
$ git cat-file -p 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
my lose message
$ git cat-file -t b2484b5ab58c5cb6ecd92dacc09b41b78e9b0001
tree
$ git cat-file -p b2484b5ab58c5cb6ecd92dacc09b41b78e9b0001
100644 blob f9894f4195f4854cfc3e3c55960200adebbc3ac5 README.md
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 need_stash.txt
100644 blob 83f50ec84c00f5935da8089bac192171cfda8621 share_file.txt
100644 blob f0664bd6a49e268d3db47c508b08d865bc25f7bb time.txt
find .git/objects -type f | xargs ls -lt | sed 3q
返回了近 3 个修改的文件,想要更多就改3q
这个数值,比如你想输出 100 个就用100q
git cat-file -t 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
就能看见文件类型 把最后一个/去掉 复制从 objects/ 后面的所有东西放在-t 后面git cat-file -p id
就能看见文件内容,是不是很爽有时候会碰到我们已经 commit 但是有修改忘记了提交,想把他们放在刚刚的commit
里面,这种时候怎么做呢?
$ git log --name-status --pretty=oneline -1
35b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4 (HEAD -> master, origin/master, origin/HEAD) [*]update time to 00:50
M time.txt
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: lose_file.txt
new file: test_amend.txt
$ git commit --amend --no-edit
[master 31cc277] [*]update time to 00:50
Date: Sun Dec 23 00:51:54 2018 +0800
3 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 lose_file.txt
create mode 100644 test_amend.txt
$ git log --name-status --pretty=oneline -1
31cc2774f0668b5b7c049a404284b19e9b40dc5d (HEAD -> master) [*]update time to 00:50
A lose_file.txt
A test_amend.txt
M time.txt
time.txt
git commit --amend --no-edit
合并到上一个提交里,如果不加--no-edit
参数的话,会提示你来修改 commit 提示信息 (这个命令也可以用在重复编辑commit message
)。标签是一个类似于快照的东西,常常用于测试和发布版本。所以我们常常把tag
名以版本号来命名,比如:v1.0beat1 这样
我们怎么创建标签呢?首先先切换到想打标签的分支,然后直接打就可以了。
$ git branch
dev/pzqu
master
* release_v1.0
$ git tag -a release_v1.0 -m "release v1.0"
$ git tag release_v1.1
$ git tag
release_v1.0
release_v1.1
$ git push --tags
Counting objects: 2, done.
Writing objects: 100% (2/2), 158 bytes | 158.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
* [new tag] release_v1.0 -> release_v1.0
* [new tag] release_v1.1 -> release_v1.1
tag
的分支release_v1.0
带有信息release v1.0
的tag
tag
的提交信息的release_v1.1
git tag
查看tag
tag
也可以推送单个 tag
$ git push origin release_v1.1
Total 0 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
* [new tag] release_v1.1 -> release_v1.1
我们来删除 tag
$ git tag -d release_v1.0
Deleted tag 'release_v1.0' (was eb5d177)
$ git push origin :refs/tags/release_v1.0
To github.com:pzqu/git_test.git
- [deleted] release_v1.0
$ git tag
release_v1.1
release_v1.0
的tag
release_v1.0
的tag
先看看当前的 log
31cc277 (HEAD -> release_v1.0, tag: release_v1.1, origin/release_v1.0, master) [*]update time to 00:50
856a740 [*]update time to 18:51
3f22a06 [+]add file time.txt
4558a25 (origin/dev/pzqu, dev/pzqu) [*]test stash
d9e018e [*]merge master to dev/pzqu
比方说要对[*]update time to 18:51
这次提交打标签,它对应的 commit id 是856a740
,敲入命令:
$ git tag v.9 856a740
$ git log --pretty=oneline --abbrev-commit
31cc277 (HEAD -> release_v1.0, tag: release_v1.1, origin/release_v1.0, master) [*]update time to 00:50
856a740 (tag: v0.9) [*]update time to 18:51
我们有两种情况,一种是我们根本就不想这些文件出现在 git 库里比如日志文件;另一种是 git 远程仓库里有这些文件,就像通用的配置文件,我们必须要在本地修改配置来适应运行环境,这种情况下我们不想每次提交的时候都去跟踪这些文件。
忽略文件的原则是:
我们要怎么做呢?
在 Git 工作区的根目录下创建一个特殊的.gitignore 文件,然后把要忽略的文件名填进去,Git 就会自动忽略这些文件。$ echo ".log" > .gitignore$ touch test.log$ touch test2.log$ ls -a . .git README.md need_stash.txt test.log test_amend.txt .. .gitignore lose_file.txt share_file.txt test2.log time.txt$ git status On branch release_v1.0 nothing to commit, working tree clean 创建并写入忽略规则
*.log
忽略全部以.log
为后缀的文件 * 创建了test.log
和test2.log
*status
查看,真是工作区是clean
,新创建的文件没有被跟踪
核心命令:
git update-index —assume-unchanged 文件名
upload successful
time.txt
文件并写入10:41
,提交到远程仓库git update-index —assume-unchanged
加time.txt
加到忽略名单里time.txt
的内容为10:43
status
查看确实没有被跟踪 看远程仓库upload successful
核心命令:
git update-index —no-assume-unchanged 文件名
upload successful
pull
同步远程仓库,真的没有更新刚刚被添加跟踪忽略的文件git update-index —no-assume-unchanged
取消跟踪忽略status
查看,出现文件的跟踪如果忘记了哪些文件被自己本地跟踪
upload successful
git update-index —assume-unchanged
加time.txt
加到忽略名单里git ls-files -v| grep '^h\ '
命令可以看到小写 h 代表本地不跟踪的文件学完本文章,你将学会
理论上,git 日常用到的命令是 diff show fetch rebase pull push checkout commit status 等,这些命令都不会导致代码丢失,假如害怕代码丢失,可以预先 commit 一次,再进行修改,但切记
不可使用自己不熟悉的命令 任何命令,不要加上-f 的强制参数,否则可能导致代码丢失
建议多使用命令行,不要使用图形界面操作
Git 基础再学习之:git checkout -- file
如何理解 git checkout -- file 和 git reset HEAD -- file
此文已由腾讯云 + 社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区 - 云加社区官方号及知乎机构号