本文内容是学习《Pro Git》 https://git-scm.com/book/en/v2 一书后的心得 ,融合了本人自己的理解和实际遇到的问题,最后整理而成。
注:本文会不定期更新,请关注 云栖社区@一唐。
安装
- Linux:
$ sudo yum install git
或$ sudo apt-get install git
- Mac: 如果Xcode带有Git就直接使用,没有请到这里下载 http://git-scm.com/download/mac
- Windows: 请到这里下载 http://git-scm.com/download/win
特点
- 直接记录文件快照,而非记录文件的差异信息(仓库中包含每个文件的独立拷贝,修改后的文件会生成新拷贝);
- 若版本A和版本B中有6个文件(共同的文件)没有做过任何修改,那么在仓库中这6个文件只会存在一份,而不是两份;
- 利用SHA-1校验机制来保证文件数据完整性;
- 用户克隆一个Git库到本地时,会把该库的所有信息都拷贝下来,包括这个库的所有历史提交记录;
- Git库一般只添加数据,意味着只要是曾经提交到仓库的内容,都可以被恢复出来。
三个区域
- 工作区:就是用户可编辑文件或目录的区域;
- Git仓库:就是包含了项目各个版本数据的地方,我们不管需要哪个版本都可从这个区域取数据;
- 暂存区:就是把用户修改、新建、删除文件的记录保存起来,作为提交操作的参考信息。
三种状态
- 已修改:如果工作区的某文件发生了修改,此文件就属于已修改状态;
- 已暂存:如果某文件作了修改并已添加(git add filename)到暂存区,此文件就属于已暂存状态;
- 已提交:如果对某文件进行了修改,并且在添加到暂存区后不久就提交到了Git仓库,此文件就属于已提交状态。
已跟踪或未跟踪
- 已跟踪:曾经被提交过本地Git库的文件,也就是被纳入了版本控制的文件;
- 未跟踪:没有被纳入版本控制的文件,就是“已跟踪”状态之外的文件;
- 工作目录下的文件,不是“已跟踪”就是“未跟踪”。
简单描述Git库保存的内容
- 假设新建了一个本地库,里面新建了3个文件,并且已用命令把它们提交到了本地库;
- 在上面提交后,本地库会有五个对象:三个 blob 对象(保存着文件快照,里面有文件内容)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)
Git使用
1、在命令窗口输入 $ git help
即可查看git的使用帮助,若不能使用此命令,说明git没有安装好;
2、查看某个git命令的帮助,例如查看config命令的帮助 $ git help config
Git的配置
- 查看当前配置的命令
$ git config --list
- 设置用户信息(Git的每次提交操作都会使用这些信息,以便让别人知道是谁提交的)
$ git config --global user.name "YourName"
$ git config --global user.email YourName@smallez.com
- 配置分为三级:系统配置、当前用户配置、当前Git库配置
- “当前Git库配置”会覆盖“当前用户配置”,“当前用户配置”会覆盖“系统配置”
- 修改系统配置方法
$ git config --system user.name "YourName"
- 修改当前用户配置方法
$ git config --global user.name "YourName"
- 修改当前Git库配置方法(先进入到该Git库的目录)
$ git config user.name "YourName"
- 查看当前的某项配置是什么
$ git config user.name
创建本地Git仓库
- 新建一个目录作为本地仓库的落脚点;
- 进入新建的目录,用仓库初始化命令把它变成Git仓库
$ git init
- 如果不是一个新建的目录,里面已经有了文件,也照样先初始化;
- 不管是此目录后来新建的文件,还是本来就有的文件,都需要先把它们加入暂存区,然后再提交它们,才算是把这些文件放入了Git仓库。
本地Git库的常用操作命令
- 把某文件添加到暂存区
$ git add filename
- 把某目录下的所有文件(包含递归子目录)添加到暂存区
$ git add dirname
- 把扩展名为.c的文件都添加到暂存区
$ git add *.c
- 把记录在暂存区的文件全部提交到本地Git仓库
$ git commit -m 'your comment'
(提交后会出现报告信息,比如在哪个分支提交的,本次提交的SHA-1校验码是什么(如463dc4f),在本次提交中有多少文件修改过,多少行添加和删改过等) - 从暂存区移出文件movefile.txt,让它先不暂存
$ git reset HEAD movefile.txt
- 从Git库中恢复一个文件并覆盖到工作区
$ git checkout -- filename.txt
-
查看当前Git库的状态
$ git status
可以知道:- a、当前所在分支(On branch xxxx)
- b、哪些文件处于暂存区(Changes to be committed)
- c、哪些属于“未跟踪”状态的新文件(Untracked files)
- d、哪些属于被修改但未处于暂存区的已跟踪文件(Changes not staged for commit)
-
简要查看当前Git库的状态
$ git status -s
前面标识含义:- ?? 未跟踪文件
- A 新添加到暂存区中的文件
- M 文件被修改了并放入了暂存区
- M (注意M的左边有个空格)文件被修改了但是还没放入暂存区(需要再 git add 一下)
- MM 在工作区被修改并添加到暂存区后又在工作区中被修改了(需要再 git add 一下)
- AM 新添加到暂存区中的文件,还未提交的情况下又被修改了(需要再 git add 一下)
- 查看工作区文件和暂存区文件之间的差异
$ git diff
- 查看已经暂存起来的文件变化状况
$ git diff --cached
- 调用文本编辑器并提交更新
$ git commit
(按回车后会出现文本编辑窗口,让你输入相关的修改记录信息,比如描述自己新增加了哪些内容、哪些功能等) - 自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过加入暂存区的操作
$ git commit -a -m 'new comment'
- 删除文件
$ git rm filename.txt
(如果只是 rm filename.txt ,并不能起到删除暂存区文件的作用) - 文件被修改了并已添加到暂存区的删除
$ git rm -f filename.txt
(此时需要加-f强制删除) - 文件被误放入暂存区,需要移出来
$ git rm --cached filename.txt
(可以使用通配符,如 *.log 表示.log结尾的文件, dir/*.a 表示dir目录下.a结尾的文件,请务必注意*号前面要加 ) - 文件改名
$ git mv oldname newname
相当于执行了三条命令:$ mv oldname newname
$ git rm oldname
$ git add newname
- 补充提交操作,比如提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了,可参考下面的步骤:
$ git commit -m 'comment'
#此时发现忘了提交forgotten.txt文件$ git add forgotten.txt
#加上forgotten.txt$ git commit --amend
#用--amend参数补交,按下回车后会让你编辑前面的comment信息 - 给某个提交历史打标签
$ git tag -a v1.1 085bb3 -m 'my version 1.1'
(085bb3是该历史版本的SHA-1校验码前6位) - 查看本地库有哪些标签
$ git tag
(标签只是一个历史提交的别名而已) - 查看指定模式匹配的标签
$ git tag -l 'v1.8*'
- 给当前版本打标签
$ git tag -a v1.2 -m 'smallez_version 1.2'
- 查看指定标签版本对应的提交信息
$ git show v1.1
- 检查要提交的文件中是否包含多余的空白字符
$ git diff --check
- 把format-patch补丁文件应用到当前分支
$ git am patch-smallez-client.patch
- 把老式diff补丁文件应用到当前分支
$ git apply patch-smallez-client.patch
- 输出master分支最后一次提交的可读名称
$ git describe master
(如果最后一次提交设置了标签,就显示标签名,否则由最近的标签名、自该标签之后的提交数目和它部分SHA-1值构成) - 为master分支打包创建一个归档
$ git archive master --prefix='projname/' | gzip > `projname_v1.x`.tar.gz
- 接上面,如果要生成zip格式
$ git archive master --prefix='projname/' --format=zip > `projname_v1.x`.zip
- 生成自master分支v1.1(标签名)以来的changelog
$ git shortlog --no-merges master --not v1.1
(如果已经合并到master,此命令就没意义了) - 查看某个提交的父提交(就是上一个提交)信息
$ git show d921970^
(使用^符号) - 从工作目录查找一个包含某关键字的文件
$ git grep -n keyword
- 将某个版本恢复到工作区
$ git checkout d92197
(d92197是要恢复的提交历史的SHA-1前6位) ,如果要再恢复到最新的提交$ git checkout branch_name
(branch_name是分支名称)
涉及远程库的常用操作命令
- 把远程库的项目克隆到本地
$ git clone https://github.com/libgit2/libgit2
(此时远程库的默认名为origin) - 把远程库的项目克隆到本地并改名为smallez
$ git clone https://github.com/libgit2/libgit2 smallez
- 查看本地库所配置的远程库信息
$ git remote
(显示的是远程库的简写名) - 查看远程库简写名对应的URL
$ git remote -v
(注意:一个本地库可以拥有多个远程库,即可以把本地库的版本推送到多个远程库保存) - 为本地库添加一个新的远程库
$ git remote add remote-name https://smallez.com/xxx
(remote-name是远程库的简写名,你可以*设置) - 更改远程库的简写名
`$ git remote rename oldname newname
- 删除一个远程库配置
$ git remote rm remote-name
- 从远程库抓取最新更改的文件
$ git fetch remote-name
(并不会自动合并或修改你当前的工作) - 从远程库拉取最新更改的文件并自动尝试合并到当前所在的分支
$ git pull remote-name
-
把本地库的master分支版本推送到简写名为origin的远程库
$ git push origin master
可能会推送失败的情况:- a、没有远程库的写入权限
- b、上次从远程库抓取文件下来后,在你准备推送前,又有其它人更新了远程库
- c、由于上面的情况导致服务器上的文件可能会比你本地的要新,所以必须先从远程服务器拉取最新的文件合并到你的本地库后才能推送
- 查看某个远程仓库的信息
$ git remote show remote-name
- 当把本地库文件推送到远程库时,并不会把标签信息也自动推送过去,要另外用命令推送
$ git push origin v1.5
- 把所有不在远程库上的本地标签都推送过去
$ git push origin --tags
- 用指定的标签创建一个新分支
$ git checkout -b branchname tagname
- 把本地库的master分支推送到命名为origin的远程库的master分支,并建立跟踪
$ git push -u origin master
- 把本地库的test分支推送到命名为origin的远程库的newtest分支,并建立跟踪
$ git push -u origin test:newtest
-
当从远程库抓取更新文件时,发现一个新的分支,本地不会自动生成一份可编辑的拷贝,解决方法:
- a、抓取远程库
$ git fetch origin
(发现远程库有serverfix分支) - b、在本地库创建localfix分支,并让它保持和origin/serverfix跟踪 $ git checkout -b localfix origin/serverfix
- c、上面的操作,如果localfix名字变成了serverfix(即和远程库一样),可以简化成这样
$ git checkout --track origin/serverfix
- d、如果要在本地已有的分支跟踪一个远程分支
$ git branch -u origin/serverfix
- a、抓取远程库
- 可以用@{upstream}或@{u} 作为快捷字符,来代替正在跟踪的分支路径,比如 origin/master
-
查看本地分支和远程分支的对应列表
$ git branch -vv
(注意它只是在本地做比较并未连到远程服务器,第20条有说明)
显示含义:- a、 master 1ae2a45 [origin/master] deploying index 表示本地master分支正在跟踪origin/master分支并且是最新的
- b、 iss53 7e424c3 [origin/iss53: ahead 2, behind 1] forgot the brackets 表示iss53分支正在跟踪origin/iss53并且ahead是2,意味着本地有两个提交还没有推送到服务器上,behind 1表示落后1,服务器上有一次提交还没抓到本地并入
- 接上面第19条,先抓取所有的远程库,然后再显示列表
$ git fetch --all; git branch -vv
- 删除远程库的分支
$ git push origin --delete serverfix
- 生成自己对远程库版本做了哪些修改的摘要
$ git request-pull origin/master myfork
分支操作命令(git创建的默认分支名为master)
- 只是创建一个新分支并不切换过去
$ git branch testing
- 通过查看历史版本的信息,知道各个分支当前所指的版本对象
$ git log --oneline --decorate
- 切换到另一个分支
$ git checkout testing
(这条命令做了两件事,一是使 HEAD 指向 testing 分支,二是将工作目录恢复成 testing 分支所指向的快照内容) - 查看项目分叉历史,以便知道分支的共同父节点
$ git log --oneline --decorate --graph --all
- 新建一个分支并同时切换到那个分支
$ git checkout -b new-branch-name
相当于下面两个命令:
$ git branch new-branch-name
$ git checkout new-branch-name
- 假如当前分支是master,有一个包含最新修改的hotfix分支需要合并过来,使用
$ git merge hotfix
- 假设已完成了上面的第6个操作,hotfix分支已经没用了,需要删除hotfix分支
$ git branch -d hotfix
-
合并分支时发生冲突的解决方法:
- a、先查看哪些文件冲突了
$ git status
(会出现Unmerged paths) - b、手工打开这些文件,修改里面的代码,并删除 <<<<<<< , ======= , 和 >>>>>>>
- c、把修改好的文件全都添加到暂存区
$ git add filename
- d、改完所有冲突的文件并都添加到暂存区后提交它们即可
$ git commit
- a、先查看哪些文件冲突了
- 查看当前库所有分支的列表
$ git branch
(前面有*星号的是当前工作的分支) - 查看每一个分支的最后一次提交
$ git branch -v
- 查看哪些分支已经合并到当前分支
$ git branch --merged
(未打*星号的分支都是已并入的,已经没用了,可以删除掉) - 查看所有包含未合并工作的分支
$ git branch --no-merged
- 强制删除一个未合并工作的分支
$ git branch -D unmerged-branch-name
- 合并(merge)和变基(rebase)的区别:合并会保留历史分叉信息,变基会把一个分支上的变化应用到另一个分支上,去掉了分叉
-
fix分支变基到master:把fix分支上的修改应用到master上,再把fix和master合并,最后只保留master,删除fix
- a、先切换到fix分支
$ git checkout fix
- b、变基到master分支上
$ git rebase master
或者$ git rebase master fix
- c、切换到master分支
$ git checkout master
- d、把fix合并到master上
$ git merge fix
- e、删除fix分支
$ git branch -d fix
- a、先切换到fix分支
- 变基的使用注意事项:如果版本分叉历史已提交到了远程库,切勿再在本地用变基的方法抹平这些分叉
- 基于master分支建立带命名空间的分支
$ git branch sc/branchname master
(其中sc是贡献该项工作的人名称的简写)
查看提交历史
- 查看提交历史
$ git log
- 只查看最近2次提交历史
$ git log -2
- 查看最近1次提交和上一版本的内容差异
$ git log -p -1
- 查看提交历史的简略统计信息
$ git log --stat -3
- 格式化输出提交历史
$ git log --pretty=format:"%h - %an, %ad : %s"
- 形象化展示提交历史
$ git log --pretty=format:"%h %s" --graph
(如果历史记录有分支会看得更清楚) - 显示某一特定日期前的提交历史
$ git log --since=2009-01-15
- 查看对某关键字进行了修改操作的提交历史
$ git log -Skeyword
- 查看提交历史中,2008年10月期间,Tom提交的但未合并的测试文件,可以用下面的查询命令:
$ git log --pretty="%h - %s" --author=Tom --since="2008-10-01" --before="2008-11-01" --no-merges
- 只显示所有origin/master分支,但不在issue54分支的提交
$ git log --no-merges issue54..origin/master
- 查看最后一次提交的详细信息
$ git log --pretty=fuller -1
stash储藏工作
- 在当前工作没做完时,需要转到master分支改bug,但不想提交现在的工作,可以用储藏
$ git stash save "work in progress"
- bug改完提交后,切换回先前的分支,然后继续先前的工作
$ git stash apply
- 查看储藏队列清单
$ git stash list
- 删除指定的储藏
$ git stash drop stash@{0}
- 清空储藏队列
$ git stash clear
- 应用指定的储藏
$ git stash apply stash@{1}
- 应用最近的储藏并且应用后就删除它
$ git stash pop
- 创建一个分支来应用储藏
$ git stash branch new-branch-name
清理工作目录
- 看看当前工作目录中可删除的未跟踪文件和空目录有哪些(-n表示只看看,但不执行)
$ git clean -d -n
- 把上面列出的未跟踪文件和空目录删除掉
$ git clean -f -d
- 只移除没有加入到.gitiignore的未跟踪文件
$ git clean
- 使用-x参数将加入到.gitiignore的文件也删除
$ git clean -d -x
如何在Github上工作
- 先fork官方项目
- 把项目克隆到本地
$ git clone https://github.com/smallezcom/blink
- 进入项目目录
$ cd blink
- 创建一个分支用做开发新功能
$ git checkout -b slow-blink
- 在分支上做修改
- 改完之后提交到本地库
$ git commit -a -m 'comment'
- 推送自己的slow-blink分支到远程库的slow-blink分支
$ git push -u origin slow-blink
- 在GitHub网站上浏览分支页面,点击“Compare & pull request”按钮进入创建合并请求的页面
- 输入标题和描述,让项目的官方人员考虑接受自己的修改
- 写完上面的标题和描述后,点击“Create pull request”,项目的官方人员会收到自己的合并请求
- 项目官方人员会在GitHub上审查你代码,并可以在线单击某一行代码进行评论
- 官方人员的评论会通过电子邮件通知到自己
- 官方人员如果最终同意合并,他可以点击页面上的“Merge pull request”按钮来合并,或者把贡献者的分支拉取到本地合并后,再推送到GitHub
-
如果自己提交的合并请求与现在官方的版本产生冲突怎么办?
- a、用另外一个简写名把官方库的链接再添加一次到本地
$ git remote add blink2 https://github.com/smallezcom/blink
- b、把新命名的远程库再抓取一遍到本地
$ git fetch blink2
- c、确保当前是在自己想提交到官方的分支上,然后尝试把新抓取的官方版本合并过来
$ git merge blink2/master
- d、上面的输出信息会告知你哪些文件存在冲突
- e、手工修改这些冲突的文件,然后提交到本地库
- f、把自己想提交到官方的分支再次推送到官方远程库
$ git push origin slow-blink
- a、用另外一个简写名把官方库的链接再添加一次到本地
Git命令的别名替换
- 用 git ci 代替 git commit 的方法
$ git config --global alias.ci commit
以后提交文件可以这样输入$ git ci -m 'your comment'
- 用 git last 代替 git log -1 HEAD 的方法
$ git config --global alias.last 'log -1 HEAD'
以后查看最后一次提交可以这样输入$ git last
忽略不想被纳入Git管理的文件
- 建立忽略文件.gitignore
$ cat .gitignore
- 在文件中加入想忽略的文件路径或匹配模式
*.a
表示忽略以 .a 结尾的文件
*.[oa]
表示忽略所有以 .o 或 .a 结尾的文件*~
表示忽略所有以波浪符 ~ 结尾的文件!lib.a
表示不忽略lib.a,(即使前面忽略了所有以 .a 结尾的文件,也不能忽略lib.a)/TODO
只忽略当前目录下的TODO文件,不包含子目录下的TODO文件build/
忽略build/目录下的所有文件doc/*.txt
忽略诸如 doc/notes.txt 这样的文件,但不忽略 doc/server/arch.txtdoc/**/*.pdf
忽略 doc/test.pdf、doc/other/test.pdf 等文件,两个星号**表示匹配任意中间目录`
- 忽略文件.gitignore本身也要加入到暂存区以便提交到仓库
$ git add .gitignore
- 可以下载已经写好的,适用于各种开发环境的忽略文件 https://github.com/github/gitignore
Git服务器架设
- 服务器上的远程库通常只是一个裸仓库(bare repository)— 即一个没有工作目录的仓库
- Git可使用四种协议来传输资料:本地协议(Local),HTTP 协议,SSH(Secure Shell)协议及 Git 协议
- 本地协议范例
$ git clone /opt/git/project.git
或$ git clone file:///opt/git/project.git
- HTTP协议范例
$ git clone https://smallez.com/gitproject.git
- SSH 协议范例
$ git clone ssh://user@server/project.git
或$ git clone user@server:project.git
- Git 协议范例
$ git clone git://192.168.10.2/project.git
-
从一个Git库创建它的裸仓库方法:
- a、假如Git库的目录名是test,需要进入到它的上一级目录
- b、使用--bare参数克隆test库的裸仓库
$ git clone --bare test test.git
- c、用ls命令查看一下,新建的test.git目录就是裸仓库
- 把裸仓库拷贝到服务器上就可以用SSH协议来访问这个远程库了,例如
$ git clone user@192.168.0.1:/path/test.git
- 从一个空目录创建裸仓库,并赋予此目录的“组可写”权限:先
cd newdir
然后在newdir目录里面$ git init --bare --shared
-
修改git用户的shell工具,以保证服务器安全性:
- a、编辑文件
$ sudo vim /etc/shells
- b、在/etc/shells文件中把shell工具的绝对路径加进去 /usr/bin/git-shell (实际路径请通过命令which git-shell得知)
- c、使用chsh修改git用户的shell工具
$ sudo chsh git
(shell工具要写绝对路径)
- a、编辑文件
-
添加要用git用户登录ssh的公钥:
- a、先在git用户主目录下创建.ssh目录
$ mkdir .ssh && chmod 700 .ssh
- b、创建存放用户公钥的文件
$ touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys
- c、把用户公钥都保存到authorized_keys里面
$ cat id_rsa.john.pub >> ~/.ssh/authorized_keys
- a、先在git用户主目录下创建.ssh目录
- HTTP协议的远程库搭建请参考《Pro Git》 https://git-scm.com/book/en/v2/Git-on-the-Server-Smart-HTTP
三种分布式工作流程
- 集中式工作流 (团队共同维护一个公有远程库)
- 集成管理者工作流 (GitHub的典型模式 --- 每个人都可以fork一个自己的库,修改后通知官方维护者拉取更新)
- 司令官与副官工作流 (相当于分级别的集成管理者工作流)
常见疑问
- 把一个文件添加到暂存区后未提交的情况下,又对此文件做了修改,怎么办?
答:如果此时提交,放入到仓库的文件是前面添加该文件到暂存区时刻的内容,并非最后修改完的内容,如果要在提交时,让仓库保存最新的内容,只要在提交前再 git add 此文件即可。 - 如何生成本地电脑上的SSH公钥和私钥?
答:使用ssh-keygen命令即可,按提示输入存储路径和密码$ ssh-keygen
-
假如自己维护着一个远程库,如何知道别人提交过来版本修改了哪些东西?
答:- a、先新建一个分支contrib
$ git checkout -b contrib
- b、在新分支上应用别人提交过来的补丁,或把别人的提交抓取过来合并到该分支
- c、用log命令查看别人的提交历史(排除master分支上的历史)
$ git log contrib --not master
- d、接上面,如果要输出完整的差异信息
$ git log contrib -p --not master
- e、找出contrib和master分支的共同祖先
$ git merge-base contrib master
- f、显示contrib分支和指定分支(比如SHA-1校验码为36c7db)的差异
$ git diff 36c7db
- g、找出contrib和master分支共同祖先,然后显示contrib和这个祖先的差异
$ git diff master...contrib
(相当于ef的简化)
- a、先新建一个分支contrib
-
假如自己维护着一个远程库,对于别人提交过来的版本,只想选择里面某一个提交来应用到master分支,怎么办?
答:使用cherry-pick拣选更改即可- a、通过上面第3里面的abc步得知想要的提交为 e43a6fd3e94888d76779ad79fb568ed180e5fcdf
- b、执行拣选
$ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf
- c、拣选之后,如果新建的分支不想要了,可以删除它
- 在本地创建了一个git库,要把这个本地库提交到远程(如GitHub)新创建的库,出现No tracked branch configured for branch master 或 refusing to merge unrelated histories 错误,怎么办?
答:出现这个问题是因为两个库不同源造成的(因为是分别创建的),只需要在本地先 pull 把远程库的文件同步过来,然后再push即可, 但是pull的时候一定要加上 --allow-unrelated-histories 参数。
参考命令git pull origin master --allow-unrelated-histories
,执行了这条命令后,就可以正常push了。