本文由云+社区发表

作者:工程师小熊

版本控制工具——Git常用操作(上)

摘要:用了很久的Git和svn,由于总是眼高手低,没能静下心来写这些程序员日常开发最常用的知识点。现在准备开一个专题,专门来总结一下版本控制工具,让我们从git开始。完成本系列博客的阅读以后,你将掌握git的基本概念与git的基本命令,可以在本地随心所欲的完成代码的提交撤销保存修改等操作、可以流畅的参与多人协作,本文致力于快速的入门,如果涉及到更高级的功能需要进行更深一步的学习。

本文核心点:

  • Git的基本概念
  • 一个人使用Git时的代码版本控制–(提交、拉代码、分支操作)
  • 多人合作时的代码版本控制–(合并冲突、暂存代码)

什么是Git

简介

git是世界上目前最先进的分布式版本控制系统,致力于团队、个人进行项目版本管理,完美的解决难以比较代码、难以合并代码、难以取消修改、难以在写当前代码的过程中保存未完成的修改去修改线上版本的bug等的痛点。

git是一个非常强大的工具,但作为一个git使用者来说,不用完全学习Git的知识点与命令,因为有的命令的使用频率非常的低甚至数年都不会用到,让我们来由浅入深进行学习。

git的历史

git是linux的创始人linus,在付费版本控制工具BitMover收回对Linux社区免费使用权利的时候,一怒之下花费两个星期的时间写出来的。(牛笔的人)

开始

安装git

选择自己的操作系统对应的git版本安装,安装成功后运行 git version 后,输出git版本则安装正确。

git 官方: https://git-scm.com/downloads

配置用户信息

使用 git config 命令来配置用户名和邮箱

git config --global user.name "pzqu"
git config --global user.email pzqu@example.com

如果用了 –global 选项,那么更改的配置文件就是位于你用户主目录下的那个,以后你所有的项目都会默认使用这里配置的用户信息。如果要在某个特定的项目中使用其他名字或者电邮,只要去掉 –global选项重新配置即可,新的设定保存在当前项目的 .git/config 文件里。

使用 git config user.namegit config user.email 来检查是否成功,也可以直接用 git config --list 来列出全部git配置信息来查看

创建git托管的项目

假如我们创建一个项目叫make_money,先创建一个文件夹叫make_money,再使用 git init 命令创建git项目。

# pzqu @ pzqu-pc in ~/Documents/code/test [0:05:29]
$ mkdir make_money

# pzqu @ pzqu-pc in ~/Documents/code/test [0:06:24]
$ ls
make_money

# pzqu @ pzqu-pc in ~/Documents/code/test [0:06:29]
$ cd make_money

# pzqu @ pzqu-pc in ~/Documents/code/test/make_money [0:07:10]
$ git init
Initialized empty Git repository in /Users/pzqu/Documents/code/test/make_money/.git/

# pzqu @ pzqu-pc in ~/Documents/code/test/make_money on git:master o [0:07:12]
$ ls -al
total 0
drwxr-xr-x  3 pzqu  staff   96 11  7 00:07 .
drwxr-xr-x  3 pzqu  staff   96 11  7 00:06 ..
drwxr-xr-x  9 pzqu  staff  288 11  7 00:07 .git

创建成功以后,会出现一个叫.git的隐藏文件夹,这个就是你的git仓库,以后所有的git操作历史提交记录信息就全部记录在此了,只要这个文件夹在就可以记住我们的全部git操作

工作区和暂存区

在使用git的时候还要清楚暂存区和工作区的含义,参考 廖雪峰的官方网站-git篇-工作区和暂存区

常见情况

提交代码

新文件与修改

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [11:37:50]
$ ls
README.md

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [11:42:02]
$ touch file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [11:42:15]
$ git add file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [11:42:23]
$ 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:   file1.txt
# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [11:56:38]
$ git commit -m "[+]add new file1.txt"
[master 66cc488] [+]add new file1.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file1.txt

