最近翻看了之前的笔记,顺便又修改了一下,发现好久没更新文章了,顺便拿出来凑一篇
一、简介
Git是目前世界上最好的版本控制系统。
Git最初是用来管理Linux源码的,由Linus开发的。
-
linux安装
sudo apt-get install git
-
mac 安装
一是安装homebrew,然后通过homebrew安装Git,具体方法请参考homebrew的文档:brew.sh/。
$ brew install git 复制代码
第二种方法更简单,也是推荐的方法,就是直接从AppStore安装Xcode,Xcode集成了Git,不过默认没有安装,你需要运行Xcode,选择菜单“Xcode”->“Preferences”,在弹出窗口中找到“Downloads”,选择“Command Line Tools”,点“Install”就可以完成安装了。
-
windows 安装git
在Windows上使用Git,可以从Git官网直接下载安装程序,然后按默认选项安装即可。
集中式与分布式对比
集中式: 版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。
特点:1、联网、局域网
分布式: 首先,分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库。其次,修改完成后将修改内容推送给对方就实现了版本控制。
分布式版本控制,由于一些原因(不能访问等原因)。因此,分布式版本控制系统通常也有一台电脑充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
假如你还有10个同事,你每一次更改都要提交10次,其他同事有更改也要分别向我们提交,所以我们说干脆找一台固定电脑(服务器)用来统一规定把修改推给这台电脑,这样只需要提交1次就行了,其他人去这台机器上同步就好了。
集中式和分布式的区别是:
- 你的本地是否有完整的版本库历史!
- 假设SVN服务器没了,那你丢掉了所有历史信息,因为你的本地只有当前版本以及部分历史信息。
- 假设GitHub服务器没了,你不会丢掉任何git历史信息,因为你的本地有完整的版本库信息。你可以把本地的git库重新上传到另外的git服务商。
版本控制的选择:
- 代码等纯文本的存储适合用git
- office等二进制的文件适合用SVN
Git对非纯文本内容支持并不好,虽然能管理版本,但是无法方便的diff,merge。
Git一般对文本文件支持比较好,但word、excel等office文档是二进制文件。用Git的一个场景是,多人同时编辑一个文件,可能会产生文件编辑的冲突。如果是文本文件,那么最后合并的时候会看到每个人修改的地方,冲突也会标明,有助于你修复冲突。而二进制文件没有这样的功能。其实,如果是普通的office文档的话,可以使用有道云协助、腾讯文档等工具,它们可以标注出word或其他office文档的修改痕迹。
所有的版本控制都只能针对文本内容,像word这样的二进制内容,所谓版本,就是文件快照,不能比较差异,只能算带备份的网盘
二、创建版本库
版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”
-
创建文件夹
zbt:WorkSpace zbt$ mkdir learngit zbt:WorkSpace zbt$ cd learngit zbt:learngit zbt$ pwd /Users/zbt/Documents/WorkSpace/learngit // pwd 显示目录全称 复制代码
-
初始化(创建)仓库 git init
zbt:learngit zbt$ git init Initialized empty Git repository in /Users/zbt/Documents/WorkSpace/learngit/.git/ zbt:learngit zbt$ ls -ah . .. .git 复制代码
-
添加文件到版本库 git add xxx
zbt:learngit zbt$ open readme.txt zbt:learngit zbt$ git add readme.txt 复制代码
-
提交文件 git commit -m “explore”
zbt:learngit zbt$ git commit -m "wrote a readme file" [master (root-commit) baf57ac] wrote a readme file 1 file changed, 1 insertion(+) create mode 100644 readme.txt 这里可以看到的信息是提交了主仓库 复制代码
-
修改及查看文件 git status \ git diff
zbt:learngit zbt$ git status On branch master nothing to commit, working tree clean zbt:learngit zbt$ open readme.txt zbt:learngit zbt$ git status On branch 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: readme.txt ------ diff --git a/readme.txt b/readme.txt index 01f4d2b..f1a8b98 100644 --- a/readme.txt +++ b/readme.txt @@ -1 +1,3 @@ -Git test \ No newline at end of file +Git test + +Git second line \ No newline at end of file zbt:learngit zbt$ 复制代码
-
再次提交
zbt:learngit zbt$ git add readme.txt //(这个命令是必要的,因为add只负责添加当时的修改,之后的不管) zbt:learngit zbt$ git commit -m "second commit" [master c8225b5] second commit 1 file changed, 3 insertions(+), 1 deletion(-) 复制代码
三、版本回退
zbt:learngit zbt$ git log --pretty=oneline
5823e442831f54bbe6996afd7262d31bb5662152 (HEAD -> master) thrid commit
c8225b571a416bcca9700cde70fa1cd71e9e172e second commit
baf57ac7f2dff4411607688d255a259aa42f26c9 wrote a readme file
复制代码
和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且你看到的commit id和我的肯定不一样,以你自己的为准。
zbt:learngit zbt$ git reset --hard HEAD^
HEAD is now at c8225b5 second commit
zbt:learngit zbt$ open readme.txt
zbt:learngit zbt$ git log --pretty=oneline
c8225b571a416bcca9700cde70fa1cd71e9e172e (HEAD -> master) second commit
baf57ac7f2dff4411607688d255a259aa42f26c9 wrote a readme file
(HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id。)
复制代码
可以看到版本回退到了上一个版本。
zbt:learngit zbt$ git reset --hard 5823
HEAD is now at 5823e44 thrid commit
zbt:learngit zbt$ git log --pretty=oneline
5823e442831f54bbe6996afd7262d31bb5662152 (HEAD -> master) thrid commit
c8225b571a416bcca9700cde70fa1cd71e9e172e second commit
baf57ac7f2dff4411607688d255a259aa42f26c9 wrote a readme file
复制代码
那如果想又退回到底三次提交这里呢,那么你需要,知道commitid
zbt:learngit zbt$ git reset --hard 5823e
HEAD is now at 5823e44 thrid commit
zbt:learngit zbt$
复制代码
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向第二次提交的内容
Git提供了一个命令git reflog用来记录你的每一次命令
git reflog
84b708a08 HEAD@{6}: merge origin/develop: Fast-forward
0946357c9 HEAD@{7}: reset: moving to HEAD
0946357c9 HEAD@{8}: commit: fix: APP-627 延后切换tab为report,减少发生该情况概率
3e7838651 HEAD@{9}: commit: fix:
复制代码
git reset 和 git revert 对比:
如果已经有A -> B -> C,想回到B:
方法一:reset到B,丢失C:
A -> B
--soft:保留源码,只回退commit信息到某个版本,不涉及index(stage)的回退。如果还需要提交,直接commit即可。
--mixed:会保留源码,只是将git commit和index的信息回退到了某个版本。(git reset默认的就是--mixed模式,即git reset等价于git reset --mixed)
--hard:源码也会回退到某个版本,commit和index都会回退到某个版本。(注意这种方式是会改变本地代码仓库源码)
方法二:再提交一个revert反向修改,变成B:
A -> B -> C -> B
C还在,但是两个B是重复的
看你的需求,也许C就是瞎提交错了(比如把密码提交上去了),必须reset
如果C就是修改,现在又要改回来,将来可能再改成C,那你就revert
复制代码
四、工作区和暂存区
工作区: 工作文件的目录
版本库(Repository): 工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。
第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。
我们对文件作出修改之后那么使用add命令后,修改放到暂存区(Stage)
使用commit命令后出,修改就从暂存区提交到master分支
管理修改
如果进行第一次修改 -> git add -> 第二次修改 -> git commit,这种情况下,只会提交一次add的修改。因为git commit只负责把暂存区的修改提交到分支。add是把修改放入暂存区。
撤销修改
命令git checkout — readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:
一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit或git add时的状态。
git checkout — file命令中的–很重要,没有–,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout命令。
用命令git reset HEAD 可以把暂存区的修改撤销掉(unstage),重新放回工作区
在Android studio中可以使用Rollback将暂存区的修改放入工作区,并且将修改全部撤销。
删除文件
rm test.txt
复制代码
五、远程仓库
目前远程仓库有许多github、gitee、aliyuncode。
由于本地仓库和远程仓库之间传输是通过SSH加密。那么需要设置SSH Key。
第1步:创建SSH Key。在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsa和id_rsa.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:
$ ssh-keygen -t rsa -C "youremail@example.com"
复制代码
你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。
如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有id_rsa和id_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。
第2步:登陆XXX,打开“Account settings”,“SSH Keys”页面。然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容。
为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。
当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
配置多个 ssh key
# gitee
Host gitee.com
HostName gitee.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/gitee_id_rsa
# github
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/github_id_rsa
这里最好可以加一个 user
USER zhuqt@wegene.com
复制代码
添加远程仓库
-
登录账号,然后 Create a new repo, 在Repository name填入xxx,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git远程仓库。
-
然后按照生成步骤可以进行链接远程仓库
//or create a new repository on the command line
//echo “# learngit” >> README.md
git init
git add README.md
git commit -m “first commit”
git branch -M main
git remote add origin github.com/DragonTotem…
git push -u origin main//…or push an existing repository from the command line
git remote add origin github.com/DragonTotem…
git branch -M main
git push -u origin main
把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。
由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。
git push origin master
复制代码
SSH警告
当你第一次使用Git的clone或者push命令连接GitHub时,会得到一个警告:
The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?
复制代码
这是因为Git使用SSH连接,而SSH连接在第一次验证GitHub服务器的Key时,需要你确认GitHub的Key的指纹信息是否真的来自GitHub的服务器,输入yes回车即可。
Git会输出一个警告,告诉你已经把GitHub的Key添加到本机的一个信任列表里了:
Warning: Permanently added 'github.com' (RSA) to the list of known hosts.
复制代码
这个警告只会出现一次,后面的操作就不会有任何警告了。
从远程仓库克隆
git clone git@github.com:michaelliao/gitskills.git
Cloning into 'gitskills'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 3
Receiving objects: 100% (3/3), done.
复制代码
###E 六、分支管理
每次提交修改Git都会修改版本连成一条时间线,这条时间线就是一个分支。主分支即master。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。
origin、HEAD、master区别
HEAD:当前提交你的repo。大多数时候都 HEAD 指向分支中的最新提交。
master:git在首次创建repo时为您创建的默认分支的名称。在大多数情况下,“主”意味着“主要分支”。
origin:git为远程仓库提供的默认名称。
一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点。
Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:
假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:
首先,我们创建dev分支,然后切换到dev分支:
$ git checkout -b dev
Switched to a new branch 'dev'
复制代码
git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:
$ git branch dev
$ git checkout dev
Switched to branch 'dev'
复制代码
然后,用git branch命令查看当前分支:
$ git branch
* dev
master
复制代码
git branch命令会列出所有分支,当前分支前面会标一个*号。
然后,我们就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行:
Creating a new branch is quick.
然后提交:
$ git add readme.txt
$ git commit -m "branch test"
[dev b17d20e] branch test
1 file changed, 1 insertion(+)
复制代码
现在,dev分支的工作完成,我们就可以切换回master分支:
$ git checkout master
Switched to branch 'master'
复制代码
切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变:
现在,我们把dev分支的工作成果合并到master分支上:
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
复制代码
Rebase(变基)
- rebase操作可以把本地未push的分叉提交历史整理成直线;
- rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。
git merge 操作合并分支会让两个分支的每一次提交都按照提交时间(并不是push时间)排序,并且会将两个分支的最新一次commit点进行合并成一个新的commit,最终的分支树呈现非整条线性直线的形式
git rebase操作实际上是将当前执行rebase分支的所有基于原分支提交点之后的commit打散成一个一个的patch,并重新生成一个新的commit hash值,再次基于原分支目前最新的commit点上进行提交,并不根据两个分支上实际的每次提交的时间点排序,rebase完成后,切到基分支进行合并另一个分支时也不会生成一个新的commit点,可以保持整个分支树的完美线性
另外值得一提的是,当我们开发一个功能时,可能会在本地有无数次commit,而你实际上在你的master分支上只想显示每一个功能测试完成后的一次完整提交记录就好了,其他的提交记录并不想将来全部保留在你的master分支上,那么rebase将会是一个好的选择,他可以在rebase时将本地多次的commit合并成一个commit,还可以修改commit的描述等
注意点:如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。
变基 vs. 合并
到底哪种方式更好。
有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱
改。 从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。 如果由合并产生的提
交历史是一团糟怎么办?
既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。
另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维
护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故
事,怎么方便后来的读者就怎么写。
Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。
总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行
变基操作,这样,你才能享受到两种方式带来的便利。
标签TAG
git tag v1.0
git tag v0.9 f52c633
git push origin v1.0
git push origin --tags
//查看
git tag
// 删除标签
git tag -d v0.9
git push origin :refs/tags/v0.9
复制代码
注意一点: 默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样——你可以运行 git push origin [tagname]。
七、搭建Git服务器
忽略文件.gitignore
配置别名
git config --global alias.co checkout
git config --global alias.ci commit
git config --global alias.br branch
复制代码
以后提交就可以简写成:
git ci -m "bala bala bala..."
复制代码
创建git步骤
-
安装git
sudo apt-get install git 复制代码
-
创建一个git用户,用来运行git服务:
sudo adduser git 复制代码
-
创建证书登录:
收集所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个。 复制代码
-
初始化Git仓库:
git init --bare sample.git 复制代码
Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。然后,把owner改为git:
sudo chown -R git:git sample.git 复制代码
-
禁用shell登录
出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/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每次一登录就自动退出。
什么是登录shell: 登录shell是可以用户登录使用的,比如/bin/bash ,/bin/sh ,/bin/csh……一般 Linux默认的用户shell都是bash,也就是你可以登录进去写命令。
shell 登录
非登录shell:经典的/bin/nologin就是一个非登录shell,也就是说如果一个用户默认的是它,这个用户即使登录进linux也无法使用linux。
shell是用户和计算机交流的媒介,登录shell保证用户和计算机交流,非登录shell无法让计算机和用户交流。
关于用户的默认登录shell是在/etc/passwd文件中记录的。
非登录shell有他特定的用途,比如一个用linux搭建的ftp服务器,并且创建了多个用户,那么就可以将这些用户默认shell改成nologin,这样一来,这些用户虽然是linux上的用户却无法登录进linux主机,只能进入ftp服务器,这样也保证了安全!
-
克隆远程仓库
git clone git@server:/srv/sample.git 复制代码
管理公钥
如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。
管理权限
有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。
第三方托管
如果不想设立自己的 Git 服务器,你可以选择将你的 Git 项目托管到一个外部专业的托管网站。 这带来了一些好
处:一个托管网站可以用来快速建立并开始项目,且无需进行服务器维护和监控工作。 即使你在内部设立并且
运行了
八、Git 内部原理
从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。
早期的 Git(主要是 1.5 之前的版本)的用户界面要比现在复杂的多,因为它更侧重于作为一个文件系统,而不
是一个打磨过的版本控制系统。 不时会有一些陈词滥调抱怨早期那个晦涩复杂的 Git 用户界面;不过最近几年
来,它已经被改进到不输于任何其他版本控制系统地清晰易用了。
8.1 底层命令与上层命令
由于 Git 最初是一套面向版本 控制系统的工具集,而不是一个完整的、用户友好的版本控制系统, 所以它还包含了一部分用于完成底层工作
的子命令。 这些命令被设计成能以 UNIX 命令行的风格连接在一起,抑或藉由脚本调用,来完成工作。 这部分
命令一般被称作 “底层(plumbing)”命令,而那些更友好的命令则被称作 “上层(porcelain)”命令。
因为,底层命令得以让你窥探 Git 内部的工作机制,也有助于说明 Git是如何完成工作的,以及它为何如此运作。多数底层命令并不面向最终用户:它们更适合作为新工具的组件和自定义脚本的组成部分。
当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储
和操作的东西。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。
git init 默认结构
$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/
复制代码
使用过一段时间的.git 目录
COMMIT_EDITMSG // 最后一次commit的描述语
FETCH_HEAD // FETCH_HEAD表示某个branch在服务器上的最新状态。
HEAD // 指向目前被检出的分支
ORIG_HEAD // 其实存放的也是commit。当进行一些有风险的操作的时候,如reset、merge或者rebase,Git会将HEAD原来所指向commit对象的sha-1值存放于ORIG_HEAD文件中。
config // 包含项目特有的配置选项。
description // 仅供 GitWeb 程序使用
hooks/ // 包含客户端或服务端的钩子脚本(hook scripts)
index // 保存暂存区信息
info/ // 包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore文件中的忽略模式(ignored patterns)
logs/ // 存放提交日志的
objects/ // 存储所有数据内容
packed-refs // 存储提交最新指针和TAGs
refs/ // 存储指向数据(分支、远程仓库和标签等)的提交对象的指针
sourcetreeconfig
每一个执行过fetch操作的项目都会存在一个FETCH_HEAD列表,其中每一行对应于远程服务器的一个分支。
复制代码
当前分支指向的FETCH_HEAD, 就是这个文件第一行对应的那个分支。存在两种情况:如果没有显式的指定远程分支, 则远程分支的master将作为默认的FETCH_HEAD;如果指定了远程分支, 就将这个远程分支作为FETCH_HEAD.
其中,最重要的几个文件分别是hooks/、HEAD、index、objects/、refs/
8.2 hooks(Git 钩子)
Git 能在特定的重要动作发生时触发自定义脚本。 有两组这样的钩子:客户端的和
服务器端的。 客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。
git init 初
始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本。这些脚本除了本身可以被调用外,它们还透
露了被触发时所传入的参数。 所有的示例都是 shell 脚本,其中一些还混杂了 Perl 代码,不过,任何正确命名的可执行脚本都可以正常使用 ——可以用 Ruby 或 Python,或任何你熟悉的语言编写它们。 这些示例的名字都是以 .sample 结尾,如果你想启用它们,得先移除这个后缀。
commit-msg.sample
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}
commit-msg 钩子接收一个参数,此参数即上文提到的,存有当前提交信息的临时文件的路径。 如果该钩子脚
本以非零值退出,Git 将放弃提交,因此,可以用来在提交通过前验证项目状态或提交信息。
复制代码
自定义的commit-msg
unset GREP_OPTIONS
CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test|Feature|Fixes|Fixed"
MSG="$1"
# Check for, and add if missing, a unique Change-Id
#
add_ChangeId() {
clean_message=`sed -e '
/^diff --git .*/{
s///
q
}
/^Signed-off-by:/d
/^#/d
' "$MSG" | git stripspace`
if test -z "$clean_message"
then
return
fi
# Do not add Change-Id to temp commits
if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!'
then
return
fi
if test "false" = "`git config --bool --get gerrit.createChangeId`"
then
return
fi
# Does Change-Id: already exist? if so, exit (no change).
if grep -i '^Change-Id:' "$MSG" >/dev/null
then
return
fi
id=`_gen_ChangeId`
T="$MSG.tmp.$$"
AWK=awk
if [ -x /usr/xpg4/bin/awk ]; then
# Solaris AWK is just too broken
AWK=/usr/xpg4/bin/awk
fi
# Get core.commentChar from git config or use default symbol
commentChar=`git config --get core.commentChar`
commentChar=${commentChar:-#}
# How this works:
# - parse the commit message as (textLine+ blankLine*)*
# - assume textLine+ to be a footer until proven otherwise
# - exception: the first block is not footer (as it is the title)
# - read textLine+ into a variable
# - then count blankLines
# - once the next textLine appears, print textLine+ blankLine* as these
# aren't footer
# - in END, the last textLine+ block is available for footer parsing
$AWK '
BEGIN {
if (match(ENVIRON["OS"], "Windows")) {
RS="\r?\n" # Required on recent Cygwin
}
# while we start with the assumption that textLine+
# is a footer, the first block is not.
isFooter = 0
footerComment = 0
blankLines = 0
}
# Skip lines starting with commentChar without any spaces before it.
/^'"$commentChar"'/ { next }
# Skip the line starting with the diff command and everything after it,
# up to the end of the file, assuming it is only patch data.
# If more than one line before the diff was empty, strip all but one.
/^diff --git / {
blankLines = 0
while (getline) { }
next
}
# Count blank lines outside footer comments
/^$/ && (footerComment == 0) {
blankLines++
next
}
# Catch footer comment
/^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) {
footerComment = 1
}
/]$/ && (footerComment == 1) {
footerComment = 2
}
# We have a non-blank line after blank lines. Handle this.
(blankLines > 0) {
print lines
for (i = 0; i < blankLines; i++) {
print ""
}
lines = ""
blankLines = 0
isFooter = 1
footerComment = 0
}
# Detect that the current block is not the footer
(footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) {
isFooter = 0
}
{
# We need this information about the current last comment line
if (footerComment == 2) {
footerComment = 0
}
if (lines != "") {
lines = lines "\n";
}
lines = lines $0
}
# Footer handling:
# If the last block is considered a footer, splice in the Change-Id at the
# right place.
# Look for the right place to inject Change-Id by considering
# CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first,
# then Change-Id, then everything else (eg. Signed-off-by:).
#
# Otherwise just print the last block, a new line and the Change-Id as a
# block of its own.
END {
unprinted = 1
if (isFooter == 0) {
print lines "\n"
lines = ""
}
changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):"
numlines = split(lines, footer, "\n")
for (line = 1; line <= numlines; line++) {
if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) {
unprinted = 0
print "Change-Id: I'"$id"'"
}
print footer[line]
}
if (unprinted) {
print "Change-Id: I'"$id"'"
}
}' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
}
_gen_ChangeIdInput() {
echo "tree `git write-tree`"
if parent=`git rev-parse "HEAD^0" 2>/dev/null`
then
echo "parent $parent"
fi
echo "author `git var GIT_AUTHOR_IDENT`"
echo "committer `git var GIT_COMMITTER_IDENT`"
echo
printf '%s' "$clean_message"
}
_gen_ChangeId() {
_gen_ChangeIdInput |
git hash-object -t commit --stdin
}
add_ChangeId
复制代码
这个conit-msg中主要是有一个add_ChangeId函数,用于在commit时,对这次提交生成一个changeId。Gerrit使用中会用到这个changeId,当Abandoned一次提交后,这个changeId就不能用了,本地需要reset,然后重新提交生成新的changeId。
关于change-id可以看这篇文章 git commit 中的change-id
8.3 Git 对象
Git 是一个内容寻址文件系统, 这意味着,Git 的核心部分是一个简单的键值
对数据库(key-value data store)。 你可以向Git仓库中插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。
可以通过底层命令 git hash-object 来演示上述效果——该命令可将任意数据保存于 .git/objects 目录
(即 对象数据库),并返回指向该数据对象的唯一的键。
可以通过底层命令 git hash-object 来演示上述效果——该命令可将任意数据保存于 .git/objects 目录
(即 对象数据库),并返回指向该数据对象的唯一的键。
首先,我们需要初始化一个新的 Git 版本库,并确认 objects 目录为空:
git init test
Initialized empty Git repository in /tmp/test/.git/
cd test
// 如果是在window系统下,则可以在 Git Bash Here窗口下执行该命令
find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
find .git/objects -type f
复制代码
可以看到 Git 对 objects 目录进行了初始化,并创建了 pack 和 info 子目录,但均为空。 接着,我们用 git
hash-object 创建一个新的数据对象并将它手动存入你的新 Git 数据库中:
echo 'projectName content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
复制代码
在这种最简单的形式中,git hash-object 会接受你传给它的东西,而它只会返回可以存储在 Git 仓库中的唯一键。 -w 选项会指示该命令不要只返回键,还要将该对象写入数据库中。 最后,–stdin 选项则指示该命令
从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径
git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
复制代码
每一个版本所对应一个SHA-1值;另一个问题是,在这个(简单的版本控制)系统中,仅保存了文件的内容。 上述类型的对象我们称之为 数据对象(blob object) 。
8.3.1 树对象
Git对象类型是树对象(tree object)它能解决文件名保存的问题,也允许我们将多个文件组织到一起。 Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
复制代码
对象树是在暂存区中对应执行了git add 的文件。这些数据可以是多个对象树也可以是一个对象树,可以自己添加对象树,并给对象树添加相应的文件。
底层命令
// 可以通过底层命令 git update-index 为一个单独文件—— test.txt 文件的首个版本——创建一个暂存区。 利用该命令,可以把 test.txt 文件的首个版本人为地加入一个新的暂存区。 必须为上述命令指定 --add 选项,因为此前该文件并不在暂存区中; 同样必需的还有 --cacheinfo 选项,因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下。 同时,需要指定文件模式、SHA-1 与文件名
git update-index --add --cacheinfo 100644 \ 83baae61804e65cc73a7201a7252750c76066a30 test.txt
// 从底层命令可以看出很多博客将暂存区命名为index的原因
// 将暂存区内容写入一个树对象
git write-tree
复制代码
8.3.2 提交对象
上面对象树分别代表想要跟踪的项目的快照,谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。 提交对象(commit object) 保存的基本信息。
可以通过调用 commit-tree 命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父
提交对象(如果有的话)。
echo 'first commit' | git commit-tree d8329f
复制代码
现在可以通过 git cat-file 命令查看这个新提交对象:
$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
first commit
复制代码
提交对象的格式很简单:它先指定一个顶层树对象,代表当前项目快照; 然后是可能存在的父提交(前面描述
的提交对象并不存在任何父提交); 之后是作者/提交者信息(依据你的 user.name 和 user.email 配置来设定,外加一个时间戳); 留空一行,最后是提交注释。
接着,我们将创建另两个提交对象,它们分别引用各自的上一个提交(作为其父提交对象):
$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit' | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9
复制代码
如果跟踪所有的内部指针,将得到一个类似下面的对象关系图:
8.3.3 对象存储
向 Git 仓库提交的所有对象都会有个头部信息一并被保存。
Git 首先会以识别出的对象的类型作为开头来构造一个头部信息,接着 Git 会在头部的第一部分添加一个空格,随后是数据内容的字节数,最后是一个空字节,Git会将上述头部信息和原始数据拼接起来,并计算出这条新内容的 SHA-1 校验和。
Git 会通过 zlib 压缩这条新内容。
最后,需要将这条经由 zlib压缩的内容写入磁盘上的某个对象。数据对象。
所有的 Git 对象均以这种方式存储,区别仅在于类型标识——对象类型的头部信息以字符串“commit”或“tree”开头。
8.4 引用
如果我们有一个文件来保存SHA-1值,而该文件有一个简单的名字,然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。在 Git中,这种简单的名字被称为“引用(references,或简写为 refs)”。
底层命令:更新某个引用
$ git update-ref refs/heads/master
1a410efbd13591db07496601ebc7a059dd55cfe9
复制代码
这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 若想在第二个提交上创建一个分支,可
以这么做:
git update-ref refs/heads/test cac0ca
复制代码
当运行类似于 git branch 这样的命令时,Git 实际上会运行 update-ref 命令, 取得当前所在分
支最新提交对应的 SHA-1 值,并将其加入你想要创建的任何新引用中。
$ find refs
refs
refs/.DS_Store
refs/heads
refs/heads/develop
refs/heads/master
refs/tags
refs/tags/1.8.3
refs/tags/1.8.4
refs/tags/1.8.2
refs/tags/1.8.0
refs/tags/1.8.1
refs/remotes
refs/remotes/.DS_Store
refs/remotes/origin
refs/remotes/origin/develop
refs/remotes/origin/HEAD
refs/remotes/origin/master
refs/stash
复制代码
8.4.1 HEAD引用
当你执行 git branch 时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。
HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。
所谓符号引用,表示它是一个指向其他引用的指针。
当我们执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置
其父提交字段。
8.4.2 标签引用 (tag reference)
前面我们刚讨论过 Git 的三种主要的对象类型(数据对象、树对象 和 提交对象 ),然而实际上还有第四种。 标签对象(tag object)非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
8.4.3 远程引用(remote reference)
如果你添加了一个远程版本库并对其执行过推送操作,Git会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。
远程引用和分支(位于 refs/heads目录下的引用)之间最主要的区别在于,远程引用是只读的。 虽然可以git checkout 到某个远程引用,但是 Git 并不会将 HEAD引用指向该远程引用。因此,你永远不能通过commit 命令来更新远程引用。 Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。
8.5 包文件
objects 中存储了数据对象、树对象、提交对象以及标签对象。其中有些对象的内容基本相似,造成空间占用。Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc命令,或者你向远程服务器执行推送时,Git 都会这样做。
手动打包:
zbt:.git zbt$ find objects -type f
objects/d9/91018fc9cc614671bebb0e77310941a222c607
objects/pack/pack-8c76af529753f98845955e2a1d792d6f8dc2560a.idx
objects/pack/pack-8c76af529753f98845955e2a1d792d6f8dc2560a.pack
objects/81/5f3ab2a2b996a772f2dd67c078bd535f7df344
objects/info/packs
zbt:.git zbt$
复制代码
手动打包后,你会发现大部分的对象都不见了,与此同时出现了一对新文件。剩下的文件是新创建的包文件和一个索引。包文件包含了刚才从文件系统中移除的所有对象的内容。 索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。
gc之后 objects文件大小减少了不少M
8.6 引用规范
链接本地仓库和远程仓库
git remote add origin git@code.aliyun.com:wegene-mobile-dev/wegene_android.git
复制代码
运行上述命令会在你仓库中的 .git/config 文件中添加一个小节,并在其中指定远程版本库的名称(origin)、URL 和一个用于获取操作的 引用规范(refspec):
[remote "origin"]
url = git@code.aliyun.com:wegene-mobile-dev/wegene_android.git
fetch = +refs/heads/*:refs/remotes/origi
复制代码
引用规范的格式由一个可选的 + 号和紧随其后的 : 组成, 其中 是一个模式(pattern),代表远程版本库中的引用; 是本地跟踪的远程引用的位置。 + 号告诉 Git 即使在不能快进的情况下也要
(强制)更新引用。
默认情况下,引用规范由 git remote add origin 命令自动生成, Git 获取服务器中 refs/heads/ 下面的所有引用,并将它写入到本地的 refs/remotes/origin/ 中。所以,如果服务器上有一个 master分支,你可以在本地通过下面任意一种方式来访问该分支上的提交记录。
8.7 传输协议
Git 可以通过两种主要的方式在版本库之间传输数据:“哑(dumb)”协议和“智能(smart)”协议。
8.7.1 哑协议
如果你正在架设一个基于 HTTP协议的只读版本库,一般而言这种情况下使用的就是哑协议。 这个协议之所以
被称为“哑”协议,是因为在传输过程中,服务端不需要有针对 Git特有的代码;抓取过程是一系列 HTTP 的GET 请求,这种情况下,客户端可以推断出服务端 Git 仓库的布局。
8.7.2 智能协议
哑协议虽然很简单但效率略低,且它不能从客户端向服务端发送数据。智能协议是更常用的传送数据的方法,但它需要在服务端运行一个进程,而这也是 Git的智能之处——它可以读取本地数据,理解客户端有什么和需要什么,并为它生成合适的包文件。 总共有两组进程用于传输数据,它们分别负责上传和下载数据。
上传数据
为了上传数据至远端,Git 使用 send-pack 和 receive-pack 进程。运行在客户端上的 send-pack 进程连接到远端运行的 receive-pack 进程。
git push origin master
复制代码
由于origin是基于SSH协议的URL所定义的。Git会运行send-pick进程,该进程尝试连接服务器,并且通过SSH在服务器端执行行命令,如下:
$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000
复制代码
git-receive-pack 命令会立即为它所拥有的每一个引用发送一行响应——在这个例子中,就只有 master 分支和它的 SHA-1 值。
第一行响应中也包含了一个服务端能力的列表(这里是 report-status、deleterefs
和一些其它的,包括客户端的识别码)。
每一行以一个四位的十六进制值开始,用于指明本行的长度。 你看到第一行以 00a5 开始,这在十六进制中表示165,意味着第一行有 165 字节。 下一行是0000,表示服务端已完成了发送引用列表过程。
现在它知道了服务端的状态,你的 send-pack 进程会判断哪些提交记录是它所拥有但服务端没有的。 sendpack 会告知 receive-pack 这次推送将会更新的各个引用(上节中的引用)。 举个例子,如果你正在更新 master 分支,并且增加 experiment 分支,这个 send-pack 的响应将会是像这样:
0076ca82a6dff817ec66f44342007202690a93763949
15027957951b64cf874c3557a0f3547bd83b3ff6 \
refs/heads/master report-status
006c0000000000000000000000000000000000000000
cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
refs/heads/experiment
0000
复制代码
第一行也包括了客户端的能力。 这里的全为 0 的 SHA-1值表示之前没有过这个引用——因为你正要添加新的experiment 引用。 删除引用时,将会看到相反的情况:右边的 SHA-1 值全为 0。
连接是从下面这个请求开始的:
=> GET http://server/simplegit-progit.git/info/refs?service=git-receivepack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master reportstatus
\
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000
复制代码
这完成了客户端和服务端的第一次数据交换。接下来客户端发起另一个请求,这次是一个 POST 请求,这个请求
中包含了 send-pack 提供的数据。
=> POST http://server/simplegit-progit.git/git-receive-pack
复制代码
这个 POST 请求的内容是 send-pack 的输出和相应的包文件。服务端在收到请求后相应地作出成功或失败的HTTP 响应。
下载数据
当你在下载数据时, fetch-pack 和 upload-pack 进程就起作用了。 客户端启动 fetch-pack 进程,连接至远端的 upload-pack 进程,以协商后续传输的数据。
如果你通过 SSH 使用抓取功能,fetch-pack 会像这样运行:
$ ssh -x git@server “git-upload-pack ‘simplegit-progit.git'”
在 fetch-pack 连接后,upload-pack 会返回类似下面的内容:
00dfca82a6dff817ec66f44342007202690a93763949 HEAD multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000
复制代码
这与 receive-pack 的响应很相似,但是这里所包含的能力是不同的。 而且它还包含 HEAD 引用所指向内容(symref=HEAD:refs/heads/master),这样如果客户端执行的是克隆,它就会知道要检出什么。
这时候,fetch-pack 进程查看它自己所拥有的对象,并响应 “want” 和它需要的对象的 SHA-1 值。 它还会发送“have”和所有它已拥有的对象的 SHA-1 值。 在列表的最后,它还会发送“done”以通知upload-pack进程可以开始发送它所需对象的包文件。
抓取操作的握手需要两个 HTTP 请求。 第一个是向和哑协议中相同的端点发送 GET 请求。
这和通过 SSH 使用 git-upload-pack是非常相似的,但是第二个数据交换则是一个单独的请求。
8.8 维护与数据恢复
清理数据、更新数据以及恢复数据
8.8.1 维护
Git 会不定时地自动运行一个叫做 “auto gc” 的命令。大多数时候,这个命令并不会产生效果。 然而,如果有太多松散对象(不在包文件中的对象)或者太多包文件,Git 会运行一个完整的 git gc 命令。 “gc”代表垃圾回收,这个命令会做以下事情:收集所有松散对象并将它们放置到包文件中, 将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象。
可以像下面一样手动执行自动垃圾回收:
$ git gc --auto
复制代码
就像上面提到的,这个命令通常并不会产生效果。 大约需要 7000个以上的松散对象或超过 50 个的包文件才能让 Git 启动一次真正的 gc 命令。 你可以通过修改 gc.auto 与 gc.autopacklimit 的设置来改动这些数值。
8.8.2 数据恢复
比如在在某次提交后,我们使用了 git reset 该分支就重置到某一个提交了,那么中间有一些提交就消失。这种情况下如何恢复?
这时可以是用git reflog工具。
当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。每一次你提交或改变分支,引用日志都会被更新。引用日志(reflog)也可以通过git update-ref 命令更新。
9d9451c (HEAD -> dev) HEAD@{0}: reset: moving to 9d9451
bc48bd6 HEAD@{1}: reset: moving to HEAD
bc48bd6 HEAD@{2}: reset: moving to HEAD
bc48bd6 HEAD@{3}: commit: reset test 2
9d9451c (HEAD -> dev) HEAD@{4}: reset: moving to 9d9451c4b0243c7423d44449d7150c9444f17967
842faa0 HEAD@{5}: reset: moving to 842faa0ee8c366b53164241defcd967ac9d7d618
842faa0 HEAD@{6}: reset: moving to 842faa0ee8c366b53164241defcd967ac9d7d618
842faa0 HEAD@{7}: reset: moving to HEAD
复制代码
这样就可以根据sha1值来更新
8.8.3 移除对象
比如提交大文件,需要删除,那么它会从你必须修改或移除一个大文件引用最早的树对象开始重写每一次提交。如果你在导入仓库后,任何人开始基于这些提交工作前执行这个操作,那么将不会有任何问题——否则,你必须通知所有的贡献者他们需要将他们的成果变基到你的新提交上。
8.9 环境变量
Git 总是在一个 bash shell 中运行,并借助一些 shell环境变量来决定它的运行方式。 有时候,知道它们是什么以及它们如何让 Git 按照你想要的方式去运行会很有用。 这里不会列出所有的 Git环境变量,但我们会涉及最有用的那部分。
8.9.1 全局行为
就是配置环境变量,像通常的程序一样,Git 的常规行为依赖于环境变量。
GIT_EXEC_PATH 决定 Git 到哪找它的子程序 (像 git-commit, git-diff 等等)。你可以用 git –exec-path 来查看当前设置。‘
通常不会考虑修改 HOME 这个变量(太多其它东西都依赖它),这是 Git查找全局配置文件的地方。 如果你想要一个包括全局配置的真正的便携版 Git, 你可以在便携版 Git 的 shell 配置中覆盖 HOME 设置。
PREFIX 也类似,除了用于系统级别的配置。 Git 在 $PREFIX/etc/gitconfig 查找此文件。
如果设置了 GIT_CONFIG_NOSYSTEM,就禁用系统级别的配置文件。这在系统配置影响了你的命令,而你又无权限修改的时候很有用。
GIT_PAGER 控制在命令行上显示多页输出的程序。 如果这个没有设置,就会用 PAGER 。
GIT_EDITOR 当用户需要编辑一些文本(比如提交信息)时, Git 会启动这个编辑器。 如果没设置,就会用EDITOR 。
8.9.2 版本库位置
Git 用了几个变量来确定它如何与当前版本库交互。
GIT_DIR 是 .git 目录的位置。 如果这个没有设置, Git 会按照目录树逐层向上查找 .git 目录,直到到达 ~或 /。
GIT_CEILING_DIRECTORIES 控制查找 .git 目录的行为。如果你访问加载很慢的目录(如那些磁带机上的或通过网络连接访问的),你可能会想让 Git 早点停止尝试,尤其是 shell 构建时调用了 Git 。
GIT_WORK_TREE 是非空版本库的工作目录的根路径。 如果指定了 –git-dir 或 GIT_DIR 但未指定 –work-tree、GIT_WORK_TREE 或core.worktree,那么当前工作目录就会视作工作树的顶级目录。
GIT_INDEX_FILE 是索引文件的路径(只有非空版本库有)。
GIT_OBJECT_DIRECTORY 用来指定 .git/objects 目录的位置。
GIT_ALTERNATE_OBJECT_DIRECTORIES 一个冒号分割的列表(格式类似/dir/one:/dir/two:…)用来告
诉 Git 到哪里去找不在 GIT_OBJECT_DIRECTORY 目录中的对象。
如果你有很多项目有相同内容的大文件,这个可以用来避免存储过多备份。
8.9.3 路径规则
所谓 “pathspec” 是指你在 Git 中如何指定路径,包括通配符的使用。 它们会在 .gitignore 文件中用到,命令行里也会用到(git add *.c)。
8.9.4 提交
Git 提交对象的创建通常最后是由 git-commit-tree 来完成, git-commit-tree 用这些环境变量作主要的
信息源。 仅当这些值不存在才回退到预置的值。
8.9.5 网络
Git 使用 curl 库通过 HTTP 来完成网络操作, 所以 GIT_CURL_VERBOSE 告诉 Git 显示所有由那个库产生的消息。 这跟在命令行执行 curl -v 差不多。
8.9.6 比较和合并
8.9.7 调试
GIT_DIFF_OPTS 这个有点起错名字了。 有效值仅支持 -u 或–unified=,用来控制在 git diff 命令中显示的内容行数。
GIT_EXTERNAL_DIFF 用来覆盖 diff.external 配置的值。 如果设置了这个值, 当执行 git diff 时,Git会调用该程序。
GIT_DIFF_PATH_COUNTER 和 GIT_DIFF_PATH_TOTAL 对于 GIT_EXTERNAL_DIFF 或 diff.external 指定的程序有用。 前者表示在一系列文件中哪个是被比较的(从 1 开始),后者表示每批文件的总数。
GIT_MERGE_VERBOSITY 控制递归合并策略的输出。 允许的值有下面这些:
0. 什么都不输出,除了可能会有一个错误信息。
1. 只显示冲突。
2. 还显示文件改变。
3. 显示因为没有改变被跳过的文件。
4. 显示处理的所有路径。
5. 显示详细的调试信息。
复制代码
默认值是 2。
调试
8.9.8 其他
如果指定了 GIT_SSH, Git 连接 SSH 主机时会用指定的程序代替 ssh 。 它会被用 $GIT_SSH[username@]host [-p ] 的命令方式调用。 这不是配置定制 ssh 调用方式的最简单的方法; 它不支持额外的命令行参数, 所以你必须写一个封装脚本然后让 GIT_SSH 指向它。 可能用~/.ssh/config 会更简单。
9 git命令
9.1 安装配置命令
#sudo apt-get install git
#sudo apt-get install git-doc git-svn git-email git-gui gitk
初始化git
ssh-keygen #生成git key 默认在.ssh目录下
git init # 在当前目录新建一个Git代码库
git init [project-name] # 新建一个目录,将其初始化为Git代码库
git clone [url]# 下载一个项目和它的整个代码历史
git config --list# 显示当前的Git配置
git config -e [--global]# 编辑Git配置文件
# 设置提交代码时的用户信息
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
复制代码
9.2 代码提交相关命令
git clone https://github.com/username/repo name.git # 从远程仓库下载代码
git status . # 查看当前工作区的修改
git add * # 跟踪新文件
git add -A 提交所有变化
git add -u 提交被修改(modified)和被删除(deleted)文件,不包括新文件(new)
git add . 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件
rm *&git rm * # 移除文件
git rm -f * # 移除文件
git rm --cached * # 停止追踪指定文件,但该文件会保留在工作区
git mv file_from file_to # 重命名跟踪文件
git log # 查看提交记录
git commit # 提交更新
git commit [file1] [file2] ... # 提交指定文件
git commit -m 'message'
git commit -a # 跳过使用暂存区域,把所有已经跟踪过的文件暂存起来一并提交
git commit --amend#修改最后一次提交
git commit -v # 提交时显示所有diff信息
git reset HEAD *#取消已经暂存的文件
git reset --mixed HEAD *#同上
git reset --soft HEAD *#重置到指定状态,不会修改索引区和工作树
git reset --hard HEAD *#重置到指定状态,会修改索引区和工作树
git reset -- files#重置index区文件
git revert HEAD #撤销前一次操作
git revert HEAD~ #撤销前前一次操作
git revert commit ## 撤销指定操作
git checkout -- file#取消对文件的修改(从暂存区——覆盖worktree file)
git checkout branch|tag|commit -- file_name#从仓库取出file覆盖当前分支
git checkout -- .#从暂存区取出文件覆盖工作区
git diff file #查看指定文件的差异
git diff --stat #查看简单的diff结果
git diff #比较Worktree和Index之间的差异
git diff --cached #比较Index和HEAD之间的差异
git diff HEAD #比较Worktree和HEAD之间的差异
git diff branch #比较Worktree和branch之间的差异
git diff branch1 branch2 #比较两次分支之间的差异
git diff commit commit #比较两次提交之间的差异
git log --grep=aplog #查找关键字aplog
git log #查看最近的提交日志
git log --pretty=oneline #单行显示提交日志
git log --graph # 图形化显示
git log --abbrev-commit # 显示log id的缩写
git log -num #显示第几条log(倒数)
git log --stat # 显示commit历史,以及每次commit发生变更的文件
git log --follow [file] # 显示某个文件的版本历史,包括文件改名
git log -p [file] # 显示指定文件相关的每一次diff
git log --oneline #只用一行显示每次的提交包含哈希号和commit内容
git stash #将工作区现场(已跟踪文件)储藏起来,等以后恢复后继续工作。
git stash list #查看保存的工作现场
git stash apply #恢复工作现场
git stash drop #删除stash内容
git stash pop #恢复的同时直接删除stash内容 最好不要这么干,除非你下次确认不会再使用缓存数据
git stash apply stash@{0} #恢复指定的工作现场,当你保存了不只一份工作现场时。
复制代码
9.3 分支操作命令
注意:其中 #XXX 是对命令的解释,使用命令时,不要携带
git branch #列出本地分支
git branch -r #列出远端分支 为了方便查看,前面会有一个*号显示
git branch -a #列出所有分支
git branch -v #查看各个分支最后一个提交对象的信息
git branch --merge #查看已经合并到当前分支的分支
git branch --no-merge #查看为合并到当前分支的分支
git branch test #新建test分支
git branch branch [branch|commit|tag] # 从指定位置出新建分支
git branch --track branch remote-branch # 新建一个分支,与指定的远程分支建立追踪关系
git branch -m old new #重命名分支
git branch -d test #删除test分支
git branch -D test #强制删除test分支 可以多分支同时删除
git branch --set-upstream dev origin/dev #将本地dev分支与远程dev分支之间建立链接
git checkout test #切换到test分支
git checkout -b test #新建+切换到test分支
git checkout -b test dev #基于dev新建test分支,并切换
git merge test #将test分支合并到当前分支
git merge --squash test ## 合并压缩,将test上的commit压缩为一条
git cherry-pick commit #拣选合并,将commit合并到当前分支,比如我们之前在dev分支下做了一个修改,然后切换到master下面来,同步了最新的代码,可以通过cherry-pick把之前的提交同步过来
git cherry-pick -n commit #拣选多个提交,合并完后可以继续拣选下一个提交
git rebase master #将master分之上超前的提交,变基到当前分支
git rebase --onto master 169a6 #限制回滚范围,rebase当前分支从169a6以后的提交
git rebase --interactive #交互模式
git rebase --continue #处理完冲突继续合并
git rebase --skip #跳过
git rebase --abort #取消合并
复制代码
9.4 远程分支相关命令
git fetch origin remotebranch[:localbranch] # 从远端拉去分支[到本地指定分支]
git merge origin/branch #合并远端上指定分支
git pull origin remotebranch:localbranch # 拉去远端分支到本地分支
git push origin branch #将当前分支,推送到远端上指定分支
git push origin localbranch:remotebranch #推送本地指定分支,到远端上指定分支
git push origin :remotebranch #删除远端指定分支
git push origin remotebranch --delete #删除远程分支
git branch -dr branch #删除本地和远程分支
git checkout -b [--track] test origin/dev #基于远端dev分支,新建本地test分支[同时设置跟踪]
复制代码
9.5 其它命令
// 远程库
git remote add origin1 git@github.com:weiqifa/demo.git #添加源
git remote #显示全部源
git remote -v #显示全部源+详细信息
git remote rename origin1 origin2 #重命名
git remote rm origin #删除
git remote show origin #查看指定源的全部信息
// 尽量避免使用的命令
git push -f origin master
// 同步其它分支的文革文件
// 把yan3_dev 分支下的resource_tool 同步到当前分支下
git show yan3_dev:scripts/resource_tool>scripts/resource_tool
// 打包项目源命令相关
git clone –bare kernel/ #这样生成kernel.git 这里会包含.git文件夹
git clone kernel.git/ #这样来解包
// 打补丁命令
git diff ./ > *.patch
patch -p1
// 查看历史分支
git log –graph –all
// 利用提示或者help
git help commit
复制代码