Git基本概念
Git与传统的版本控制软件不同,它保存的不是文件之间的diff,而是文件的snapshot,文件的snapshot组成了不同的版本:
Git管理的文件有三大状态:Commited,Modified和Staged.
- Commited: 数据保存在本地数据库了。
- Modified:文件有改动,但还没存数据库。
- Staged: 文件有改动,而且标记了会进入下次commit的snapshot。
根据文件的三个状态,Git维护了下面三个组件Working Directory, Staging Area, 和 Git Directory:
基本上所有的命令都是在操作管理这三个组件:
- Working Directory:当前从.git拉出来的版本,是实实在在的可以编辑的文件。
- Staging Area(又称作Index): 通常保存在.git目录下,保存了进入下一commit的信息。
- Git Directory: .git目录,保存了项目所有信息,clone的时候就是在复制这个目录。
这三个概念很重要,下面结束reset的时候对理解很有帮助
Git基本操作
Working Directory下的文件有两种可能的状态:tracked和untracked。tracked的文件是之前snapshot里就有的文件,untracked的文件是即不在之前版本里存在,也不在Staging Area的文件。下面是一个文件的所有可能的生命周期:
git status
: 查看项目文件状态。git status -s
: 同上,不过是简略版。git add filename
: track新文件,或者Stage修改过的文件。git commit -m "message here"
: commit。git rm filename
: 删除文件,相当于rm filename & git add filename
。git rm --cached filename
: 从Staging Area删除文件,但保存硬盘上的文件。git mv file_from file_to
: 移动文件。
Log
git log
: 查看日志。git log -p -2
: 查看带上diff的最近两个commit的日志。git log --stat
: 简化版的日志,显示文件变更的status。git log --pretty=oneline
: 究极简化版日志,每个commit一行。git log --graph
: 美化版日志,带分支图的那种。git log --since=2.weeks
: 最近两周的日志。git log --Sfunction_name
: 搜索引入或删除了function_name
的commit。
Undo
git commit --amend
: 把Staging Area的改动合入上次的commit;会弹出一个编辑器让你修改commit信息,你也可以不改。git reset HEAD filename
: Unstage文件。git checkout -- filename
: 恢复文件到上次commit时的状态,注意这个是危险命令,不能恢复回来了。原则上只要commit了,git就能恢复回来,没commit的话没了就没了。
Remote
git clone会默认添加一个叫origin的remote,origin没有什么特别之处,只是个名字而已,和默认的master分支一样,master也和其他分支本质上没什么区别。
git remote -v
: 显示remote的信息。git remote add cg https://someurl
: 添加一个叫cg的remote。git remote show cg
: 显示remote cg的详细信息。git fetch cg
: 从cg拉取信息。git fetch --all
: 拉取所有分支信息。git push cg branch
: 更新remotecg的分支branch。git remote rename cg gc
: 重命名remote。git remote rm cg
: 删除remote。
Git分支
Git分支本质上就是个指向commit object的指针(实际就是个保存了commit hash的41byte文件)。那Git怎么知道你当前在哪个分支上呢?Git保存了一个特殊的叫做HEAD
的指针,它指向的永远是你本地当前所在的分支。
- 假设当前在master分支。
git branch testing
会新建一个叫testing分支,但不会切过去:
- 要切过去,要用checkout:
git checkout testing
,这个命令干了两件事儿,第一个是更新HEAD指向,二是更新Working Directory下的文件。
- 如果这时你在testing上commit了,HEAD也会跟着更新:
- 再切回master:
git checkout master
:
5.如果再在master上commit,这时这两个分支就diverged了,下面讲merge的时候会涉及这个diverged的概念:
git checkout -b testing
相当于git branch testing
+git checkout testing
。
远程分支也没啥特别的,就是个指针,只不过不能随意改动,通常只能通过push命令改变。拉取远程分支信息用fetch就行了。如果想让本地分支跟踪远程分支,可以用下面几个命令:
git checkout -b cg origin/testing
: 新建本地分支cg,并跟踪远程分支testing(从origin/testing指向的地方开始开发)。git checkout -t origin/testing
: 功能同上,只不过本地分支名字默认和远程分支名一样(这个例子中是testing)。git branch -u origin/testing
: 手动设置本地已有分支跟踪远程分支testing。
想查看各个分支跟踪信息可以用git branch -vv
:
Merge
Git有两种可能的merge方式。
- 第一种
fast forward
,当两个分支没有diverge的时候会fast forward,假如下图是当前分支的状态:
如果在master上运行git merge hotfix
,master会直接做个简单的向前移动:
- 第二种
three-way merge
,当两个分支已经diverge了,就会进行three-way merge,假如下图是当前分支的状态:
如果再master上运行git merge iss53
,Git会把两个分支的snapshot(C4,C5)外加公共祖先的snapshot(C2)三者一起merge出一个merge commit(c6),注意C6有两个父节点(C4,C5)。
如果你不想merge的时候出现一个多余的merge commit,就要需要用另外一个工具:rebase。但是rebase有点容易把自己操作迷了,不推荐新手使用,这篇入门文章就不介绍了。
Git常用工具
RefLog
reflog保存了近几个月HEAD所有的历史记录:
- 查看某个reflog:
git show HEAD@{5}
。 - 查看昨天master分支在哪:
git show master@{yesterday}
。
这个结合reset可以很方便我们恢复到某个状态,比如你一顿操作后状态乱了不知道咋恢复了,可以先查看reflog,然后git reset HEAD@{N}
。
Ancestry References
Git有很多方便的引用,比如:
HEAD^
:HEAD的父节点,可以直接这样用:git show HEAD^
。d921970^
: d921970的父节点,其中d921970是某个commit。HEAD^2
: HEAD的另外一个父节点,一般只有merge commit才有多个父节点。HEAD~
: 也是HEAD的父节点,只不过HEAD~2
是父节点的父节点,也就是祖父节点。HEAD^^
和HEAD~2
是一样的。^
和~
可以一起用,比如HEAD~3^2
。
Stashing
有时你开发的很开心,突然测试啪一个问题单过来,得,需要切分支修bug,但又不想commit,咋办咧,就可以用stash把所有的变动存起来,修完bug又可以应用stash恢复之前的状态(类似把所有的改动复制出去保存着,完事儿在复制回来):
git stash
git stash list
git stash apply
, 等同于git stash apply stash@{0}
git stash
默认不会stage之前stage过的文件(全都是unstaged),如果要完全恢复状态需要加--index
- 如果你不想stash用git add过的文件,可以用
git stash --keep-index
- 如果你连untracked的文件也要stash,可以用
git stash -u
Cleaning
git clean -f -d
: 删除所有untracked的文件(危险命令,不可恢复)。git clean -n
: 预先显示要删除的文件。git clean -x
: 连.gitignore里ignore的文件也删了。
RESET!!
还记得三大组件吗,这时候就有用了。先看图:
- 空项目,刚刚git init,Working Directory有一个文件file.txt,Index和HEAD都为空,这时
git status
会在Untracked files里有个红色的文件,untracked显示的就是Index和Working Directory的区别:
- 接下来
git add
会把Working Directory复制到Index里,这时git status
会在Changes to be committed里有个绿色的文件,Changes to be committed显示的就是Index和HEAD的区别:
- 接下来
git commit
会把Index永久保存到一个snapshot里,并创建一个commit object指向那个snapshot,最后更新master指向那个commit,此时三大组件都是一样的了,git status
里就是空的了:
- 如果我们修改file.txt并commit,做两次:
git reset
做的就是改变HEAD指向的commit,这和改变HEAD本身不一样,那是checkout干的事儿;比如如果你在master上,git reset 9e5e64a
会让master指向9e5e64a。下面是git reset --soft HEAD~
之后的状态:
基本上就是undo了最后一次git commit
。无论git reset加了什么参数,改变HEAD永远是它干的第一件事儿,如果参数是--soft
的话就到此结束了,此时git status
会有一个绿色的文件,它显示的就是Index和HEAD的区别。
如果git reset HEAD^
不加参数,那默认就是git reset --mixed HEAD
,它会把Index也更新到和HEAD一样:
翻译一下就是把文件unstage了。
你应该猜到还有一个参数把Working Directory也回滚了,是的它就是--hard
:
注意reset命令只有加了--hard
才是危险操作,如果v3的file.txt没有commit的话,它就永远丢失了,这里v3 commit过了我们可以用reflog来恢复。
开发日常
场景1,测试提单要切分支修bug了:
- 先stash:
git stash
- 切分支:
git checkout some_other_branch
,然后修bug git add . & git commit -m "some awesome message"
- 切回来并应用stash:
git checkout some_original_branch & git stash apply
场景2,MR里有审核意见,要改代码了,但不想新建新的commit:
- 改代码,然后
git add . & git commit --amend --no-edit
- push到你的fork仓:
git push -f fork
这个只能改上个commit,想改多个commit,请搜索git rebase
场景3,有些需要ignore的文件但没写在.gitignore文件里(比如junit下的配置)
可以修改私有的ignore文件,路径是.git/info/exclude
场景4,有些commit信息要改,但是好几个commit之前了,而且已经push了
git diff origin/dev_6.10.1 dev_6.10.1 > ../patchfile
git checkout -t dev_6.10.1_ok
git apply ../patchfile
git commit 'some_new_message'
这样相当于把MR里的代码提出来合成一个commit。
场景5,还有就是
你要是commit了,别没事pull,你pull一下就是个merge commit,要是没事儿还好,有事儿这玩意改起来麻烦的很。
OK
Git日常使用的话这些内容足够了,欢迎更新/指正。