上图操作包含:

  • 创建新文件file1.txt
  • add 添加修改的内容到索引
  • status 查看修改的内容
  • commit 把索引提交到本地分支

git add .:监控工作区的状态树,此命令会把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(neW) ,但不包括被删除的文件。

git add -u:他仅监控已经被add的文件(即tracked file),他会将被修改的文件提交到暂存区。add -u 不会提交新文件(untracked file)。(git add –update的缩写)

git add -A:是上面两个功能的合集(git add –all的缩写)

git show 列出最近一次的提交

对于commit:像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

删除文件

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [12:55:24]
$ ls
README.md file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [12:55:25]
$ git rm file1.txt
rm 'file1.txt'

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [12:55:30]
$ ls
README.md

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [12:55:32]
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [12:55:40] C:128
$ git commit -m "[-]delete file1.txt"
[master e278392] [-]delete file1.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 file1.txt

上图操作包含:

  • 创建新文件file1.txt
  • git rm 删除file1.txt文件
  • status 查看修改的内容
  • commit 把索引提交到本地分支

tip1: 如果没有用git rm删除文件,在本地删除文件后,git add一下再提交可以达到同样的效果

tip2: 要是你加班太晚,头晕不小心删除了不想删除的文件怎么办?见

下一篇:版本控制工具——Git常用操作(下)-后悔药

拉代码

方法一 pull

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [17:01:13]
$ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:pzqu/git_test
   5fd4d8f..7b54a8a  master     -> origin/master
Merge made by the 'recursive' strategy.
 share_file.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 share_file.txt

上图命令:

  • git pull

查看本地仓库变化 git log

上图可以看到向远程仓库pull的时候,出现了两个新的commit, commit 7b54a8ae74... 的提交信息为 Create share_file.txt,另一个 commit fdbb19cf4c51770 的提交信息为 Merge branch 'master' of github.com:pzqu/git_test。事实上主线只有一个提交,为什么会出现这种情况? 是因为pull其实会做两个操作

  • 拉远程仓库代码到本地
  • 自动与当前分支合并并生成一个合并成功的提交

注意这里的第二个个步骤如果远程有人和你改了同一个文件就会出现一个冲突,这个时候git会提示你哪些文件有冲突,手动改了再提交一次就可以了。详情见合并冲突

方法二 fetch

我在远程修改了文件,向 share_file.txt 加了一行内容 tom modify,此时拉代码。

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [21:07:21]
$ git fetch

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [21:08:43]
$ git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: [+]add new file1.txt
Applying: [-]delete file1.txt

上图所示有以下两个操作

  • fetch 拉取远端代码到本地
  • rebase 把本地代码提交基于远端分支重新replay

效果如下:

上图是 git log 所输出的提交内容,刚刚pull的时候忘记把pull自动产生的merge提交到远程,rebase的时候把本地的提交放到了远程提交之后,看起来就是一条直线,比较优雅,也是推荐的方式。

同样的,如果产生了冲突,详情见合并冲突

分支操作

创建分支

分支是多人协同最经典的地方所在,我们来创建一个分支

$ git checkout -b dev/pzqu origin/master
Branch 'dev/pzqu' set up to track remote branch 'master' from 'origin'.
Switched to a new branch 'dev/pzqu'

$ git branch
* dev/pzqu
  master
  • git checkout -b 分支名 其他分支, -b 代表创建并切换到新建的分支, 分支名 代表新创建的分支叫什么名字,这里叫 dev/pzqu其他分支 代表基于哪一个分支来创建,这里基于远程的master分支 origin/master,如果省略则代表基于当前分支
  • git branch 展示本地的分支情况,加 -a 参数可以展示全部的分支,包括远程分支
  • * 在分支前,指明了现在所在的分支是 dev/pzqu

切换分支

$ git checkout -b dev/pzqu2
Switched to a new branch 'dev/pzqu2'

$ git branch
  dev/pzqu
