git学习练习总资源链接: https://try.github.io/ (练习已通,有document)
本沙盒游戏教学:https://learngitbranching.js.org/?demo
自由沙盒模拟网页 : http://git-school.github.io/visualizing-git/
好的译文: https://github.com/geeeeeeeeek/git-recipes/wiki
什么是git?
一个分布式的源代码库。管理Linux内核源代码。
git已快照形式保存和处理内容,每一个提交都是一次快照。git可以在快照之间回滚。
一个节点代表一个commit.
*代表当前分支的最后一次提交:HEAD
master是主干。
其他名字是分支。
git merge :用于合并分支的代码。
git rebase : 线性合并分支:
git rebase [-i] [目标] [移动记录]
git rebaes [目标] #省略[要移动的记录],则为当前分支的所有commit。
假如当前分支是bugFix:
- git rebase master. 这样bugFix分支就相当于在master的基础上新增的代码了。
- git checkout master 回到master
- git rebase bugFix, master和bugFix的代码都一样了。
HEAD:
是一个对当前检出记录的符号引用 -- 也就是指向你正在其基础上进行工作的提交记录
它总是指向当前分支上最后一次的提交记录。 大多数提交树的git命令都是从改变HEAD的指向开始的。
⚠️,后面章节讲的远程分支 origin/master是例外
HEAD 通常是指向分支名的(如bugFix)。在你提交commit时,改变了分支的状态,这一变化通过HEAD变得可见。
分离的HEAD:
让它指向某个具体的提交记录(hash值)而不是分支名。
git checkout <hash>
git checkout命令本质就是移动HEAD,到目标commit点, 然后更新工作目录以匹配这个commit点。
因为这个操作会overwrite local changes,导致改变的文件丢失,所以Git强迫你先commit或stash工作目录中的改变的文件。
⮀ git checkout master error: Your local changes to the following files would be overwritten by checkout: app/assets/javascripts/search.js Please commit your changes or stash them before you switch branches.
关于git stash (具体工作原理和全部的知识见链接文章)
会把尚未加入stage的文件和statge中的文件保存(就是未commited的文件),以便在之后使用。
之后可以revert them from your working copy.
现在可以进入任何其他操作,如创建新commits, 转变分支,执行其他git操作了。
⚠️stash是本地的。当你push的时候,stash不会被传输。
Re-applying your stashed changes:
$ git stash pop
另外使用git stash apply, 可以reapply the changes的同时在stash中保留它们。这在为多个分支应用时有用。
⚠️:默认Git不会stash 未tracked文件和ignored files。
相对引用
通过指定提交记录hash值的方式在Git中移动不方便操作。
必须用到git log, 而且hash值非常长。
因此可以只使用前几个字符代表一个提交记录 , 即“相对引用”。
^ 代表?向上移动一个commit记录。
~2 代表向上移动2个提交记录,~5,代表移动5个提交记录
使用git checkout HEAD^, 就代表向上移动一次。
强制修改分支位置--移动分支
git branch -f master HEAD~3
代表把master向上移动三个提交节点,即第3个father note
-f :代表--force, force creation, move/rename, deletion
撤销变更
- git reset
- git revert
Command | Scope | Common use cases |
---|---|---|
git reset | Commit-level | Discard commits in a private branch or throw away uncommited changes |
git reset | File-level | Unstage a file |
git checkout | Commit-level | Switch between branches or inspect old snapshots |
git checkout | File-level | Discard changes in the working directory |
git revert | Commit-level | Undo commits in a public branch |
git revert | File-level | (N/A) |
Git Reset
原文:
takes a specified commit and resets the "three trees" to match
the state of the repository at that specified commit.
Three Trees
把分支回退指定个数的commit记录,来实现撤销改动。相当于使用时间机器回退到过去开发的阶段。(撤销的提交记录还存在,只是未加入stage暂存区)
⚠️ 这条命令对团队使用的远程分支无效!
例子:
# 从当前工作目录回退一个commit. git reset HEAD~ # 从当前工作目录回退2个commit git reset HEAD~2
⚠️被回退的2个commit提交变成悬挂提交。下次Git执行垃圾回收时,这两个提交会被删除!
git reset可以把stage中的文件拿出stage。git reset </filename>
⭠ autoquery± ⮀ git reset README.md
Unstaged changes after reset:
M README.md
三个模式选项
- --soft -stage缓存区和工作目录都不会改变。
- --mixed默认选项。✅
- 缓冲区和你指定的提交同步a(在提交a后,加入缓存区的文件被拿出来了)
- 工作目录不受影响(在提交a后对工作目录中的文件进行的改变被保留!)。
- --hard -缓存区和工作目录都被更新,以匹配指定的commit点a。
- 相当于回退到刚刚提交完a的状态!
- 在提交a后的操作全部删除,包括工作目录中对文件的改变,相当于时光倒流
可以认为这个三个模式是对a git reset操作的作用域的定义!
一般使用默认的--mixed。
Git Revert (点击见详细)
专门用于撤销远程提交记录。但本质上是新增一个commit记录,但更改了code,去掉了之前那个commit记录中的变更代码。
c0-c1-c2(->c3)
git revert C1, 结果是新增了一个c3.
c3是c1的反转操作,即c1中变化的代码,在c3中被撤销了。
例如, 你追踪一个bug并发现它是在某个commit点a内增加的一个变量。你无需手动进入这个commit点,删除这个变量,然后再committing一个新的snapshot,你直接使用git revert a命令自动为你做上面的事情。
⚠️:如果你revert提交点c1, 但是c2中有对c1中变化的代码的进一步修改,你不能使用git revert c1, 系统会提示你冲突,需要先搞定冲突代码。
How it works
git revert命令用于撤销一个仓库的commit历史中的某个变化。
其他撤销命令如git checkout 和 git reset,移动HEAD和branch ref pointers到一个指定的commit点。
Git revert也take a specified commit,但是,git revert不会移动ref pointers到这个commit点。
而是执行一个反转操作,反转那个commit的改变的代码,并创建一个新的“revert commit”。
最后ref pointers 会更新,指向这个新的"revert commit", 让这个commit成为分支的端点tip。
选项
-e --edit (默认选项) 打开系统的编辑器,提示你编辑commit信息。
-n --no-commit (一个特别的选项)使用它,git revert不会创建新的commit, 只会在working directory反转变化的代码,并在缓存中加入反转代码后的文件。一句话理解:需要你手动提交!其他没变化。
和get reset的比较:
1:不会改变历史记录。
- 基于这个优点git revert可以在一个public branch上使用,
- 而git reset最好在个人的branch上进行操作。
2:git revert只改变单一的提交点。而git reset会从当前提交往回退(回到过去)。
可以这么理解:
- git revert是撤销committed changes的工具
⚠️git revert也和git checkout类似,在revert操作期间会重写工作目录中的文件。所以它会要求你commit/stash changes。
File-level Operations
git reset和git checkout命令可以接收一个file path作为参数。这会强制把它们的操作限制到一个单一文件。
例子:
git reset HEAD~2 foo.py
当引用一个文件路径时, git reset更新缓存区以匹配特定commit点的版本的指定文件。
?的命令会取得在提前两个提交点的版本的foo.py文件,并把它放入Staged files缓存区中。
但是工作区出现对应的文件的unstaged 状态:
⭠ autoquery ⮀ git reset head~3 README.md Unstaged changes after reset: M README.md ⭠ autoquery± ⮀ git status On branch autoquery Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: README.md 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: README.md
我的理解:
- 执行git reset head~2 foo.py命令
- 取得的文件foo.py放入缓存区,但程序发现取出的文件和当前的foo.py文件的内容不一致,所以出现?的情况。
另外,如果我修改了上一个commit点的foo.py文件并放入缓存区。
然后又执行git reset head~2 foo.py。
程序发现取出的文件和当前修改的foo.py文件的内容不一致,会把当前修改的foo.py文件放入工作目录区。
⚠️复杂的操作尽量配合可视辅助工具Sourcetree
Git checkout head File命令
它会把file放入working directory工作目录。
⚠️我的实际操作是使用此条命令后,取出的文件会放入到缓存区!我的理解:
- git checkout head File
- 程序自动把取出的文件执行git add命令,放入缓存区。
git checkout HEAD~2 foo.py
⚠️此时git checkout命令不会移动HEAD标签,即你不会切换分支!
⚠️:如果把这个变化git commit就相当于直接执行一个git revert命令了!!
整体提交记录
当开发人员说,我想把这个commit放到这里,那个commit放到刚才的提交的后面,就可以使用:
将一些commit复制到当前位置HEAD下面的话,使用这个命令:
git cherry-pick <提交号>...
⚠️个人理解,是每个commit应该是一个独立的模块。
⚠️,前提是你知道你想要的commit记录的hash值,才行。如果不知道,往下看⬇️
交互式的rebase
git rebase -i HEAD~3
指带参数 --interactive的rebase命令,简写-i
rebase会打开一个UI界面:
git 技巧 1: 本地stage提交
假如当前在bugFix分支(C4), 而master在C1, 希望只把C4合并到master上。
c1(master)-c2-c3-c4(bigFix*)
第一种办法:
git checkout master#HEAD回到master
git cherry-pick C4#ok了
第二种办法:
git rebase -i C1 #会打开UI界面,选择C4,去掉C2, C3 ,确定。
#这时buFix是当前分支它包含C4直接放到了C1下面。
git checkout master #回到master, 因为master的数据比较旧
git merge bugFix#合并分支。
git 技巧 2:
git commit --amend
修复最新提交的便捷方式。作用是:
将缓存的修改和之前的commit合并到一起,生成一个新的提交并替换掉原来的提交。
这是从写项目历史的命令。
讨论:
仓促的提交在你日常开发过程中时常会发生。很容易就忘记了缓存一个文件或者弄错了提交信息的格式。--amend
标记是修复这些小意外的便捷方式。
注意:⚠️
不要修复public commit! 永远不要重设和其他开发者共享的commit。
修复也一样:永远不要修复一个已经推送到公共仓库的commit!
https://learngitbranching.js.org/?demo (点击链接看演示, 然后选择第四行第2个按钮)
c1(master)—c2(newImage)—c3(caption*)
设计师需要在c2上调整图片格式,如何做?
|再问:
设计师干嘛不在C3上调整?
答:猜测C2提交记录是针对图片的设计,c3提交记录是针对其他设计。
假设:
git checkout newImage
git commit --amend
git checkout capiton
git merge newImage #这会导致新产生一条commit记录。C4, 不符合线性的要求。
而使用:
# 调整C2, C3的位置,让C2位于分支tip, 因为git commit --amend用于最新提交点。 git rebase -i C1 # 对C2进行修改 git commit --amend #再调整回原先的结构。 git rebase -i C1 #相当于,历史commit记录树没有发生变化。
#然后就可以合并了
git checkout master
git checkout caption
⚠️,作者提示这可能会导致冲突:改用挑?,更快捷。cherry-pick
git checkout newImage
git commit --amend #这会出现一个分叉。
git checkout master
git cherry-pick C2' C3
标签的作用:tag
给某个提交记录一个标签,类似⚓️。 用于重要版本的标记。
git tag V1 <hash>
如果不指定<hash>提交记录,则标记到HEAD指向的位置,因此可以写两条语法:
git checkout <hash>
git tag V1
Git Describe
用来找到最近的tag。帮助你在commit record的历史中移动了多次后找到方向。
git describe <ref>
<ref>是任何识别commit记录的引用,不指定的话,则以当前HEAD位置为准。
它的输出结构:
<tag>_<numCommits>_g<hash>
解释:
<tag>是离<ref>最近的标签,
numCommits是表示和<ref>相差多少个commit记录,
hash表示的是你所给定的<ref>所表示的提交记录hash值的前几位。
Pushing tags
默认,标签不会自动推送上去。 --tags将你所有的本地标签推送到远程仓库。
git push <remoteName> <TagName>
push all tags:
git push <remoteName> --tags
挑战1:线性移动合并分支:git rebase (可以看sandbox案例)
git rebase [-i] [目标] [要移动的记录]
git rebaes [目标] #省略[要移动的记录],则为当前HEAD
挑战2: 使用HEAD~和HEAD^2来移动HEAD的位置
相对引用的扩展:
^2代表第二个父引用记录,可以链式使用。
git checkout HEAD~^2~
表示上一个父记录,然后再第二个父记录,然后再上一个父记录。
如果要在这里建立一个新分支
git branch bugFix HEAD~^2~
挑战3 ,git rebase 的再次使用。
HEAD也可以做[目标]
git rebase [-i] [目标] [要移动的记录]
remote repertory
简单来说就是你的仓库在其他机器上的备份。
特点:
- 备份,恢复丢失数据
- 远程让代码可以社交化了!其他人可以为你的代码做贡献。
git clone: 在本地创建一个远程仓库的拷贝
远程跟踪分支 remote-tracking branch
在本地仓库多了一个名为o/master的分支,这种类型的分支叫做 远程跟踪分支。
远程跟踪分支反应了远程仓库(在你上次和它通信时)的状态。
这有助于理解你本地的工作和公共工作的差别 -- 这是和别人分享工作成果最重要的一步。
特别的属性:在你checkout时,自动进入分离的HEAD状态。 原因是Git要求,不能在远程跟踪分支上直接写代码,需要先在其他地方写好代码后,更新到远程仓库对应的位置,远程跟踪分支才会更新。
git checkout o/master;
git commit;
在有新的commit提交记录后,o/master不会同步更新,会和HEAD分离。
⚠️,Head总是指向当前分支上最后一次的提交记录,但这里是例外。
o/master只有在远程仓库中对应的分支更新后,才会更新。
格式:<remote name>/<branch name>
简称: o/
默认: remote name 是 origin
Git Fetch
从remote repertory获得数据。
当从远程仓库获得数据时, 远程分支会自动更新,以反应最新的远程仓库。
git fetch会做的事情:
- 从远程仓库下载本地仓库缺少的commit记录
- 更新远程跟踪分支 如 origin/feature_branch
- ⚠️不会更新你的master分支和其他分支,也不会修改你磁盘上的文件。
- 因此,不是说git fetch后本地仓库就和远程仓库同步了,这是❌的想法。
- git fetch只是单纯的下载服务。
//取指定的分支或tags,下载所有需要的commits和文件 git fetch <repository> [<refspec>...] // 取所有remote branch git fetch --all [<options>]
// 试运行,就是一次彩排,看看会出现什么结果
git fetch --dry-run
使用git fetch 同步远程仓库:
git fetch origin
//会显示我们下载的branchs
//a1e8fb5..45e66a4 master -> origin/master
//a1e8fb5..9e8ab1c develop -> origin/develop
//* [new branch] some-feature -> origin/some-feature
如果想要查看上游master增加了什么commit,可以运行git log命令,并使用origin/master进行检索:
git log --oneline master..origin/master
然后批准这些变化并合并它们到你的本地master分支:
git checkout master git log origin/master
//现在origin/master和master分支指向同一个commit点了,
//并且你和上游upstream 开发同步了。 git merge origin/master
Git Pull
下载下来后,把远程分支合并到本地的方法:
- git cherry-pick o/master
- git rebase o/master
- git merge o/master
- 等等
git pull 就是直接一步完成2个命令。即git fetch和git merge的方便代码。
等同于:git fetch 和git cherry-pick o/master
效果等同于:git fetch, git rebase master o/master, git rebase o/master master。但结构是线性的,因为使用git rebase是线性合并,o/master会指向新C3'
模拟团队合作:下载。
- git fetch
- 在本地的master分支新增了commit
- git merge o/master, 合并远程分支到本地的master。
Git Push
当本地仓库被修改,需要执行git push操作来分享这个修改的代码给team members:
git push <remote> <branch>
git push <remote> -all
如果不带任何参数,会使用push.default的设置。他的默认值是正在使用的Git的版本,推送前最好检查一下这个配置
偏离的困惑
假如周一你克隆了一个仓库,然后开发某个新功能。到周五时,可以提交到远程仓库了。但是,
这周你的同事写了一堆代码并修改了你还在使用的API。这些变动让你的新开发的功能不可用。
但他已经提交推送到远程仓库了。你的工作变成了基于旧版本的代码,已和远程仓库的最新的代码不匹配了。
这时,Git不会允许你push,它会要求你先合并远程最新的代码,然后你才能分享你的工作。
这就是历史偏移
需要:
- git fetch
- git rebae o/master#这里可能你的新增功能,会失效,
- 你需要先修改代码,然后commit。最后:
- git push
- git fetch#更新本地仓库中的远程分支
- git merge o/master#这会产生新的commit记录。C4
- git push
--rebase选项:
git pull --rebase , 等同于用git rebase合并远程分支,而默认是git merge
小结:工作流程: fetch, rebase/merge, push
强制push
Git 为了防止你覆盖中央仓库的历史,会拒绝你会导致非快速向前合并的推送请求。
--force
这个标记覆盖了这个行为,让远程仓库的分支符合你的本地分支,删除你上次 pull 之后可能的上游更改。
只有当你意识到你刚刚共享的提交不正确,并用 git commit --amend
或者交互式 rebase 修复之后,你才需要用到强制推送。
⚠️ 但是,你必须绝对确定在你使用 --force
标记前你的同事们都没有 pull 这些提交。
关于origin和它的周边 --Git 远程仓库高级操作
推送push 主分支
大型项目,开发人员会在特性分支上工作,工作完成后只做一次集成。
但有些开发者只在master上push,pull。这样master总是最新的,始终与远程分支保持一致。
- 将特性分支集成到master上。
- push并更新远程分支
为什么操作远程分支不喜欢用merge, 见仁见智
- 喜欢干净的提交树,用rebase
- 喜欢保留提交历史的,用merge
远程跟踪分支:remote-tracking branches
Git 设置了master和o/master的关联。
- pull时,commit记录会被先下载到远程分支,如:origin/master上,之后会再合并到master分支。
- push时,我们把工作从master推到远程仓库中的master分支,同时更新远程分支origin/master。
它们的关联关系,是由"remote tracking" 属性决定的。
master被设定为跟踪o/master。
当你克隆远程仓库时,Git会为远程仓库的每个分支都设定一个远程分支,然后再在本地创建一个和远程仓库
中的分支一样的本地分支。
克隆完成后,你会得到本地分支,如果没有就是空白。
这也解释了在克隆时会看到下面的输出:
local branch "master" set to track remote branch "o/master"
本地分支“master”设置跟踪远程分支 "o/master"
可以指定"remote tracking" 属性属性
让任意分支跟踪o/master,然后该分支就会像master分支一样得到隐藏的push目的地和merge的目标。
这意味着你可以在分支XXX上指向git push, 将工作推送到远程仓库的master分支上。
两种设置方法:
- git checkout -b XXX o/master
- 如果已经有了XXX分支,则使用git branch -u o/master XXX
Git push的参数
默认Git通过当前checkout分支的属性来确定远程仓库和要push的目的地。
我们也可以明确指定push的参数:
git push <options> <remote-Repository-name> <place>
例子:
git push --set-upstream origin master:
把当前分支push, 并设置远程仓库origin为upstream。
解释:
这行代码一般用在创建一个远程链接并同步上传数据:
⭠ master ⮀ git remote add origin https://github.com/xxxxxx/yyyyyy.git
//链接远程仓库origin.
⭠ master ⮀ git push -u origin master //Branch master set up to track remote branch master from origin. //本地分支master已经开始追踪远程仓库origin的master分支!
<place> 详细解释:
可以分解为<localbranch-source>:<destination>
即本地分支source,提交到远程的另一个分支destination。
<source>可以是任何commit记录位置。
如果<destination>在远程仓库中并不存在,则会在远程仓库中新建这个分支。
例子: git push origin localbranch:remotebranch
强制执行一次non-fast-forward merge
git push origin --force
⚠️,只有绝对确定你正在做的才这么用!
--all选项:推送所有分支:
git push <remote> --all
把标签也推送上去,默认tags是不推送的。
git push <remote> --tags
Amended force push
git commit --amend 用于更新提交点,
本质就是把commit原先的改变和更新的内容存入一个新的commit点,扔掉原来的commit点。
但是如此,git push 会导致失败,因为Git会发现Amended的commit点和远程的commit点的内容是分离的。
此时需要使用--force选项来push一个amended commit!
# make changes to a repo and git add git commit --amend
#此时如果直接git push 会报告错误❌:
# ! [rejected] master -> master (non-fast-forward)
#需要使用--force
git push --force origin master
注意⚠️没有同步更新remote-tracking branch!
当本地master分支和origin/master在一起时,执行一次修改并commit,再git push, 这时origin/master不能同步更新,需要再次执行一次同步命名。git push ,git fetch都可以.
再次提交一次:
#修改一些文件并提交 git commit -am 'd'
可以发现origin/master是处于4ebc769,
如果使用
git fetch
origin/master会更新到最新commit点。
删除远程分支或者tag
实际是推送一个空的分支替换远程一个分支,相当于删除远程的这个分支。
//查看本地和远程的分支 git branch --all //删除本地分支 git branch -D branch_name //删除远程分支 git push origin origin :branch_name
Git remote
如⬆️代码,git remote add origin <url>, 创建了本地和远程仓库origin的链接!
本地./.git/config文件储存了这条记录信息:
[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [remote "origin"] url = https://github.com/chentianwei411/practice.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master config (END)
git push -u origin master命令,则添加了[branch "master"]这个记录。
所有git remote的操作,都会记录在./.git/config文件中!
//执行git remote remove origin命令: //会断开本地和远程的连接:
//所有对跟踪远程的分支的设置和对远程的配置设置被移除!
//结果: [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [branch "master"] config (END)
再次使用git remote add origin <url>, 即可再连接上远程仓库:见./.git/config文件增加的代码:
[branch "master"] [remote "origin"] url = https://github.com/chentianwei411/practice fetch = +refs/heads/*:refs/remotes/origin/*
git remote get-url --all origin
列出所有的远程连接URLs.
git remote show <name>
这条命令会给出详细的关于一个远程连接的配置信息.
先git remote -v命令,查看。然后用git remtoe show <name>:
- show origin,
- show upstream ,
- shou other_users_repo
* remote origin Fetch URL: https://github.com/chentianwei411/practice Push URL: https://github.com/chentianwei411/practice HEAD branch: master Remote branch: master tracked Local ref configured for 'git push': master pushes to master (up to date)
git remote prune [--dry-run] origin
Deletes all stale remote-tracking branches under <name>.
删除所有过期的远程跟踪分支, 即origin/xxx。
⚠️origin/xxx是存在于本地仓库的,用于接收从远程仓库pull下来的数据。然后再merge到xxx分支。
These stale branches have already been removed from the remote repository
referenced by <name>, but are still locally available in "remotes/<name>".
这些过期的分支已经从远程仓库origin移除了,但是在本地仓库仍然可以在remotes/origin中找到:
⭠ master ⮀ git branch --all hotfix * master try remotes/origin/hotfix remotes/origin/master
--dry-run, 列出什么分支会被剪除掉prune!
⮀ git remote prune --dry-run origin Pruning origin URL: https://github.com/chentianwei411/practice * [would prune] origin/hotfix
⮀ git remote prune origin Pruning origin URL: https://github.com/chentianwei411/practice * [pruned] origin/hotfix
再次使用git branch -a命令查询所有分支,会发现remotes/origin/hotfix分支已经被删除!!
显示你的remotes
git remote
git remote -v
//-v选项,是verbose的意思 // 会列出标记的仓库名字和相关信息,合作的仓库URL. origin git@bitbucket.com:origin_user/reponame.git (fetch) origin git@bitbucket.com:origin_user/reponame.git (push) upstream https://bitbucket.com/upstream_user/reponame.git (fetch) upstream https://bitbucket.com/upstream_user/reponame.git (push) other_users_repo https://bitbucket.com/other_users_repo/reponame (fetch) other_users_repo https://bitbucket.com/other_users_repo/reponame (push)
添加远程仓库
当你添加了一个远程仓库。 你就可以使用仓库名字origin作为<url>的简写,在其他git命令上使用了。
这是因为./.git/config中记录了url的信息:
[remote "origin"] url = https://github.com/chentianwei411/practice fetch = +refs/heads/*:refs/remotes/origin/*
Git Fetch origin <remotebranch-source>
和git push正相反,Git会查找remotebranch的本地远程分支,并下载到本地的远程分支。
这样不会弄坏你的本地同名分支。
也可以另外指定远程仓库的分支下载到哪个本地的分支<destination>,
这样会直接下载到某个分支上(不是远程分支),⚠️开发人员很少这么做,有风险。
如果本地没有一个bar分支:
git fetch origin XX:bar的结果是, 会在本地自动创建一个bar分支,用来存储远程仓库的commit信息。
如果只有git fetch:
Git会下载远程仓库中所有的提交记录到各个远程分支...
省去<source> 的特殊用法:
删除远程仓库的分支:
git push origin :foo #推送一个空的source到远程仓库,如果远程仓库有foo分支,这个分支将被删除。
git fetch origin :bar #下载一个远程仓库没有的空分支给本地,本地创建一个bar分支。
Git Pull参数
git pull origin foo 相当于:
git fetch origin foo; git merge o/foo
⚠️git pull origin master 会先下载到o/master, 然后merge到当前checkout检出位置。
所以,使用git pull要不熟练,要不就别用。
git pull也可以使用<source>:<destination>:
如:git pull origin master:foo
- 如果本地没有foo, 先在本地创建一个foo分支,
- 然后从远程仓库下载master分支中的提交记录并合并到foo分支
- 然后再merge到当前的检出分支checkout分支上。
git clean
将未跟踪的文件从你的工作目录working directory中移除。
未跟踪文件是新增到在工作目录但尚未添加到用 git add添加到repo's index。
它只是提供了一条捷径,因为用 git status
查看哪些文件还未跟踪然后手动移除它们也很方便。
和一般的 rm
命令一样,git clean
是无法撤消的,所以在删除未跟踪的文件之前想清楚,你是否真的要这么做。
git clean
命令经常和 git reset --hard
一起使用。
⚠️记住,reset 只影响被跟踪的文件,所以还需要git clean来清理未被跟踪的文件。这个两个命令相结合,你就可以将工作目录回到之前特定提交时的状态。
# 先测试一下比较好-n ,--dry-run
git clean --dry-run
# 移除当前目录下未被跟踪的文件。-f(强制)标记是必需的。
# 不会删除未跟踪的目录directory和.gitignore中的文件。 git clean -f # 移除未跟踪的文件,但限制在某个路径下 git clean -f <path> # 测试移除未跟踪的目录directory git clean -dn
# 移除未跟踪的目录
git clean -d
⚠️请牢记,和 git reset
--hard 一样, git clean
是仅有的几个可以永久删除提交的命令之一,所以要小心使用
栗子:
# 编辑了一些文件 # 新增了一些文件 # 『糟糕』 # 将跟踪的文件回滚回去, 新增文件从head, index, 工作目录中删除了!!修改的文件恢复到之前的代码!! git reset --hard # 移除未跟踪的文件,先测试-n, -dn
git clean -n git clean -df
改变旧的或者多个commits: git rebase
rebase改基。从一个分支移动到另一个分支,即改基。
移动或联合一系列的提交点到一个新的base commit。实际是创建了一系列新的提交点。
效果和目的:
⚠️不要用在public commit。
- 让你修改你的历史, 并且可交互的改基允许你如此做而不留下杂乱的痕迹。
- 让你在修改错误和从新定义你的任务后,仍然保持了一个干净,线性linear的程序历史。
在真实的场景:
- 在主分支发现bug.。一个功能分支的功能由此坏掉。
- 开发者检查主分支历史git log。 因为clean history,开发者能快速的找出project的历史。
- 开发者使用git log还是不能识别出bug是何时插入的introduced。所以他执行了git bisect
- 因为git history很干净, git bisect有一个refined set of commits 来比较。开发者快速的找到了这个插入bug的commit点。
例子:
当你在一个功能分支上进行开发时,主分支发现一个bug。
新增一个hotfix分支,用于fix bug。 在搞定bug后, 把bug分支合并到master。
你想要在你的功能分支上使用最新版本的主分支,但你想要保持你的功能分支的历史干净,即好似你一直在最新版的master上开发功能分支。
# 开始新的功能分支 git checkout -b new-feature master # 编辑文件 git commit -a -m "Start developing a feature" #在 feature 分支开发了一半的时候,我们意识到项目中有一个安全漏洞:---- # 基于master分支创建一个快速修复分支 git checkout -b hotfix master # 编辑文件 git commit -a -m "Fix security hole" # 合并回master git checkout master git merge hotfix git branch -d hotfix #将 hotfix 分支并回之后 master,我们有了一个分叉的项目历史。---- git checkout new-feature git rebase master #它将 new-feature 分支移到了 master 分支的末端, #然后在master上进行标准的快速向前合并了: git checkout master git merge new-feature
Rebasing的一个常用方式是:把upstream的变化集成到你的本地仓库。
移动整个功能分支⬆️
git rebase -i <base>
用 -i
标记运行 git rebase
开始交互式 rebase。交互式 rebase 给你在过程中修改单个提交的机会,而不是盲目地将所有提交都移到新的基上。你可以移除、分割提交,更改提交的顺序。
讨论
交互式 rebase 给你了控制项目历史的完全掌控。它给了开发人员很大的自由,因为他们可以提交一个「混乱」的历史而只需专注于写代码,然后回去恢复干净。
大多数开发者喜欢在并入主代码库之前用交互式 rebase 来完善他们的 feature 分支。他们可以将不重要的提交合在一起,删除不需要的,确保所有东西在提交到「正式」的项目历史前都是整齐的。对其他人来说,这个功能的开发看上去是由一系列精心安排的提交组成的。
执行:git rebase -i master 后出现vim的交互界面:
- 如果删除所有pick,然后:wq保存退出,相当于取消rebase 这条命令。提示:Nothing to do
- 如果直接:q退出,不做任何修改,相当于执行了git rebase master命令。
- 提示:Successfully rebased and updated
如果只pick 37c8c61,第一行。则提示成功:
Successfully rebased and updated refs/heads/b2.
git checkout master
git merger b2
⚠️另外2个pick就被分支b2扔掉了!!
如果知道这2个commit的id,就可以进入它们:
git chekcout xxxx
得到提示信息:You are in 'detached HEAD' state. 你处于分离的HEAD!
可以把这2个提交点合并到b2, 再把b2合并到master。
Rebasing的其他几个有趣的命令:
- edit
- reword -可以重写提交信息message
- squash -把当前的commit合并到上一个commit, 并提示你重写提交message
- fixup -和squash一样,不会重写提交message.
edit 9a29b81
如可以使用命令edit, 代替pick,当:wq保存退出vim后,提示
⭠ b3 ⮀ git rebase -i master Stopped at bb28411... change yangcheng You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
➦ bb28411 ⮀
然后你可以在这个提交点上做修改,然后
➦ bb28411± ⮀ git add . ➦ bb28411± ⮀ git commit --amend [detached HEAD 36f12d6] change yangcheng Date: Sun Nov 25 11:45:44 2018 +0800 1 file changed, 1 insertion(+), 1 deletion(-) ➦ 36f12d6 ⮀ git rebase --continue Successfully rebased and updated refs/heads/b3. ⭠ b3 ⮀
squash 9a29b81
合并后放入新增的一个commit中。 这是rebase的主要功能!!
原因:
大量的细小的改动,每个改动都是一个提交。导致仓库的history看起来很乱。
许多这样的commit并没有实际地给你的仓库history增加任何价值
它们弄乱了blame(新旧版本的对比), make bisects take longer and make the history hard to navigate.
⭠ b4 ⮀ git rebase -i master
#此时进入vim编辑器。重写提交message,然后:wq
[detached HEAD fac0e93] change content and guangzhou Date: Sun Nov 25 12:42:45 2018 +0800 2 files changed, 3 insertions(+), 1 deletion(-) Successfully rebased and updated refs/heads/b4.
然后:
git checkout master
git merge b4
git branch -d b4
还有另一种使用squash的方式:
git merge --squash <commit> 可以把一个分支合并为一个commit, 然后执行
git commit -m 'squash ...'
最后删除分支b3: git branch -D b3
安全网:git reflog
Git保持对分支的tip的追踪!这种机制叫做reflog(reference logs), 引用日志。
Git 用引用日志这种机制来记录分支顶端的更新和其他commit引用。
它允许你回到那些不被任何分支或标签引用的commits。在重写历史后,reflog包含了分支旧状态的信息,有需要的话你可以回到这个状态。
每次当你的分支tip被任何原因所更新(包括切换分支,pulling in new changes, 重写历史或者仅仅是增加新的commits), 一个新的entry将被增加到reflog。
reflog提供了一张安全网,所有的分支顶端的变化都会被记录。
另外, reflog提供了到期日。默认设置expiration time是90天。
用法
git reflog # 是git reflog show HEAD的简写
显示:
fac0e93 HEAD@{0}: merge b4: Fast-forward 6f70a12 HEAD@{1}: checkout: moving from b4 to master fac0e93 HEAD@{2}: rebase -i (finish): returning to refs/heads/b4 fac0e93 HEAD@{3}: rebase -i (squash): change content and guangzhou 0481ff2 HEAD@{4}: rebase -i (pick): change content 6f70a12 HEAD@{5}: rebase -i (start): checkout master 3bc3338 HEAD@{6}: checkout: moving from master to b4 。。。
#fac0e96,第一行是最新的一次relog。
还可以用:
git reflog --relative-date
fac0e93 HEAD@{3 hours ago}: merge b4: Fast-forward
6f70a12 HEAD@{3 hours ago}: checkout: moving from b4 to master
fac0e93 HEAD@{4 hours ago}: rebase -i (finish): returning to refs/heads/b4
...
#显示相对现在,每条记录发生的时间!
默认,git reflog会输出HEAD ref。但也可以显示其他ref。
如其他分支,tags, remotes, Git stash都可以被引用。
引用中的语法格式:name@{qualifier}
获得全部的reflog:
git reflog show --all
查看具体某一个分支的引用:
⭠ b5 ⮀ git reflog show b5 //显示: 905b28c b5@{0}: commit: change bj fac0e93 b5@{1}: branch: Created from HEAD (END)
如果使用过git stash命令储存了缓存区的文件, 并且还未取出,则可以用git reflog命令查看记录:
git reflog stash
//显示
bd1f5f7 stash@{0}: WIP on b5: 905b28c change bj
另外, 可以使用git diff: Show changes between commits, commit and working tree
//git diff stash@{0} otherbranch@{0}
⭠ b5 ⮀ git diff stash@{0} b5@{0}
//显示 diff --git a/beijing.txt b/beijing.txt index 7719339..6815264 100644 --- a/beijing.txt +++ b/beijing.txt @@ -1,3 +1,4 @@ +123 hahaha hello hello this is a beautiful city! (END)
一般用不到,使用图形编辑器sourcetree,就可直观的看一个commit的变化。
但用代码,可以有更丰富的细节设置,如加上一个到期的时间:
git diff master@{0} master@{1.day.ago}
具体可见git diff --help
恢复丢失的commits
Git 从不真地丢失任何东西,继续执行了历史重写操作,如rebasing, commit amending。
git log的搜索功能很强大,有丰富的设置可以查看各种情况。
例如:
分支b5有4个commit,master有1个commit超过b5
⭠ b5 ⮀ git rebase -i master //进入vi编辑器。把第4行的pick改成squash, :wq。 //terminal: [detached HEAD 55ce6f9] change one Date: Sun Nov 25 17:06:31 2018 +0800 2 files changed, 3 deletions(-) Successfully rebased and updated refs/heads/b5.
结果附加到master上的commit只有3个,最后一个是合并的commit
git log --pretty=oneline //也只能看到新增的3行log 55ce6f9c6f60d80f9b042f5f8a99556333d5854a change one 23bd2a5ead2b5a19d6bcc8667fbd23c636135264 change sth c4e32fb82669aa16ac60e2b9f8a1bc488a366be5 change bj
似乎b5分支的最后2个commit,由于squash,导致无法找到了!其实不然:
使用git reflog命令,即可看到最新的ref日志⬇️:
55ce6f9 HEAD@{0}: rebase -i (finish): returning to refs/heads/b5 55ce6f9 HEAD@{1}: rebase -i (squash): change one 07e7fbf HEAD@{2}: rebase -i (pick): 1 23bd2a5 HEAD@{3}: rebase -i (pick): change sth c4e32fb HEAD@{4}: rebase -i (pick): change bj eb80dcf HEAD@{5}: rebase -i (start): checkout master 1c226ea HEAD@{6}: checkout: moving from master to b5
可以看到从start到finish的全部细节:一部了然!!
- 4行绿色是4个commit点
- 第2行操作的方式是squash。
如果想要恢复到执行git rebase之前,可以使用:
git reset HEAD@{6}
保存改变的5个命令:
-
git add
-
git commit
-
git diff (比较commit的不同,可以用可视化工具:sourcetree, 或者上远程仓库,在网页上看。)
-
git stash(上面已经介绍,把staged和未staged的文件储存起来)
-
.gitignore :这里就介绍它。
.gitignore
Git从copy的行为上分类: 把文件分成3种类别:
- tracked -一个文件之前被staged or commited
- untracked -一个从未被staged or commited的文件,一般是新建的文件。
- ignored -不加入tracked的文件。
包括:
- 独立缓存/packages
- build output directories:如 /bin, /out, /target
- compiled code: .pyc, .class文件
- 在运行时产生的文件: .log, .lock, .tmp
- 隐藏的系统文件: .DS_store, Thumbs.db
- 个人的IDE配置文件
一般可以在~/.gitignore文件内查看仓库中被忽略的文件:
当你有新的文件需要被忽略,.gitignore文件必须手动编辑和提交。
Global Git ignore rules
你需要自己建立.gitignore, 并设置core.excludesFile属性。
$ touch ~/.gitignore $ git config --global core.excludesFile ~/.gitignore
Shared .gitignore files in your repository
通常,Git ignore rules被定义在仓库根目录的.gitignore文件中。
但也可以在你的仓库中的不同的目录中定义多个.gitignore文件。
然而最简单的方法还是在根目录创建.gitignor文件,因为它本身也是被版本控制的,当你push,就可以和团队共享。
Personal Git igonore rules
你也可以定义个人的ignore模式,这在特殊的.git/info/exclude文件中。
它不会被版本控制!所以你可以做一些私事!
如何忽略一个之前commit过的文件?
从仓库删除这个文件,然后增加一个.gitignore rule.
使用 --cached选项和git rm
git-rm - Remove files from the working tree and from the index. --cached: unstage and remove paths only from the index. 但会保留在working directory!
例子:
$ echo debug.log >> .gitignore $ git rm --cached debug.log //提示:rm 'debug.log' $ git commit -m "Start ignoring debug.log"
[master 6d51c73] Start ignoring debug.log
1 file changed, 1 deletion(-)
delete mode 100644 debug.log
.gitignore自身是被追踪的!,需要git add .gitignore
Committing an ignored file
可以强制一个被忽略的文件被提交到仓库,使用-f选项即可:
$ cat .gitignore *.log $ git add -f debug.log $ git commit -m "Force adding debug.log"
当然,这不是一个明显的,可以让团队成员了解的方法:改为:
$ echo !debug.log >> .gitignore $ cat .gitignore *.log !debug.log $ git add debug.log $ git commit -m "Adding debug.log"
*.log表示所有带.log后缀的文件都会被忽略,但是!dubug.log表示这个文件不会被忽略!
Stashing an ignored file
使用git stash回你历史的存储本地的变化,并在之后用git stash pop取回。
但是默认git stash忽略.gitignore中的文件。
使用--all选项,可以stash 忽略的和未跟踪的文件。
具体git stash 说明文档见:https://www.atlassian.com/git/tutorials/git-stash/#stashing-untracked-or-ignored
Debugging .gitignore files
如果你有复杂的.gitignore模式或者有多个.gitignore文件,那么想要知道一个文件为何被忽略,就比较麻烦。
你可以使用git check-ignore -v <file-name>来看什么模式让这个文件被忽略
git rm
用于把一个文件从a Git repository中移除(working directory和index, 或者只从index中移除)。
某种程度上可以认为是git add命令的相反命令。
git rm命令可以用于移除单独的文件或一组文件集合。
它的主要功能是从Git index中移除跟踪的文件。
另外它也能同时从staging index和 working directory移除文件。
注意git rm 不会移除branches。
选项--cached:
这个选项的用途是取消文件的tracking,但保留这个文件在working directory!
案例见上.gitignore
如何undo git rm?
git rm命令需要执行git commit来生效。
git rm将更新staging index 和 working directory,
⚠️这些变化不会马上产生效果,只有当新的commit被创建,这些变化才会被添加到commit history中。
这就意味着git rm可以恢复。(符合git 作为时光机的原则)执行git reset HEAD
⚠️git rm只能用在当前current branch的文件!
为什么使用git rm来代替rm
当一个被跟踪的文件被rm命令执行,Git仓库会识别这个标准的壳命令rm。
Git仓库会更新working directory来反应这个移除。但它不会更新staging index。
所以需要额外的git add命令,把这个移除的改变添加到staging index。
git rm 是一个方便的shorcut。它将同时更新working directory and the staging index 。
例子:取消文件shanghai.txt的跟踪,并删除这个文件。
⭠ master ⮀ git rm shanghai.txt 提示:rm 'shanghai.txt'
⭠ master± ⮀ git status 提示: On branch master Your branch is ahead of 'origin/master' by 4 commits. (use "git push" to publish your local commits) Changes to be committed: (use "git reset HEAD <file>..." to unstage) deleted: shanghai.txt #帮你使用了git add命令。
⚠️sourcetree,可以使用discard,取消一个文件的修改或删除。就是恢复操作!
等同命令:
git checkout <file>
git checkout -- <file> #加上-- 防止文件名和branch名字重复导致❌。
Unstage a file:
git reset HEAD <file>
git rm --cached <file> #删除staging保留working direotry中的文件。
小节:
git rm --cache <file>用于不再对某个文件进行tracking。之前commi过的文件,不再跟踪!
git clean -df 删除未跟踪的文件和目录
git reset --hard 回滚跟踪的文件,即恢复文件到未修改的状态并不再跟踪,新增的文件则从工作目录删除。
git fetch 命令是如何与remote branches 合作的?(深入分析)
Behind the scenes,在幕后, ./.git/objects目录中,Git存储了所有的commits, 包括local和remote。
~/practice/.git/objects ⮀ ⭠ master ⮀ ls 00 0d 1b 24 38 40 51 60 70 7e 8b 9b a8 bb c7 d4 e7 fd 02 11 1c 27 3b 43 55 65 76 81 90 9d ab bd c8 de eb info 04 15 1f 2d 3c 49 59 68 77 84 92 a1 ad be ce e0 f0 pack
通过使用branch refs(就是commit点的?), GIT让分支commits清楚的分开。
远程分支的Refs, 储存在./.git/refs/remotes/
//?两条命令都可以查看 git branch -r git branch --all
本地分支的Refs,储存在./.git/refs/heads/
~/practice/.git/refs/heads ⮀ ⭠ master ⮀ ls hotfix master try
less try可以看到:
ad64e76234f1dee8ea19618d4f5964edbc20bd36
try (END)
git log
针对commit点的查询和搜索功能:
//在屏幕上显示几行 git log -n <limit> //单行显示,用于全局的概览, 内容包括id和messages git log --oneline
// 显示每个commit的文件改变的概览 git log --stat //使用-p选项,看每个提交点的patch。比--stat更详细的每个commit的区别 git log -p
通过对message进行匹配来搜索,<pattern>可以是字符串和正则表达式:
git log --grep="<pattern>"
看一个特定文件的提交历史:
git log <file>
显示一个类似sourcetree的结构:
git log --graph --decorate --oneline
⚠️,选项可以一起使用。
git blame
- The high-level function of
git blame
is the display of author metadata attached to specific committed lines in a file. This is used to explore the history of specific code and answer questions about what, how, and why the code was added to a repository.
用于比较commit点的历史代码。信息还包括添加代码的作者名字,解释。
合作者可以查看这些历史代码。
一般使用UGI网页,如在线git上查看这些历史代码。
例子:example
git clone https://kevzettler@bitbucket.org/kevzettler/git-blame-example.git cd git-blame-example
//使用git log查看所有commit信息。
git log
git blame只用于单独的文件的历史commit点的比较。需要提供file-path来输出信息。
//输出帮助信息 git blame //输出文件的commit历史: 即每个commit的代码的增减。 git blame README.md