赞
踩
git 教程:https://www.liaoxuefeng.com/wiki/896043488029600/897013573512192
Git官网、pro git、Git manual:https://git-scm.com/
小猪的Git使用总结:https://blog.csdn.net/coder_pig/article/details/54346867
菜鸟教程:git 教程:https://www.runoob.com/git/git-tutorial.html
图解git原理与日常实用指南:https://juejin.cn/post/6844903782451511303
Git 常用命令速查表(图文+表格):https://www.cnblogs.com/kenshinobiy/p/4543976.html
GitHub 秘籍:https://github.com/tiimgreen/github-cheat-sheet/blob/master/README.zh-cn.md
Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了。Linus 虽然创建了Linux,但Linux的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为Linux编写代码,那Linux的代码是如何管理的呢?事实是,在2002年以前,世界各地的志愿者把源代码文件通过diff的方式发给Linus,然后由Linus本人通过手工方式合并代码!你也许会想,为什么Linus不把Linux代码放到版本控制系统里呢?不是有CVS、SVN这些免费的版本控制系统吗?因为Linus坚定地反对CVS和SVN,这些集中式的版本控制系统不但速度慢,而且必须联网才能使用。有一些商用的版本控制系统,虽然比CVS、SVN好用,但那是付费的,和Linux的开源精神不符。不过,到了2002年,Linux系统已经发展了十年了,代码库之大让Linus很难继续通过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满,于是Linus选择了一个商业的版本控制系统BitKeeper,BitKeeper的东家BitMover公司出于人道主义精神,授权Linux社区免费使用这个版本控制系统。安定团结的大好局面在2005年就被打破了,原因是Linux社区牛人聚集,不免沾染了一些梁山好汉的江湖习气。开发Samba的Andrew试图破解BitKeeper的协议(这么干的其实也不只他一个),被 BitMover 公司发现了(监控工作做得不错!),于是BitMover公司怒了,要收回Linux社区的免费使用权。Linus可以向BitMover公司道个歉,保证以后严格管教弟兄们,不过这是不可能的。实际情况是这样的:Linus花了两周时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!牛就是这么定义的,大家可以体会一下。然后 Git 迅速成为最流行的分布式版本控制系统,尤其是2008年,GitHub网站上线了,它为开源项目免费提供Git存储,无数开源项目开始迁移至GitHub。
集中式 版本控制:集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。
分布式版本控制:没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
图 2
Git 和 SVN 思想最大的差别有四个:
Git 是分布式的版本控制系统,客户端不只是提取最新版本的快照,而且将整个代码仓库镜像复制下来。如果任何协同工作用的服务器发生故障了,也可以用任何一个代码仓库来恢复。而且在协作服务器宕机期间,你也可以提交代码到本地仓库,当协作服务器正常工作后,你再将本地仓库同步到远程仓库。SVN是集中化版本控制系统,集中化版本控制系统如果中央服务器宕机则会影响数据和协同开发。
能够对文件版本控制和多人协作开发
拥有强大的分支特性,所以能够灵活地以不同的工作流协同开发
分布式版本控制系统,即使协作服务器宕机,也能继续提交代码或文件到本地仓库,当协作服务器恢复正常工作时,再将本地仓库同步到远程仓库。
当团队中某个成员完成某个功能时,通过pull request操作来通知其他团队成员,其他团队成员能够review code后再合并代码。
首先祭出原网址:https://git-scm.com/download/win 不出意外的话,卡的相当蛋疼。。。
国内镜像的速度还是可以的,几乎实时更新
Git 作为一个复杂的版本控制系统,命令很多,即使经常使用,一些命令也记不住,所以要借助一个图形化的软件,TortoiseGit 就是这样一个软件来操作 Git。
下载:https://tortoisegit.org/download/
下载 TortoiseGit 及中文语言包,每个语言包的大小为 2-7 MB。
TortoiseGit 小乌龟下载安装教程(保姆级):https://zhuanlan.zhihu.com/p/669999994
图中左侧为工作区,右侧为版本库。在版本库中标记为 "index" 的区域是 "暂存区(stage/index)或者 缓存区",标记为 "master" 的是 master 分支所代表的目录树。
图中我们可以看出此时 "HEAD" 实际是指向 master 分支的一个"游标"。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。
图中的 objects 标识的区域为 Git 的对象库,实际位于 ".git/objects" 目录下,里面包含了创建的各种对象及内容。
当对工作区修改(或新增)的文件执行 "git add" 命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。
当执行提交操作(git commit)时,暂存区的目录树写到版本库(对象库)中,master 分支会做相应的更新。即 master 指向的目录树就是提交时暂存区的目录树。
当执行 "git reset HEAD" 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。
当执行 "git rm --cached <file>" 命令时,会直接从暂存区删除文件,工作区则不做出改变。
当执行 "git checkout ." 或者 "git checkout -- <file>" 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。
当执行 "git checkout HEAD ." 或者 "git checkout HEAD <file>" 命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。
图 1
图 2
图 3
图 4
流程简述:
按大类划分,分为两种状态:Tracked(已跟踪)和Untracked(未跟踪),
依据是:该文件是否已加入版本控制?
假设某个项目已加入版本控制系统
- 1.新建一个文件,该文件处于 Untracked 状态;
- 2.通过 git add 命令添加到缓存区,此时文件处于 Tracked 状态又或者说
此时这个文件已经被版本控制系统所跟踪,而且他处于Staged(暂存)状态;- 3.通过git commit命令把暂存区的文件提交提交到本地仓库,此时文件
处于Unmodified(未修改)状态;- 4.此时如果去编辑这个文件,文件又会变成Modified(修改)状态;
用法: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
[--config-env=<name>=<envvar>] <command> [<args>]常用 git 命令。查看对应命令帮助。示例:git clone --help
开始一个新的工作空间 (更多帮助:git help tutorial)
clone 克隆一个仓库到新的目录
init 创建一个空的Git存储库,或重新初始化一个现有的Git存储库处理当前的变化 (更多帮助: git help everyday)
add 将文件内容添加到 index(索引)
mv 移动或者重命名 "文件, 目录, 符号链接"
restore Restore working tree files
rm 同时从工作区和暂存区(缓存区) 删除文件查看提交历史和仓库状态 (参看: git help revisions)
bisect 使用二进制搜索来查找引入bug的提交
diff 显示两次提交之间的变化
grep 打印匹配到的行
log 显示提交日志
show 展示各种类型的对象
status 显示状态"成长、标记、调整" 分支
branch 列出、创建、删除 分支
commit 提交到本地仓库
merge 将两个或多个分支合并到本地当前分支
rebase Reapply commits on top of another base tip
reset 将当前HEAD重置为指定状态
switch 切换分支
tag 创建、列出、删除或验证使用GPG签名的tag合作 (参看: git help workflows)
fetch Download objects and refs from another repository
pull Fetch from and integrate with another repository or a local branch
push Update remote refs along with associated objects'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.
everything-is-local
Git 常用6个命令:git clone、git push、git add 、git commit、git checkout、git pull
由于一些众所周知的原因,github上传代码的默认分支由master变为了main。
但本地分支仍旧为 master,这就导致上传之后仓库有两个分支,还得手动合并,很麻烦。
不如直接把本地的分支也修改为main。
git checkout -b main 新建main分支,并转至main分支
git merge master 合并两个分支
然后就可以进行add,commit
将 本地main分支 的推送至 远程叫origin的分支:git push -u origin main
Git 不会跟踪或添加空目录,因为 Git 只关注文件的变化,而不关注目录本身。如果尝试使用 git add
命令来添加一个空目录,它将被忽略,提交时也不会包含在版本控制中。但是,可以在空目录下创建一个占位文件(如 .gitkeep
或 .gitignore
),然后将该文件添加到 Git 中。这将使空目录保留在仓库中,并且可以进行提交、克隆等操作。
git init 初始化仓库。
git clone 拷贝一份远程仓库。
git add . 添加文件到暂存区。
git commit 将暂存区内容添加到仓库中。git add 添加文件到暂存区
git status 查看仓库当前的状态,显示有变更的文件。
git diff 比较文件的不同,即暂存区和工作区的差异。
git commit 提交暂存区到本地仓库。
git commit -am "" 直接提交全部修改,相当于 add 和 commit 一起执行了。
git reset 回退版本。reset 是替换整个目录树,多余的文件将被删除。
git checkout
git rm 将文件从暂存区和工作区中删除。
使用 --cached 只从暂存区中删除。
使用 –rf <directory> 可删除指定目录下的所有文件和子目录。
git mv 在工作区和暂存区中进行移动或重命名。若 <destination> 不为一个目录名,则执行重命名。
如果为一个目录名,则执行移动。
git checkout 分支切换。对多余的文件保留不做任何处理。
git switch 更清晰地切换分支。(Git 2.23 版本引入)
git restore 恢复或撤销文件的更改。(Git 2.23 版本引入)
git log 查看历史提交记录
git blame <file> 以列表形式查看指定文件的历史修改记录
git remote 远程仓库操作
git fetch 从远程获取最新版本到本地,不会自动merge
git merge 从指定的commit(s)合并到当前分支,用来合并两个分支;
git pull 下载远程代码,并合并到本地
git push 上传并合并到远程代码
git pull 相当于 git fetch + git mergegit diff:查看工作区与暂存区的不同。
git diff –cached [<commit>]:查看暂存区与指定提交版本的不同,版本可缺省(为HEAD)。
git diff <commit>:查看工作区与指定提交版本的不同。
git diff <commit>..<commit>:查看2个指定提交版本的不同,其中任一可缺省(为HEAD)。
git diff <commit>...<commit>:查看2个不同分支指定提交版本的不同,其中任一可缺省(为HEAD),该命令相当于git diff $(git-merge-base A B) B。
git init # 初始化本地git仓库(创建新仓库)
git config --global user.name "xxx" # 配置用户名
git config --global user.email "xxx@xxx.com" # 配置邮件
git config --global color.ui true # git status等命令自动着色
git config --global color.status auto
git config --global color.diff auto
git config --global color.branch auto
git config --global color.interactive auto
git config --global --unset http.proxy # remove proxy configuration on git
git clone git+ssh://git@192.168.53.168/VT.git # clone远程仓库
git status # 查看当前版本状态(是否修改)
git add xyz # 添加xyz文件至index
git add . # 增加当前子目录下所有更改过的文件至index
git commit -m 'xxx' # 提交
git commit --amend -m 'xxx' # 合并上一次提交(用于反复修改)
git commit -am 'xxx' # 将add和commit合为一步
git rm xxx # 删除index中的文件
git rm -r * # 递归删除
git log # 显示提交日志
git log -1 # 显示1行日志 -n为n行
git log -5
git log --stat # 显示提交日志及相关变动文件
git log -p -m
git show dfb02e6e4f2f7b573337763e5c0013802e392818 # 显示某个提交的详细内容
git show dfb02 # 可只用commitid的前几位
git show HEAD # 显示HEAD提交日志
git show HEAD^ # 显示HEAD的父(上一个版本)的提交日志 ^^为上两个版本 ^5为上5个版本
git tag # 显示已存在的tag
git tag -a v2.0 -m 'xxx' # 增加v2.0的tag
git show v2.0 # 显示v2.0的日志及详细内容
git log v2.0 # 显示v2.0的日志
git diff # 显示所有未添加至index的变更
git diff --cached # 显示所有已添加index但还未commit的变更
git diff HEAD^ # 比较与上一个版本的差异
git diff HEAD -- ./lib # 比较与HEAD版本lib目录的差异
git diff origin/master..master # 比较远程分支master上有本地分支master上没有的
git diff origin/master..master --stat # 只显示差异的文件,不显示具体内容
git remote add origin git+ssh://git@192.168.53.168/VT.git # 增加远程定义(用于push/pull/fetch)
git branch # 显示本地分支
git branch --contains 50089 # 显示包含提交50089的分支
git branch -a # 显示所有分支
git branch -r # 显示所有原创分支
git branch --merged # 显示所有已合并到当前分支的分支
git branch --no-merged # 显示所有未合并到当前分支的分支
git branch -m master master_copy # 本地分支改名
git checkout -b master_copy # 从当前分支创建新分支master_copy并检出
git checkout -b master master_copy # 上面的完整版
git checkout features/performance # 检出已存在的features/performance分支
git checkout --track hotfixes/BJVEP933 # 检出远程分支hotfixes/BJVEP933并创建本地跟踪分支
git checkout v2.0 # 检出版本v2.0
git checkout -b devel origin/develop # 从远程分支develop创建新本地分支devel并检出
git checkout -- README # 检出head版本的README文件(可用于修改错误回退)
git merge origin/master # 合并远程master分支至当前分支
git cherry-pick ff44785404a8e # 合并提交ff44785404a8e的修改
git push origin master # 将当前分支push到远程master分支
git push origin :hotfixes/BJVEP933 # 删除远程仓库的hotfixes/BJVEP933分支
git push --tags # 把所有tag推送到远程仓库
git fetch # 获取所有远程分支(不更新本地分支,另需merge)
git fetch --prune # 获取所有原创分支并清除服务器上已删掉的分支
git pull origin master # 获取远程分支master并merge到当前分支
git mv README README2 # 重命名文件README为README2
git reset --hard HEAD # 将当前版本重置为HEAD(通常用于merge失败回退)
git rebase
git branch -d hotfixes/BJVEP933 # 删除分支hotfixes/BJVEP933(本分支修改已合并到其他分支)
git branch -D hotfixes/BJVEP933 # 强制删除分支hotfixes/BJVEP933
git ls-files # 列出git index包含的文件
git show-branch # 图示当前分支历史
git show-branch --all # 图示所有分支历史
git whatchanged # 显示提交历史对应的文件修改
git revert dfb02e6e4f2f7b573337763e5c0013802e392818 # 撤销提交dfb02e6e4f2f7b573337763e5c0013802e392818
git ls-tree HEAD # 内部命令:显示某个git对象
git rev-parse v2.0 # 内部命令:显示某个ref对于的SHA1 HASH
git reflog # 显示所有提交,包括孤立节点
git show HEAD@{5}
git show master@{yesterday} # 显示master分支昨天的状态
git log --pretty=format:'%h %s' --graph # 图示提交日志
git show HEAD~3
git show -s --pretty=raw 2be7fcb476
git stash # 暂存当前修改,将所有至为HEAD状态
git stash list # 查看所有暂存
git stash show -p stash@{0} # 参考第一次暂存
git stash apply stash@{0} # 应用第一次暂存
git grep "delete from" # 文件中搜索文本“delete from”
git grep -e '#define' --and -e SORT_DIRENT
git gc
git fsck
git 详解及实用指南之一 (本地操作):https://segmentfault.com/a/1190000016720411
git 详解及实用指南之二 (远程操作):https://segmentfault.com/a/1190000016737370
git 详解及实用指南之三 (分支管理):https://segmentfault.com/a/1190000016755475
git 详解及实用指南之四 (标签管理):https://segmentfault.com/a/1190000016790744
每个仓库的Git配置文件都放在.git/config
文件中:
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:michaelliao/learngit.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[alias]
last = log -1
别名就在[alias]
后面,要删除别名,直接把对应的行删掉即可。
当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig
中:
- $ cat .gitconfig
- [alias]
- co = checkout
- ci = commit
- br = branch
- st = status
- [user]
- name = Your Name
- email = your@email.com
配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。
git config --list 显示当前的Git配置
git config -e [--global] 编辑Git配置文件# 设置提交代码时的用户信息
git config --global user.name "[name]"
git config --global user.email "[email address]"git archive 生成一个可供发布的压缩包
git config --global core.autocrlf false 设置换行符为LF
git config --global core.safecrlf true 拒绝提交包含混合换行符的文件
安装完成后,还需要最后一步设置,在命令行输入:
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"因为Git是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和Email地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都是善良无知的群众,其次,真的有冒充的也是有办法可查的。
注意
git config
命令的--global
参数,用了这个参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。
有没有经常敲错命令?比如
git status
?status
这个单词真心不好记。如果敲git st
就表示git status
那就简单多了,当然这种偷懒的办法我们是极力赞成的。我们只需要敲一行命令,告诉Git,以后st
就表示status
:$ git config --global alias.st status好了,现在敲
git st
看看效果。当然还有别的命令可以简写,很多人都用
co
表示checkout
,ci
表示commit
,br
表示branch
:$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch以后提交就可以简写成:$ git ci -m "bala bala bala..."
--global
参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。配置一个
unstage
别名:$ git config --global alias.unstage 'reset HEAD' ,当你敲入命令 git unstage test.py 实际上Git执行的是 git reset HEAD test.py配置一个
git last
,让其显示最后一次提交信息:git config --global alias.last 'log -1'这样,用
git last
就能显示最近一次的提交:git last甚至还有人丧心病狂地把
lg
配置成了:git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"来看看
git lg
的效果:为什么不早点告诉我?别激动,咱不是为了多记几个英文单词嘛!
有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status
都会显示Untracked files ...
,有强迫症的童鞋心里肯定不爽。好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore
文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
不需要从头写.gitignore
文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore
忽略文件的原则是:
.class
文件;举个例子:
假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini
文件,因此你需要忽略Windows自动生成的垃圾文件,然后,继续忽略Python编译产生的.pyc
、.pyo
、dist
等文件或目录,加上你自己定义的文件,最终得到一个完整的.gitignore
文件,内容如下:
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build# My configurations:
db.ini
deploy_key_rsa
最后一步就是把 .gitignore
也提交到Git,就完成了!当然检验.gitignore
的标准是git status
命令是不是说working directory clean
。有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore
忽略了:
$ git add App.class
The following paths are ignored by one of your .gitignore files:
App.class
Use -f if you really want to add them.
如果你确实想添加该文件,可以用-f
强制添加到Git:$ git add -f App.class,或者可能是.gitignore
写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore
命令检查:$ git check-ignore -v App.class
小结:
设置代理
git config --global http.proxy 'http://proxy.server.com:8080'
git config --global https.proxy 'https://proxy.server.com:8080'
删除代理
git config --global --unset http.proxy
git config --global --unset https.proxy
如果只是临时需要用代理,则可以省略 --global 参数,这样代理设置只对当前仓库有效。
如果 git 已经接管了文件或者文件夹,则上面会显示git的状态(对钩或红色感叹号),如果不显示,解决方法为:win+r,输入regedit,找到以下路径:HKEY_LOCAL_MACHINE–>SOFTWARE–>Microsoft–>Windows–>CurrentVersion–>Explorer–>ShellIconOverlayIdentifiers
将Tortoise开头的几个文件夹重命名,在前面敲几个空格,使得这几个文件夹名称的前面空格最多
重启资源管理器。打开任务管理器,找到Windows资源管理器,重新启动。查看图标,已经可以正常显示 git 状态
git init 初始化当前目录为Git代码库,会在当前目录下创建 .git 文件夹
git init [project-name] 新建一个目录,并将其初始化为Git代码库
git clone [url] 下载一个项目和它的整个代码历史
Git 的版本库里存了很多东西,其中最重要的就是称为 "stage/index 的暂存区",还有 Git 自动创建的第一个分支master
,以及指向 master
的一个指针叫 HEAD
。
把文件添加到 "Git版本库的本地仓库" 是分两步执行的:
俗话说,实践出真知。
创建 readme.txt 和 LICENSE 文件,使用 git add readme.txt LICENSE 后,用 git status 查看:
所以,
git add
命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit
就可以一次性把暂存区的所有修改提交到分支。一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是 "干净" 的。执行 git status 查看,现在版本库变成了这样,暂存区就没有任何内容了:
暂存区 是 Git 非常重要的概念,弄明白了暂存区,就弄明白了Git的很多操作到底干了什么。
git add 就是告诉 Git,把文件添加到 缓存区:
git add [file1] [file2] ... 添加指定文件到暂存区
git add [dir] 添加指定目录到暂存区,包括子目录
git add . 添加当前目录的所有文件到暂存区
git add -p # -p,--patch 以交互方式,对于同一个文件的多处变化,可以实现分次提交
git rm [file1] [file2] ... 删除工作区文件,并且将这次删除放入暂存区
git rm --cached [file] 停止追踪指定文件,但该文件会保留在工作区
git mv [file-original] [file-renamed] 改名文件,并且将这个改名放入暂存区命令 git rm 用于删除一个文件。如果一个文件已经被提交到版本库,那么永远不用担心误删
git mv 命令用于移动或重命名一个文件、目录。
git mv <file>
git mv <old name> <new name>
其实,运行 git mv 就相当于运行了下面三条命令:
mv README.md README
git rm README.md
git add README
git status 查看仓库的当前状态,随时掌握工作区的状态。
git diff 顾名思义就是查看 difference。示例:git diff test.txt 查看更改内容
git log 查看提交的历史记录
git log --pretty=oneline
git commit -m [message] 提交暂存区到仓库区
git commit [file1] [file2] ... -m [message] 提交暂存区的指定文件到仓库区
git commit -a 提交工作区自上次commit之后的变化,直接到仓库区
git commit -v 提交时显示所有diff信息使用一次新的commit,替代上一次提交
如果代码没有任何新变化,则用来改写上一次commit的提交信息
git commit --amend -m [message]
重做上一次commit,并包括指定文件的新变化
git commit --amend [file1] [file2] ...
为什么 Git 比其他版本控制系统设计得优秀,因为 Git 跟踪并管理的是修改,而非文件。什么是修改?比如:新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。
示例:验证 "管理的是修改,而不是文件"
对 readme.txt 随便修改下,例如,随便 增加、删除一行
提交:git add readme.txt ,查看状态:git status
然后再修改 readme.txt,并再次提交:git commit -m "修改",提交后再看看状态:git status
怎么第二次的修改没有被提交?别激动,我们回顾一下操作过程:
第一次修改 ->
git add
-> 第二次修改 ->git commit
前面讲了,Git 管理的是修改,当用
git add
命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit
只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。提交后用 git diff HEAD -- readme.txt 可以查看工作区和版本库里面最新版本的区别。
那怎么提交第二次修改呢?你可以继续 git add
再git commit
,也可以别着急提交第一次修改,先 git add
第二次修改,再git commit
,就相当于把两次修改合并后一块提交了:第一次修改 -> git add
-> 第二次修改 -> git add
-> git commit
,现在就把第二次修改提交了。
Git 鼓励大量使用分支,分支相关操作命令如下:
查看分支
git branch // 列出所有 "本地分支",前面有 * 号的代表当前分支
git branch <本地分支名> // 新建分支,但依然停留在当前分支
git branch <本地分支名> <origin/远程分支名> // 新建分支并从远程拉取代码到到新建分支
git branch -r // 列出所有 "远程分支"
git branch -a // 列出所有 "本地、远程" 分支
git branch -vv // 查看本地分支和远程分支关联情况
git remote // 查看远程仓库
拉取 远程分支
git fetch origin 不指定分支名就是远程全部分支
git fetch origin XXX 拉下远程指定的分支更新远程分支列表:git remote update origin --prune
切换分支
git checkout - //切换到上一个分支
git checkout <分支名> 或者 git switch <分支名> // 切换到另一个分支"创建+切换" 分支
git checkout -b <本地分支名> // 新建本地分支,并切换到该分支
git checkout -b <本地分支名> <origin/远程分支名> // 新建分支并从远程拉取分支到本地分支
合并分支,想将 dev 分支合并到 master 分支
git checkout master // 切换到master分支上
git pull origin master // 把远程分支pull下去,及时更新
git merge dev // 把dev分支的代码合并到master上
git status // 查看状态
git push origin master // push 到远程分支
删除分支
删除本地分支:git branch -d <name>
删除远程分支:
git push origin --delete [branch-name]
git branch -dr [remote/branch]删除远程仓库(用一个空的本地分支更新远程分支):git push origin :XXX
示例:
删除远程分支 Chapater6:git push origin --delete Chapater6
删除本地分支 Chapater6:git branch -d Chapater6
"本地、远程" 分支关联。
// 将本地新建的分支与远程分支相关联(在当前分支下输入以下命令)
git branch -u origin/分支名 // "origin/分支名" 中 "分支名" 为远程分支名
或使使用命令:git branch --set-upstream-to origin/分支名
撤销本地分支与远程分支的关系:git branch --unset-upstream新建分支与指定的远程分支建立追踪关系:git branch --track [branch] [remote-branch]
现有分支与指定的远程分支建立追踪关系:git branch --set-upstream [branch] [remote-branch]
git remote -v // 查看远程仓库详细信息,可以看到仓库名称
git remote remove orign // 删除orign仓库(如果把origin拼写成orign,删除错误名称仓库)
git remote add origin 仓库地址 // 重新添加远程仓库地址
gti push -u origin master // 提交到远程仓库的master主干
Git 的分支本质是一个指向提交快照的指针,是从某个提交快照往回看的历史。当创建/切换分支的时候,只是变换了指针指向。
分支 就像 科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!
分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
在版本回退里,已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在 Git 里,这个分支叫主分支,即
master
分支。HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是 当前分支。一开始的时候,master
分支是一条线,Git 用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:每次提交时master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长:
video: http://liaoxuefeng.gitee.io/git-resources/master-branch-forward.mp4
当创建新的分支,例如 dev 时,Git 新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:
可以看到Git 创建一个分支很快,因为除了增加一个
dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化!不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变:假如我们在
dev
上的工作完成了,就可以把dev
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:所以Git合并分支也很快!就改改指针,工作区内容也不变!合并完分支后,甚至可以删除
dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支:真是太神奇了,你看得出来有些提交是通过分支完成的吗?
video: http://liaoxuefeng.gitee.io/git-resources/master-and-dev-ff.mp4
下面开始实战。
创建dev分支,然后切换到dev分支
git checkout -b dev
等价于
git branch dev
git checkout dev用 git branch 命令查看当前分支:
$ git branch 命令会列出所有分支,当前分支前面会标一个
*
号。
* dev
master然后,我们就可以在
dev
分支上正常提交,比如对readme.txt做个修改,加上一行。然后提交:$ git add readme.txt
$ git commit -m "branch test"现在 dev 分支的工作完成,我们就可以切换回master分支:git checkout master
切换回 master 分支后,再查看一个 readme.txt 文件,刚才添加的内容不见了!因为那个提交是在dev 分支上,而 master 分支此刻的提交点并没有变:
现在,我们把 dev 分支的工作成果合并到master分支上:git merge dev
合并后,再查看readme.txt的内容,就可以看到,和
dev
分支的最新提交是完全一样的。注意到上面的
Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward
,后面会讲其他方式的合并。合并完成后,就可以放心地删除
dev
分支了:git branch -d dev删除后,查看
branch
,就只剩下master
分支了:$ git branch
* master因为创建、合并和删除分支非常快,所以Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在
master
分支上工作效果是一样的,但过程更安全。
人生不如意之事十之八九,合并分支往往也不是一帆风顺的。
准备新的
feature1
分支,继续我们的新分支开发:$ git checkout -b feature1修改 readme.txt 在最后一行随便添加些内容,在
feature1
分支上提交:$ git add readme.txt
$ git commit -m "add"切换到
master
分支:$ git checkout masterGit 会自动提示我们当前 master 分支比远程的master分支要超前1个提交。在master分支上把readme.txt 文件的最后一行随便修改,然后提交:
$ git add readme.txt
$ git commit -m "add"现在,
master
分支和feature1
分支各自都分别有新的提交,变成了这样:这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:$ git merge feature1
果然冲突了!Git 告诉我们,
readme.txt
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:我们可以直接查看readme.txt的内容:
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1Git用
<<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,修改冲突的部分后保存,再提交:$ git add readme.txt
$ git commit -m "conflict fixed"现在,
master
分支和feature1
分支变成了下图所示:用
git log
也可以看到分支的合并情况:$ git log --graph --pretty=oneline --abbrev-commit* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file最后,删除
feature1
分支:$ git branch -d feature1工作完成。
小结:当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。用
git log --graph
命令可以看到分支合并图。
含义:从目标 commit 和当前 commit (即 HEAD 所指向的 commit)分叉的位置起,把目标 commit 的路径上的所有 commit 的内容一并应用到当前 commit,然后自动生成一个新的 commit。
当执行
git merge branch1
操作,Git 会把 5 和 6 这两个 commit 的内容一并应用到 4 上,然后生成一个新的提交 7 。
merge 的特殊情况:
git merge --abort
放弃解决冲突,取消merge通常,合并分支时,如果可能,Git会用
Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。如果要强制禁用Fast forward
模式,Git就会在merge时生成一个新的commit,这样从分支历史上就可以看出分支信息。下面实战一下 --no-ff 方式的 git merge
首先,仍然创建并切换
dev
分支:$ git checkout -b dev修改 readme.txt文件,并提交一个新的 commit:
$ git add readme.txt
$ git commit -m "add merge"现在,我们切换回
master
:$ git checkout master准备合并dev分支,--no-ff 表示禁用 Fast forward:git merge --no-ff -m "merge with no-ff" dev
因为本次合并要创建一个新的commit,所以加上-m参数,把 commit 描述写进去。
合并后,我们用
git log
看看分支历史:$ git log --graph --pretty=oneline --abbrev-commit
* e1e9c68 (HEAD -> master) merge with no-ff
|\
| * f52c633 (dev) add merge
|/
* cf810e4 conflict fixed
...可以看到,不使用
Fast forward
模式,merge 后就像这样:
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,
master
分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;那在哪干活呢?干活都在
dev
分支上,也就是说,dev
分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev
分支合并到master
上,在master
分支发布1.0版本;你和你的小伙伴们每个人都在
dev
分支上干活,每个人都有自己的分支,时不时地往dev
分支上合并就可以了。所以,团队合作的分支看起来就像这样:小结:Git 分支十分强大,在团队开发中应该充分应用。合并分支时,加上
--no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward
合并就看不出来曾经做过合并。
stash 指令可以帮你把工作目录的内容全部放在你本地的一个独立的地方,它不会被提交,也不会被删除,你把东西放起来之后就可以去做你的临时工作了,做完以后再来取走,就可以继续之前手头的事了。
操作步骤:
(1)git stash
可以加上save参数后面带备注信息(git stash save '备注信息'
)
(2)此时工作目录已经清空,可以切换到其他分支干其他事情了
(3)git stash pop
弹出第一个stash(该stash从历史stash中移除);或者使用git stash apply
达到相同的效果(该stash仍存在stash list中),同时可以使用git stash list
查看stash历史记录并在apply后面加上指定的stash返回到该stash。
注意:没有被track的文件会被git忽略而不被stash,如果想一起stash,加上-u参数。
软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支
issue-101
来修复它,但是,等等,当前正在dev
上进行的工作还没有提交:git status ,并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?幸好,Git 还提供了一个stash
功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:$ git stash,现在,用git status
查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。首先确定要在哪个分支上修复 bug,假定需要在
master
分支上修复,就从master
创建临时分支:$ git checkout master
$ git checkout -b issue-101现在修复bug,需要把“Git is free software ...”改为“Git is a free software ...”,然后提交:
$ git add readme.txt
$ git commit -m "fix bug 101"修复完成后,切换到
master
分支,并完成合并,最后删除issue-101
分支:$ git checkout master
$ git merge --no-ff -m "merged bug fix 101" issue-101太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到
dev
分支干活了!$ git checkout dev
$ git status工作区是干净的,刚才的工作现场存到哪去了?用
git stash list
命令看看:$ git stash list工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
- 方法 1:用git stash apply恢复,但是恢复后stash内容并不删除,需要用git stash drop来删除;
- 方法 2:用git stash pop,恢复的同时把stash内容也删了:$ git stash pop,再用 git stash list 查看,就看不到任何stash内容了
可以多次stash,恢复的时候先用
git stash list
查看,然后恢复指定的 stash,用命令:$ git stash apply stash@{0}小结:修复bug时,会通过创建新的bug分支进行修复,然后合并,最后删除;当手头工作没有完成时,先把工作现场
git stash
一下,然后去修复bug,修复后,再git stash pop
,回到工作现场。
软件开发中,总有无穷无尽的新的功能要不断添加进来。添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
假设你现在接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。
于是准备开发:$ git checkout -b feature-vulcan
5分钟后,开发完毕:
$ git add vulcan.py
$ git status
$ git commit -m "add feature vulcan"切回
dev
,准备合并:$ git checkout dev一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。但是!就在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:$ git branch -d feature-vulcan
销毁失败。Git友情提醒,
feature-vulcan
分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D
参数。。现在我们强行删除:$ git branch -D feature-vulcan,终于删除成功!
小结:开发一个新feature,最好新建一个分支;如果要丢弃一个没有被合并过的分支,可以通过
git branch -D <name>
强行删除。
多人协作,需要从远程仓库克隆,克隆时Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。查看远程库的信息:git remote 或者 git remote -v 显示更详细的信息。可以看到拉取和推送的
origin
的地址。如果没有推送权限,就看不到push的地址。git clone 用于克隆一个远程仓库到本地。它会复制整个仓库的历史记录、分支和代码到本地机器上。通常在开始一个新项目或者从头开始参与一个已有项目时会使用 git clone 命令。
git pull 命令用于从远程仓库拉取代码并合并到当前分支。它的作用相当于执行了 git fetch 和 git merge 两个命令的组合。当你执行 git pull 命令时,Git 会自动下载最新的代码变更,并将其合并到当前分支中。
git fetch:用于从远程仓库获取最新的代码变更,但不会自动将其合并到当前分支。它只是将远程分支的更新下载到本地,并更新对应的远程跟踪分支。这样你可以查看远程分支的变更,然后决定是否将更新合并到当前分支。
这些命令都需要与远程仓库进行交互
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
格式:git push 远程分支名 本地分支名
示例:git push origin master # 推送本地master分支到远程origin分支的master
示例:git push origin dev # 推送本地dev分支到远程origin分支的master但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
- master分支是主分支,因此要时刻与远程同步;
- dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
- bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
- feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
多人协作时,大家都会往 master 和 dev 分支上推送各自的修改。现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:git clone git@github.com:michaelliao/learngit.git ,
当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的
master
分支。不信可以用git branch
命令看下。如果你的小伙伴要在dev
分支上开发,就必须创建远程origin
的dev
分支到本地,于是他用这个命令创建本地dev
分支:git checkout -b dev origin/dev现在他就可以在
dev
上继续修改,然后,时不时地把dev
分支push
到远程:git add env.txt
git commit -m "add env"
git push origin dev你的小伙伴已经向
origin/dev
分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:git add env.txt
git commit -m "add new env"
git push origin dev推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用
git pull
把最新的提交从origin/dev
抓下来,然后,在本地合并,解决冲突,再执行 git pull 推送,但是失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接:git branch --set-upstream-to=origin/dev dev ,再执行 git pull ,这回推送成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:git commit -m "fix env conflict"
git push origin dev因此,多人协作的工作模式通常是这样:
- 首先,可以试图用git push origin <branch-name>推送自己的修改;
- 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
- 如果合并有冲突,则解决冲突,并在本地提交;
- 没有冲突或者解决掉冲突后,再用git push origin <branch-name>推送就能成功!
如果 git pull 提示 no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令 git branch --set-upstream-to <branch-name> origin/<branch-name> ,这就是多人协作的工作模式,一旦熟悉了,就非常简单。
团队工作基本模型:
最流行的工作流
优势:(1)代码分享:写完之后可以在开发分支review之后再merge到master分支
(2)一人多任务:当正在开发接到更重要的新任务时,你只要稍微把目前未提交的代码简单收尾一下,然后做一个带有「未完成」标记的提交(例如,在提交信息里标上「TODO」),然后回到 master 去创建一个新的 branch 进行开发就好了。
branch 包含了从初始 commit 到它的所有路径,而不是一条路径。并且,这些路径之间也是彼此平等的。
像上图这样,master 在合并了 branch1 之后,从初始 commit 到 master 有了两条路径。这时,master 的串就包含了 1 2 3 4 7 和 1 2 5 6 7 这两条路径。而且,这两条路径是平等的,1 2 3 4 7 这条路径并不会因为它是「原生路径」而拥有任何的特别之处
创建branch:git branch 名称
切换branch:git checkout 名称
(将HEAD指向该branch)
创建+切换:git checkout -b 名称
在切换到新的 branch 后,再次 commit 时 HEAD 就会带着新的 branch 移动了:
而这个时候,如果你再切换到 master 去 commit,就会真正地出现分叉了:
删除branch:git branch -d 名称
注意:
引用的本质
所谓引用,其实就是一个个的字符串。这个字符串可以是一个 commit 的 SHA-1 码(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也可以是一个 branch(例:ref: refs/heads/feature3)。
Git 中的 HEAD 和每一个 branch 以及其他的引用,都是以文本文件的形式存储在本地仓库 .git 目录中,而 Git 在工作的时候,就是通过这些文本文件的内容来判断这些所谓的「引用」是指向谁的。
push的本质:把 branch 上传到远程仓库
git push
不加参数只能上传到从远程仓库clone或者pull下来的分支,如需push在本地创建的分支则需使用git push origin 分支名
的命令git br -r
查看远程分支的HEAD指向)。rebase:变基、复位、重定基准、 重置为原始基本
- 在 Git 中,Rebase 的主要作用是将一个分支上的提交应用到另一个分支上,通常用于合并代码或保持分支的更新。与传统的合并(merge)不同,Rebase 可以将提交应用到目标分支的最新提交之后,使提交历史更加整洁和线性。
Rebase 的基本用法如下:
- 切换到目标分支:首先,你需要切换到想要应用提交的目标分支。
- 执行 rebase 命令:运行 git rebase <源分支> 命令,其中 <源分支> 是你希望从中获取提交的分支。这将会将源分支上的提交逐个应用到目标分支上。
- 解决冲突:如果在 Rebase 过程中发生冲突,Git 会暂停 Rebase,并提示你解决冲突。你需要手动解决冲突并提交更改,然后使用 git rebase --continue 继续执行 Rebase。
- 完成 Rebase:当所有提交都成功应用到目标分支上,Rebase 就完成了。
多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。每次合并再push后,分支变成了这样:
$ git log --graph --pretty=oneline --abbrev-commit
* d1be385 (HEAD -> master, origin/master) init hello
* e5e69f1 Merge branch 'dev'
|\
| * 57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
| |/
* | 12a631b merged bug fix 101
|\ \
| * | 4c805e2 fix bug 101
|/ /
* | e1e9c68 merge with no-ff
|\ \
| |/
| * f52c633 add merge
|/
* cf810e4 conflict fixed总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?其实是可以做到的!Git有一种称为rebase的操作,有人把它翻译成 "变基"。
先不要随意展开想象。我们还是从实际问题出发,看看怎么把分叉的提交变成直线。
在和远程分支同步后,我们对
hello.py
这个文件做了两次提交。用git log
命令看看:$ git log --graph --pretty=oneline --abbrev-commit
* 582d922 (HEAD -> master) add author
* 8875536 add comment
* d1be385 (origin/master) init hello
* e5e69f1 Merge branch 'dev'
|\
| * 57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
...注意到Git用
(HEAD -> master)
和(origin/master)
标识出当前分支的HEAD和远程origin的位置分别是582d922 add author
和d1be385 init hello
,本地分支比远程分支快两个提交。现在我们尝试推送本地分支:$ git push origin master
很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先pull一下:$ git pull
再看下状态:$ git status
加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用
git log
看看:git log --graph --pretty=oneline --abbrev-commit对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push到远程,有没有问题?有!什么问题?不好看!有没有解决方法?有!这个时候,rebase就派上了用场。我们输入命令
git rebase
试试:$ git rebase输出了一大堆操作,到底是啥效果?看下log:git log --graph --pretty=oneline --abbrev-commit
原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了
f005ed4 (origin/master) set exit=1
之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello
,而是基于f005ed4 (origin/master) set exit=1
,但最后的提交7e61ed4
内容是一致的。这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。最后,通过push操作把本地分支推送到远程:git push origin master
再用
git log
看看效果:git log --graph --pretty=oneline --abbrev-commit远程分支的提交历史也是一条直线。
有些人不喜欢 merge,因为在 merge 之后,commit 历史就会出现分叉,这种分叉再汇合的结构会让有些人觉得混乱而难以管理。如果你不希望 commit 历史出现分叉,可以用 rebase 来代替 merge。
可以看出,通过 rebase,5 和 6 两条 commits 把基础点从 2 换成了 4 。通过这样的方式,就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作,就是 rebase 的含义。另外,在 rebase 之后,记得切回 master 再 merge 一下,把 master 移到最新的 commit。
为什么要从 branch1 来 rebase,然后再切回 master 再 merge 一下这么麻烦,而不是直接在 master 上执行 rebase?从图中可以看出,rebase 后的每个 commit 虽然内容和 rebase 之前相同,但它们已经是不同的 commit 了(每个commit有唯一标志)。如果直接从 master 执行 rebase 的话,就会是下面这样:
这就导致 master 上之前的两个最新 commit (3和4)被剔除了。如果这两个 commit 之前已经在远程仓库存在,这就会导致没法 push :
所以,为了避免和远程仓库发生冲突,一般不要从 master 向其他 branch 执行 rebase 操作。而如果是 master 以外的 branch 之间的 rebase(比如 branch1 和 branch2 之间),就不必这么多费一步,直接 rebase 就好。需要说明的是,rebase 是站在需要被 rebase 的 commit 上进行操作,这点和 merge 是不同的。
git tag 列出所有tag
git tag [tag] 新建一个tag在当前commit
git tag [tag] [commit] 新建一个tag在指定commit
git tag -d [tag] 删除本地tag
git push origin :refs/tags/[tagName] 删除远程tag
git show [tag] 查看tag信息
git push [remote] [tag] 提交指定tag
git push [remote] --tags 提交所有tag
git checkout -b [branch] [tag] 新建一个分支,指向某个tag
Git 有 commit,为什么还要引入 tag?“请把上周一的那个版本打包发布,commit号是6a5819e...” “一串乱七八糟的数字不好找!”如果换一个办法:“请把上周一的那个版本打包发布,版本号是v1.2”“好的,按照tag v1.2查找commit就行!”所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。
发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。Git 的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
在Git中打标签非常简单,首先,切换到需要打标签的分支上:
$ git branch
$ git checkout master然后,敲命令
git tag <name>
就可以打一个新标签:$ git tag v1.0可以用命令
git tag
查看所有标签:$ git tag默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
可以找到历史提交的commit id,然后打上就可以了:$ git log --pretty=oneline --abbrev-commit
假如要对
add merge
提交打标签,它对应的commit id是f52c633
,执行命令:$ git tag v0.9 f52c633再用命令
git tag
查看标签:$ git tag注意,标签不是按时间顺序列出,而是按字母排序的。可以用
git show <tagname>
查看标签信息:git show v0.9,可以看到,v0.9
确实打在add merge
这次提交上。还可以创建带有说明的标签,用
-a
指定标签名,-m
指定说明文字:$ git tag -a v0.1 -m "version 0.1 released" 1094adb用命令
git show <tagname>
可以看到说明文字:$ git show v0.1注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。
小结
- 命令git tag <tagname>用于新建一个标签,默认为HEAD,也可以指定一个commit id;
- 命令git tag -a <tagname> -m "blablabla..."可以指定标签信息;
- 命令git tag可以查看所有标签。
如果标签打错了,也可以删除:$ git tag -d v0.1
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令
git push origin <tagname>
:$ git push origin v1.0或者,一次性推送全部尚未推送到远程的本地标签:$ git push origin --tags
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:$ git tag -d v0.9
然后,从远程删除。删除命令也是push,但是格式如下:$ git push origin :refs/tags/v0.9
要看看是否真的从远程库删除了标签,可以登陆GitHub查看。
小结
- 命令git push origin <tagname>可以推送一个本地标签;
- 命令git push origin --tags可以推送全部未推送过的本地标签;
- 命令git tag -d <tagname>可以删除一个本地标签;
- 命令git push origin :refs/tags/<tagname>可以删除一个远程标签。
git status 显示有变更的文件
git log 显示当前分支的版本历史
git log --stat 显示commit历史,以及每次commit发生变更的文件
git log -S [keyword] 搜索提交历史,根据关键词
git log [tag] HEAD --pretty=format:%s 显示某个commit之后的所有变动,
git log [tag] HEAD --grep feature 根据搜索条件显示某个commit之后的所有变动
git log --follow [file] 显示某个文件的版本历史,包括文件改名
git whatchanged [file]
git log -p [file] 显示指定文件相关的每一次diff
git log -5 --pretty --oneline 显示过去5次提交
git shortlog -sn 显示所有提交过的用户,按提交次数排序
git blame [file] 显示指定文件是什么人在什么时间修改过
git diff 显示暂存区和工作区的差异
git diff --cached [file] 显示暂存区和上一个commit的差异
git diff HEAD 显示工作区与当前分支最新commit之间的差异
git diff [first-branch]...[second-branch] 显示两次提交之间的差异
git diff --shortstat "@{0 day ago}" 显示今天你写了多少行代码
git show [commit] 显示某次提交的元数据和内容变化
git show --name-only [commit] 显示某次提交发生变化的文件
git show [commit]:[filename] 显示某次提交时,某个文件的内容
git reflog 显示当前分支的最近几次提交
可以查看git的引用记录,不指定参数,默认显示HEAD的引用记录;如果不小心把分支删掉了,可以使用该命令查看引用记录,然后使用checkout切到该记录处重建分支即可。
注意:不再被引用直接或间接指向的 commits 会在一定时间后被 Git 回收,所以使用 reflog 来找回被删除的 branch 的操作一定要及时,不然有可能会由于 commit 被回收而再也找不回来。
git log -p
可以查看每个commit的改动细节(到改动文件的每一行)git log --stat
查看简要统计(哪几个文件改动了)git show 指定commit 指定文件名
查看指定commit的指定文件改动细节
git diff --staged
可以显示暂存区和上一条提交之间的不同。换句话说,这条指令可以让你看到「如果你立即输入 git commit,你将会提交什么」git diff
可以显示工作目录和暂存区之间的不同。换句话说,这条指令可以让你看到「如果你现在把所有文件都 add,你会向暂存区中增加哪些内容」git diff HEAD
可以显示工作目录和上一条提交之间的不同,它是上面这二者的内容相加。换句话说,这条指令可以让你看到「如果你现在把所有文件都 add 然后 git commit,你将会提交什么」(不过需要注意,没有被 Git 记录在案的文件(即从来没有被 add 过的文件,untracked files 并不会显示出来。因为对 Git 来说它并不存在)实质上,如果你把 HEAD 换成别的commit,也可以显示当前工作目录和这条 commit 的区别。
git fetch [remote] 下载远程仓库的所有变动
git remote -v 显示所有远程仓库
git remote show [remote] 显示某个远程仓库的信息
git remote add [shortname] [url] 增加一个新的远程仓库,并命名
git pull [remote] [branch] 取回远程仓库的变化,并与本地分支合并
git push [remote] [branch] 上传本地指定分支到远程仓库
git push [remote] --force 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --all 推送所有分支到远程仓库
Git 是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?肯定是有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。
你肯定会想,至少需要两台机器才能玩远程库不是?但是我只有一台电脑,怎么玩?
其实一台电脑上也是可以克隆多个版本库的,只要不在同一个目录下。不过,现实生活中是不会有人这么傻的在一台电脑上搞几个远程库玩,因为一台电脑上搞几个远程库完全没有意义,而且硬盘挂了会导致所有库都挂掉,所以我也不告诉你在一台电脑上怎么克隆多个仓库。
实际情况往往是这样,找一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。
完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。
在继续阅读后续内容前,请自行注册GitHub账号。由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:
- 第 1 步:创建 SSH Key。首先去 ~/.ssh 目录看看有没有 id_rsa 和 id_rsa.pub 这两个文件,如果有直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建 SSH Key:"ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。如果一切顺利的话,可以在 ~/.ssh 目录找到 id_rsa 和 id_rsa.pub 两个文件,这两个就是 SSH Key 的秘钥对,id_rsa 是私钥,不能泄露出去,id_rsa.pub 是公钥,可以放心地告诉任何人。
- 第 2 步:登陆 GitHub,打开 Account settings ---> SSH Keys 页面,然后点 Add SSH Key,填上任意 Title,在 Key 文本框里粘贴 id_rsa.pub 文件的内容,点 Add Key,你就应该看到已经添加的 Key
- 第3步:通过 ssh 克隆私有仓库。注意:是通过 ssh,不是通过 https
为什么 GitHub 需要 SSH Key 呢?因为 GitHub 需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。最后友情提示:在 GitHub 上免费托管的 Git 仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub 把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。另一个办法是自己动手,搭一个Git服务器,因为是你自己的Git服务器,所以别人也是看不见的,这个相当简单,公司内部开发必备。
情景:假设你已经在本地创建了一个Git 仓库后,又想在 GitHub 创建一个 Git 仓库,并且让这两个仓库进行远程同步,这样,GitHub 上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。
- 首先,登陆 GitHub,然后,在右上角找到 “Create a new repo” 按钮,创建一个新的仓库:在Repository name 填入 learn_git,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库。目前,在GitHub上的这个 learn_git 仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
- 现在,根据 GitHub 的提示,在本地的 learn_git 仓库下运行命令:
$ git remote add origin git@github.com:michaelliao/learn_git.git
# 远程库名字叫origin,对应地址为:git@github.com:自己的github账号/learn_git.git
添加后,远程库的名字就是 origin,这是 Git 默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。- 把本地库的所有内容推送到远程库上:git push -u origin master
用 git push 把当前master分支,推送到远程叫origin的库。由于远程库是空的,当第一次推送master
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令。- 推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一模一样。从现在起,只要本地作了提交,就可以通过命令:git push origin master。把本地 master分支 的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!
上面的操作是先有本地库,后有远程库,然后关联远程库。现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。
首先,登陆 GitHub创建一个新的仓库名字叫 gitskills,勾选 Initialize this repository with a README,这样 GitHub 会自动为我们创建一个README.md文件。创建完毕后可以看到README.md文件,现在远程库已经准备好了,下一步是用命令git clone克隆一个本地库:it clone git@github.com:自己的github账号/gitskills.git ,然后进入gitskills目录看看,已经有README.md文件了,如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。
克隆时 GitHub 给出的地址不止一个,还可以用 https://github.com/michaelliao/gitskills.git 这样的地址。实际上,Git支持多种协议,默认的 git:// 使用 ssh,也可以使用 https 等其他协议。使用 https 除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http 端口的公司内部就无法使用 ssh 协议而只能用 https。
我们一直用GitHub作为免费的远程仓库,如果是个人的开源项目,放到GitHub上是完全没有问题的。其实GitHub还是一个开源协作社区,通过GitHub,既可以让别人参与你的开源项目,也可以参与别人的开源项目。
在GitHub出现以前,开源项目开源容易,但让广大人民群众参与进来比较困难,因为要参与,就要提交代码,而给每个想提交代码的群众都开一个账号那是不现实的,因此,群众也仅限于报个bug,即使能改掉bug,也只能把diff文件用邮件发过去,很不方便。
但是在GitHub上,利用Git极其强大的克隆和分支功能,广大人民群众真正可以第一次自由参与各种开源项目了。
如何参与一个开源项目呢?比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,你可以访问它的项目主页 https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:git clone git@github.com:自己github账号/bootstrap.git 。一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址
git@github.com:twbs/bootstrap.git
克隆,因为没有权限,你将不能推送修改。Bootstrap的官方仓库
twbs/bootstrap
、你在GitHub上克隆的仓库my/bootstrap
,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。
如果你希望bootstrap的官方库能接受你的修改,你就可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。
如果你没能力修改bootstrap,但又想要试一把pull request,那就Fork一下我的仓库:https://github.com/michaelliao/learngit,创建一个
your-github-id.txt
的文本文件,写点自己学习Git的心得,然后推送一个pull request给我,我会视心情而定是否接受。小结
- 在GitHub上,可以任意Fork开源仓库;
- 自己拥有Fork后的仓库的读写权限;
- 可以推送pull request给官方仓库来贡献代码。
GitHub就是一个免费托管开源代码的远程仓库。但是对于某些视源代码如生命的商业公司来说,既不想公开源代码,又舍不得给GitHub交保护费,那就只能自己搭建一台Git服务器作为私有仓库使用。搭建Git服务器需要准备一台运行Linux的机器,强烈推荐用Ubuntu或Debian,这样,通过几条简单的apt
命令就可以完成安装。
假设你已经有sudo
权限的用户账号,下面,正式开始安装。
git
:$ sudo apt-get install gitgit
用户,用来运行git
服务:$ sudo adduser gitid_rsa.pub
文件,把所有公钥导入到/home/git/.ssh/authorized_keys
文件里,一行一个。/srv/sample.git
,在/srv
目录下输入命令:$ sudo git init --bare sample.git,Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git
结尾。然后,把owner改为git
:$ sudo chown -R git:git sample.git/etc/passwd
文件完成。找到类似下面的一行 git:x:1001:1001:,,,:/home/git:/bin/bash 改为 git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell ,这样git
用户可以正常通过ssh使用git,但无法登录shell,因为我们为git
用户指定的git-shell
每次一登录就自动退出。git clone
命令克隆远程仓库了:git clone git@server:/srv/sample.git ,剩下的推送就简单了。管理公钥
如果团队很小,把每个人的公钥收集起来放到服务器的
/home/git/.ssh/authorized_keys
文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。这里我们不介绍怎么玩Gitosis了,几百号人的团队基本都在500强了,相信找个高水平的Linux管理员问题不大。
管理权限
有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。这里我们也不介绍Gitolite了,不要把有限的生命浪费到权限斗争中。
小结
git checkout [file] 恢复暂存区的指定文件到工作区
git checkout * 一键还原 所有文件
git checkout [commit] [file] 恢复某个commit的指定文件到暂存区和工作区
git checkout . 恢复暂存区的所有文件到工作区
git reset [file] 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset --hard 重置暂存区与工作区,与上一次commit保持一致
git reset [commit] 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset --hard [commit] 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --keep [commit] 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git revert [commit] 新建一个commit用来撤销指定commit,后者的所有变化都将被前者抵消,并且应用到当前分支
# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop
git HEAD
git HEAD~ //上一个版本
git HEAD~100 //往上100个版本
git checkout <file> //恢复未提交的更改
git reset HEAD <file> //取消之前 git add 添加
git reset --hard HEAD~ //回退到上一个版本
git reset --hard <commit ID> //回退到指定版本
如果远程分支也想要回退,git push -f (known changes)。
git revert HEAD //撤销前一次commit
git revert 和 git reset 的区别
在 git 中,用 HEAD 表示当前版本也就是最新的提交,上一个版本就是
HEAD^
,上上一个版本就是HEAD^^
,当然往上100个版本写100个^
比较容易数不过来,所以写成HEAD~100
。回退到上一个版本:git reset --hard HEAD^
git log 看现在版本库的状态,可以发现已经回退
回退到特定版本:git reset --hard 1094a(提交id) # 版本号前几位就可以,Git会自动去找
git reflog 用来记录你的每一次命令
总结:
- HEAD 指向当前版本。使用命令 git reset --hard commit_id 在版本的历史之间穿梭
- 穿梭前,用 git log 可以查看提交历史,以便确定要回退到哪个版本。
- 要重返未来,用 git reflog 查看命令历史,以便确定要回到未来的哪个版本。
git checkout -- readme.txt 可以丢弃工作区的修改。
命令 git checkout -- readme.txt 是把文件readme.txt在工作区的修改全部撤销,有两种情况:
- readme.txt 自修改后还没有被放到暂存区,现在撤销修改就回到和版本库一模一样的状态;
- readme.txt 已经添加到暂存区后,随后又作了修改,现在撤销修改,就是回到添加到暂存区后的状态。
- git checkout 其实是用 "版本库里的版本或者暂存区的版本" 替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。总之,就是让这个文件回到最近一次git commit或git add时的状态。git checkout -- file 命令中的 -- 很重要,没有--就变成了 "切换到另一个分支" 的命令
git reset 命令既可以回退版本,也可以把暂存区的修改回退到工作区。HEAD 表示最新的版本
git reset HEAD <file> 可以把暂存区的修改撤销掉(unstage),重新放回工作区:git reset HEAD readme.txt
再用 git status 查看一下,现在暂存区是干净的,工作区有修改:
丢弃工作区的修改:git checkout -- readme.txt小结
- 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令 git checkout -- file ,会直接用暂存区最后一次提交的覆盖工作区的。
- 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD <file>,就回到了场景1,第二步按场景1操作。
- 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考 版本回退,不过前提是没有推送到远程库。
在 Git 中删除也是一个修改操作。直接在文件管理器中把没用的文件删了,或者用rm命令删了,这个时候,Git 知道你删除了文件,因此,工作区和版本库就不一致了,git status
命令会立刻告诉你哪些文件被删除了。现在你有两个选择,
再提一个修复了错误的commit?可以是可以,不过还有一个更加优雅和简单的解决方法:commit --amend。
具体做法:
(1)修改好问题
(2)将修改add到暂存区
(3)使用git commit --amend
提交修改,结果如下图:
减少了一次无谓的commit。
使用rebase -i(交互式rebase):
所谓「交互式 rebase」,就是在 rebase 的操作执行之前,你可以指定要 rebase 的 commit 链中的每一个 commit 是否需要进一步修改,那么你就可以利用这个特点,进行一次「原地 rebase」。
操作过程:
(1)git rebase -i HEAD^^
说明:在 Git 中,有两个「偏移符号」: ^ 和 ~。
^ 的用法:在 commit 的后面加一个或多个 ^ 号,可以把 commit 往回偏移,偏移的数量是 ^ 的数量。例如:master^ 表示 master 指向的 commit 之前的那个 commit; HEAD^^ 表示 HEAD 所指向的 commit 往前数两个 commit。
~ 的用法:在 commit 的后面加上 ~ 号和一个数,可以把 commit 往回偏移,偏移的数量是 ~ 号后面的数。例如:HEAD~5 表示 HEAD 指向的 commit往前数 5 个 commit。
上面这行代码表示,把当前 commit ( HEAD 所指向的 commit) rebase 到 HEAD 之前 2 个的 commit 上:
(2)进入编辑页面,选择commit对应的操作,commit为正序排列,旧的在上,新的在下,前面黄色的为如何操作该commit,默认pick(直接应用该commit不做任何改变),修改第一个commit为edit(应用这个 commit,然后停下来等待继续修正)然后:wq退出编辑页面,此时rebase停在第二个commit的位置,此时可以对内容进行修改:
(3)修改完后使用add,commit --amend将修改提交
(4)git rebase --continue
继续 rebase 过程,把后面的 commit 直接应用上去,这次交互式 rebase 的过程就完美结束了,你的那个倒数第二个写错的 commit 就也被修正了:
reset --hard 丢弃最新的提交:git reset --hard HEAD^
HEAD^ 表示 HEAD 往回数一个位置的 commit
操作步骤与修改历史提交类似,第二步把需要撤销的commit修改为drop,其他步骤不再赘述。
git rebase --onto HEAD^^ HEAD^ branch1
上面这行代码的意思是:以倒数第二个 commit 为起点(起点不包含在 rebase 序列里),branch1 为终点,rebase 到倒数第三个 commit 上。
有的时候,代码 push 到了远程仓库,才发现有个 commit 写错了。这种问题的处理分两种情况:
出错内容在自己的分支
假如是某个你自己独立开发的 branch 出错了,不会影响到其他人,那没关系用前面几节讲的方法把写错的 commit 修改或者删除掉,然后再 push 上去就好了。但是此时会push报错,因为远程仓库包含本地没有的 commits(在本地已经被替换或被删除了),此时直接使用
git push origin 分支名 -f
强制push。
问题内容已合并到master
(1)增加新提交覆盖之前内容
(2)使用git revert 指定commit
它的用法很简单,你希望撤销哪个 commit,就把它填在后面。如:git revert HEAD^
上面这行代码就会增加一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。在 revert 完成之后,把新的 commit 再 push 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit。
git reset --hard 指定commit
你的工作目录里的内容会被完全重置为和指定commit位置相同的内容。换句话说,就是你的未提交的修改会被全部擦掉。git reset --soft 指定commit
会在重置 HEAD 和 branch 时,保留工作目录和暂存区中的内容,并把重置 HEAD 所带来的新的差异放进暂存区。
什么是「重置 HEAD 所带来的新的差异」?就是这里:
git reset --mixed(或者不加参数) 指定commit
保留工作目录,并且清空暂存区。也就是说,工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作区。简而言之,就是「把所有差异都混合(mixed)放在工作区中」。
checkout的本质是签出指定的commit,不止可以切换branch还可以指定commit作为参数,把HEAD移动到指定的commit上;与reset的区别在于只移动HEAD不改变绑定的branch;git checkout --detach
可以把 HEAD 和 branch 脱离,直接指向当前 commit。
强势安利《git原理详解与实用指南》
git 流程图(如果看不清楚图片,可以 ——> 右键——> 在新标签页中打开图片):
图 1
图 2
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。