* dev/pzqu2
  master

$ git checkout dev/pzqu
Switched to branch 'dev/pzqu'
Your branch is up to date with 'origin/master'.

$ git branch
* dev/pzqu
  dev/pzqu2
  master
  • 基于当前分支创建了一个新的分支并自动切换过去 dev/pzqu2
  • git checkout 已存在的分支名 切换分支回到 dev/pzqu

删除分支

$ git branch
* dev/pzqu
  dev/pzqu2
  master

$ git branch -D dev/pzqu2
Deleted branch dev/pzqu2 (was 7c9be37).

$ git branch
* dev/pzqu
  master
  • 位于 dev/pzqu,删除了 dev/pzqu2 分支

合并冲突

合并同一个分支的冲突(常见)

为了产生一个冲突,我在另一个地方向远程仓库提交了代码,更改 share_file.txt 文件,加了一行内容 tom add for merge

本地修改同一个文件加了一行 pzqu add for merge,并提交到本地,这样一来,本地和远程仓库的同一个文件就不一样了,一会拉代码一定会产生一个冲突。效果如下:

  • 一般rebase或pull冲突的时候,都会出现提示,然后git status会出现上图图示
  • 这个时候不可以进行任何分支切换和commit操作,按照他提示进行处理
  • git status提示哪个文件是都被修改的,both modified,然后使用编辑器修改该文件,解决冲突
  • 解决完成后,git add 添加该冲突文件
  • git rebase –continue,并更新commit message,完成整个rebase流程 我们来看看这个冲突的文件:

Git用 <<<<<<<=======>>>>>>> 标记出不同分支的内容,我们修改如下后保存:

upload successful

git addgit rebase --continue 后完成rebase,效果如下,再 push 的远程仓库即可

合并不同分支的代码产生冲突

关于怎么创建分支与切换分支见创建分支和切换分支,这里只讨论合并时产生的冲突的情况,我们已经基于 master 分支创建了一个 dev/pzqu 分支

$ git branch
* dev/pzqu
  master

切换到 master 分支,加一行 master add for merge 并提交,文件内容如下:

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
master add for merge

切换到 dev/pzqu 分支,向 share_file.txt 加入一行 dev/pzqu add for merge 并提交,现在 share_file.txt 内容如下:

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
dev/pzqu add for merge

现在两个分支的同一个文件内容不一样了,现在我们在 dev/pzqu 分支上进行合并:

$ git merge master
Auto-merging share_file.txt
CONFLICT (content): Merge conflict in share_file.txt
Automatic merge failed; fix conflicts and then commit the result.

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:dev/pzqu x [11:17:31] C:1
$ git status
On branch dev/pzqu
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:   share_file.txt

no changes added to commit (use "git add" and/or "git commit -a")

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
<<<<<<< HEAD
dev/pzqu add for merge
=======
master add for merge
>>>>>>> master

上图出现了一个冲突,是我们意料之中的,修改 share_file.txt 文件,解决此冲突:

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
dev/pzqu add for merge
master add for merge

$ git add share_file.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:dev/pzqu x [11:22:40]
$ git commit -m "[*]merge master to dev/pzqu"
[dev/pzqu d9e018e] [*]merge master to dev/pzqu

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:dev/pzqu o [11:23:00]
$ git status
On branch dev/pzqu
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

冲突解决也提交了,看看我们现在的分支内容:

上图我们可以看到:

  • master 分支比远程 origin/master 分支多一次提交, dev/pzqu 分支由于是基于 origin/master 分支,合并了 master 分支的提交和当前 dev/pzqu 分支的提交,超出本地 master 两个提交,致此我们把 master 合并到 dev/pzqu 的操作就完成了。
  • 通常我们开一个新的开发分支是为了在自己的分支上写代码,方便提交也不会把主线弄乱,现在我们用同样的方法将 dev/pzqu 合并到 master 分支,然后把两个分支都提交到远程。
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

$ git merge dev/pzqu
Updating 58f047a..d9e018e
Fast-forward
 share_file.txt | 1 +
 1 file changed, 1 insertion(+)

$ git push origin master
Total 0 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
   7c9be37..d9e018e  master -> master

$ git push origin dev/pzqu
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 887 bytes | 887.00 KiB/s, done.
Total 9 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), done.
remote:
remote: Create a pull request for 'dev/pzqu' on GitHub by visiting:
remote:      https://github.com/pzqu/git_test/pull/new/dev/pzqu
remote:
To github.com:pzqu/git_test.git
 * [new branch]      dev/pzqu -> dev/pzqu
  • 切换到 master 分支
  • 合并 dev/pzqumaster 分支
  • master 推到远程仓库
  • 如果 dev/pzqu 要保留,就可以推送到远程仓库。

  • 现在我们可以看到全部的分支都在一起了,强迫症都舒服了。

暂存代码保存现场

这种情况一般是出现在你正在完成一个功能,但是忽然线上发现了一个Bug,必须马上开一个新的分支来修复bug,但是现在的功能没写完不打算提交(commit),现在怎么办??不用怕暂存代码来帮助你。

$ git status
On branch dev/pzqu
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   need_stash.txt
    modified:   share_file.txt

$ git stash
Saved working directory and index state WIP on dev/pzqu: d9e018e [*]merge master to dev/pzqu

$ git stash list
stash@{0}: WIP on dev/pzqu: d9e018e [*]merge master to dev/pzqu

$ git status
On branch dev/pzqu
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

//省略操作:去创建一个Bug分支,修复他并完成与主线的合并,删除Bug分支。
//省略操作:切回来当前分支继续开发
//下面来恢复现场

$ git stash apply stash@{0}
On branch dev/pzqu
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   need_stash.txt

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:   share_file.txt
  • status 查看到有2个文件修改没有提交
  • stash 把修改放到暂存区,并生成一个id
  • stash list 列出暂存区所有内容
  • stash apply 重新把暂存区内容放到本地

这里的 stash apply 成功的把暂存区的一次暂存恢复到了本地,但是暂存区还有会保存这次暂存,如果想删除这次暂存要用 git stash drop 来删除;也可以用 git stash pop,恢复最后一次暂存的同时把stash内容也删了。

$ git stash drop stash@{0}
Dropped stash@{0} (bfdc065df8adc44c8b69fa6826e75c5991e6cad0)

$ git stash list

好了,暂存区清干净了。

​ 注意:要放到暂存区的文件一定要先通过git add加到index

小结

本文阅读结束以后,我们学会了

  • Git的基本概念,知道git的作用、历史;学会安装配置Git,使用Git创建项目托管以及工作区和暂存区的概念
  • 学会Git的本地操作,提交、拉代码、创建切换删除分支操作,
  • 多人合作时的代码版本控制,学会了不同情况下的合并冲突、暂存代码操作

下集预告

Git常用操作(下)我计划给大家介绍以下点:

  • 后悔药-各种后悔操作(撤消commit,回滚,回退远程仓库等)
  • 哎呀,提交的时候漏了文件
  • tag操作
  • git忽略不想提交的文件

注意事项

理论上,git日常用到的命令是 diff show fetch rebase pull push checkout commit status 等,这些命令都不会导致代码丢失,假如害怕代码丢失,可以预先commit一次,再进行修改,但切记

不可使用自己不熟悉的命令 任何命令,不要加上-f的强制参数,否则可能导致代码丢失

建议多使用命令行,不要使用图形界面操作

版本控制工具——Git常用操作(下)

本文由云+社区发表

作者:工程师小熊

摘要:上一集我们一起入门学习了git的基本概念和git常用的操作,包括提交和同步代码、使用分支、出现代码冲突的解决办法、紧急保存现场和恢复现场的操作。学会以后已经足够我们使用Git参加协作开发了,但是在开发的过程中难免会出错,本文主要介绍版本控制的过程中出错了的场景,以及Git开发的一些技巧,让我们用的更流畅。

上集回顾:

  • Git的基本概念
  • 一个人使用Git时的代码版本控制–(提交、拉代码、分支操作)
  • 多人合作时的代码版本控制–(合并冲突、暂存代码)

本文核心:

  • 后悔药-各种后悔操作(撤消commit,回滚,回退远程仓库等)
  • 哎呀,提交的时候漏了文件
  • tag操作
  • git忽略不想提交的文件

后悔药

撤消当前commit

如果你发现刚刚的操作一不小心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
  • 创建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^,这样你本地修改就彻底丢掉了(慎用),如果真用了想找回来怎么办?见救命的后悔药。

当然了,你只要开心不加 softhard 参数也是安全的(相当于使用了 --mixed 参数),只不过是撤消以后你的本次修改就会回到 add 之前的状态,你可以重新检视然后再做修改和 commit

回退远程仓库

要是我们做的更过分一点,直接把这次 commit 直接给 push 怎么办?要是被发现就全完了,我们来看看github上的远程仓库。

完了,真的提交了(我刚刚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

真的撤消了远程仓库,长舒一口气。

暂存区(Stage)到工作区(Working Directory)

如果我们刚刚执行了 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.logmysql.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被改变了

我们来把time.txt恢复到版本1,同样的方法,因为版本1是上一次提交我们可以省略掉版本号

$ git checkout -- time.txt

$ cat time.txt
00:50

看到了吧!只要用 git checkout commit_id -- filename 的组合,想搞出哪个文件历史版本就搞出哪个。

到了这里,你可能会很懵比, resetcheckout 命令真的好像啊!都可以用来做撤消

  • checkout 语义上是把什么东西取出来,所以此命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。
  • reset 语义上是重新设置,所以此命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。

还想不通可以给我发邮件:pzqu@qq.com

救命的后悔药

来到这里我已经很清楚的你的现况了,你的代码丢了现在一定非常的着急,不要慌,总是有办法找回他们的。但是前提是要保证你的项目根目录下.git文件夹是完整的,要是手动删除了里面的一些东西那就真完了。还要保证一点,你的代码以前是有过git追踪的,最少 add

找回你丢失的历史记录

Git提供了一个命令 git reflog 用来记录你的每一次命令,贴个图吧直观点:

  • 有没有发现, git reflog 里的全部都是和改变目录树有关的,比如 commit rebase reset merge,也就是说一定要有改变目录树的操作才恢复的回来
  • 像add这种操作就不能恢复了吗?那肯定不是,只是要用更麻烦点的方式来恢复
  • 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)版本。
  • 我们用 statusls 再看,这个叫 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 等类型的。他们是什么含义呢?

  • 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.txt
  • git 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
  • stage里还有新的修改在
  • 使用 git commit --amend --no-edit 合并到上一个提交里,如果不加 --no-edit 参数的话,会提示你来修改commit提示信息(这个命令也可以用在重复编辑 commit message)。
  • 查看日志,合并提交成功!

tag标签

创建一个tag

标签是一个类似于快照的东西,常常用于测试和发布版本。所以我们常常把 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.0tag
  • 创建的不带有 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.0tag
  • 远程删除名为 release_v1.0tag

对历史提交打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远程仓库里有这些文件,就像通用的配置文件,我们必须要在本地修改配置来适应运行环境,这种情况下我们不想每次提交的时候都去跟踪这些文件。

忽略自动生成的垃圾文件、中间文件、敏感信息文件

忽略文件的原则是:

  1. 忽略操作系统自动生成的文件,比如缩略图等;
  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

我们要怎么做呢?

在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.logtest2.log* status 查看,真是工作区是 clean,新创建的文件没有被跟踪

忽略远程存在,本地不想与远程同步的文件

添加跟踪忽略

核心命令:

git update-index —assume-unchanged 文件名