03 git学习笔记 - xiaoxin01/Blog GitHub Wiki
传统cvs的做法是以初始档案为基础,记录每次提交的变化
git每次提交会保存一份全部文件的快照,为了高效,如果文件没有更改,则只会保存该文件的链接
Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。
Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希看起来是这样:
24b9da6552252987aa493b52f8696cd6d3b00373
Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。
你执行的 Git 操作,几乎只往 Git 数据库中增加数据。 很难让 Git 执行任何不可逆操作,或者让它以任何方式清除数据。
已提交(committed)、已修改(modified)和已暂存(staged)
- Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。
- 工作目录是对项目的某个版本独立提取出来的内容,供你使用或修改。
- 暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作“索引”,不过一般说法还是叫暂存区域。
Git 自带一个 git config 的工具来帮助设置控制 Git 外观和行为的配置变量。 这些变量存储在三个不同的位置:
- /etc/gitconfig 文件: 包含系统上每一个用户及他们仓库的通用配置。 如果使用带有 --system 选项的 git config 时,它会从此文件读写配置变量。
- ~/.gitconfig 或 ~/.config/git/config 文件:只针对当前用户。 可以传递 --global 选项让 Git 读写此文件。
- 当前使用仓库的 Git 目录中的 config 文件(就是 .git/config):针对该仓库。
在 Windows 系统中,Git 会查找 $HOME 目录下(一般情况下是 C:\Users$USER)的 .gitconfig 文件。
当安装完 Git 应该做的第一件事就是设置你的用户名称与邮件地址。因为每一个 Git 的提交都会使用这些信息,并且它会写入到你的每一次提交中,不可更改:
git config --global user.name "John Doe"
git config --global user.email [email protected]
使用 git config --list 命令来列出所有 Git 当时能找到的配置。
可能会看到重复的变量名,因为 Git 会从不同的文件中读取同一个配置(例如:/etc/gitconfig 与 ~/.gitconfig)。 这种情况下,Git 会使用它找到的每一个变量的最后一个配置。
可以通过输入 git config : 来检查 Git 的某一项配置
$ git config user.name John Doe
有三种方法可以找到 Git 命令的使用手册:
git help <verb>
git <verb> --help
man git-<verb>
例如,要想获得 config 命令的手册,执行
git help config
你也可以尝试在 Freenode IRC 服务器( irc.freenode.net )的 #git 或 #github 频道寻求帮助。 这些频道经常有上百人在线,他们都精通 Git 并且乐于助人。
文件的生命周期如下:
$ git status -s
M README # 已经修改,但是没有add(放入暂存区)
M lib/simplegit.rb # Old file Staged
MM Rakefile # add之后又再次修改
A lib/git.rb # New file Staged
?? LICENSE.txt # Untracked file
我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。格式规范如下:
- 所有空行或者以 # 开头的行都会被 Git 忽略。
- 可以使用标准的 glob 模式匹配。
- * 匹配零个或多个任意字符;
- [abc] 匹配任何一个列在方括号中的字符
- ? 只匹配一个任意字符;
- [0-9] 表示匹配所有 0 到 9 的数字
- 两个星号(*) 表示匹配任意中间目录,比如 a/**/z 可以匹配 a/z , a/b/z 或 a/b/c/z 等。
- 匹配模式可以以(/)开头防止递归。
- 匹配模式可以以(/)结尾指定目录。
- 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
- git diff
- git diff --staged
- git commit
- git commit -a (等于git add + git commit)
- git rm
- git rm --cached
- git log
- git log -p 同时显示更改内容
- git log -n 显示最近n次的提交
- git log --stat 同时显示每次提交的summary信息
- git log --author 显示指定作者的提交
- git log --grep 包含指定关键字的提交
- git log -Sfunction_name 包含function_name的修改的提交
- git log --since=2.weeks 2周以内的提交
- git log --pretty=[oneline|short|full|fuller]
- git log --pretty=format:"%h %s" --graph 图形化展示提交历史
- git log --pretty=format:"%h - %an, %ar : %s"
Table 1. git log --pretty=format 常用的选项
选项 | 说明 |
---|---|
%H | 提交对象(commit)的完整哈希字串 |
%h | 提交对象的简短哈希字串 |
%T | 树对象(tree)的完整哈希字串 |
%t | 树对象的简短哈希字串 |
%P | 父对象(parent)的完整哈希字串 |
%p | 父对象的简短哈希字串 |
%an | 作者(author)的名字 |
%ae | 作者的电子邮件地址 |
%ad | 作者修订日期(可以用 --date= 选项定制格式) |
%ar | 作者修订日期,按多久以前的方式显示 |
%cn | 提交者(committer)的名字 |
%ce | 提交者的电子邮件地址 |
%cd | 提交日期 |
%cr | 提交日期,按多久以前的方式显示 |
%s | 提交说明 |
更多格式参考:pretty-formats
更多参考:Git 基础 - 查看提交历史
- git commit --amend 继续向上一次commit提交修改,而不新增新的commit
- git reset [file] 取消暂存
- git checkout [file] 撤销修改(丢数据,危险)
- git remote
- git remote -v
- git remote add [name] [url]:添加远程仓库
- git fetch [name] :获取远程仓库数据
- git push [remote-name] [branch-name]:推送同名分支到远程仓库
- git remote show [remote-name]:列出远程仓库的 URL 与跟踪分支的信息。
- git remote rename [ori-name] [new-name]:重命名远程仓库名称
- git remote rm [remote-name]:删除远程仓库
Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。
一个轻量标签很像一个不会改变的分支——它只是一个特定提交的引用。
附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG)签名与验证。 通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的。
- git tag:显示所有标签
- git tag [tag-name]:添加轻量标签
- git tag -a [tag-name] [commit]:给指定commit打标签
- git tag -a [tag-name] -m [message]:添加附注标签
- git show [tag-name]:查看标签信息
- git push origin [tag-name]:把指定标签推送给远程仓库
- git push origin --tags:把所有远程仓库不存在的标签推送
- git tag -d [tag-name]:删除本地标签
- git push origin :refs/tags/[tag-name]:删除远程仓库标签
- git checkout [tag-name]:查看标签文件内容(detacthed HEAD),你的新提交将不属于任何分支,并且将无法访问,除非确切的提交哈希。因此,这通常需要创建一个新分支:
- git checkout -b [branch-name] [tag-name]
如果不想每次都输入完整的 Git 命令,可以通过 git config 文件来轻松地为每一个命令设置一个别名。 这里有一些例子你可以试试:
git config --global alias.unstage 'reset HEAD --'
git config --global alias.last 'log -1 HEAD'
要执行外部命令,而不是一个 Git 子命令。 如果是那样的话,可以在命令前面加入 ! 符号:
git config --global alias.visual '!gitk'
Git 仓库中有3种对象:
- blob 对象(保存着文件快照)
- 一个树对象(记录着目录结构和 blob 对象索引)
- 一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
提交对象会包含一个指向上次提交对象(父对象)的指针。
Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符)
更多参考:分支简介
- git checkout -b [branch-name]:以当前分支为基础新建分支
- git merge [branch-name]:合并指定分支到当前分支
- 如果当前分支是指定分支的直接祖先,fast-forward,直接将指针向前移动
- 如果master 分支所在提交并不是 指定 分支所在提交的直接祖先,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。见下图
- git branch -d [branch-name]:删除本地分支
手动修改冲突,然后
- git add
- git commit
- git branch:获取分支列表
- git branch -v:获取分支列表及最后一次提交
- git branch [--merged|--no-merged]:获取[已经|还没有]merge到当前分支的分支列表
- git branch -d删除no-merged分支的时候会失败,因为还没有merge
远程跟踪分支是远程分支状态的引用。 它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。
- git ls-remote:显示远程跟踪分支列表
- git remote show [branch]:显示远程分支更多信息
- git fetch origin:更新远程跟踪分支(获取远程仓库有而本地没有的数据)
- git push origin [branch]:refs/heads/[branch]:refs/heads/[branch],推送分支branch
- git push origin [branch1]:[branch2]:将本地branch1分支推送到远程branch2分支
- git checkout -b [branch] [remotename]/[branch]:根据远程分支创建本地跟踪分支
- git checkout --track [remotename]/[branch]:同上,省略了第一个branch,创建同名的
- git branch [-u|--set-upstream-to] [remotename]/[branch]:对于已经存在的分支创建跟踪
- git branch -vv:列出分支的跟踪信息
- git push origin --delete [branch]:删除远程分支
- 基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。
需要重点注意的一点是这些数字的值来自于你从每个服务器上最后一次抓取的数据。 这个命令并没有连接服务器,它只会告诉你关于本地缓存的服务器数据。
git fetch --all
- git rebase [basebranch]:将basebranch新的commit插入到当前分支前面
- git rebase --onto master server client:取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的commit,然后把它们合并到master分支
- git rebase [basebranch] [targetbranch]:将basebranch新的commit插入到targetbranch分支前面
- git pull --rebase:
一个远程仓库通常只是一个裸仓库(bare repository)——即一个没有当前工作目录的仓库。 因为该仓库仅仅作为合作媒介,不需要从磁盘检查快照;存放的只有 Git 的资料。 简单的说,裸仓库就是你工程目录内的 .git 子目录内容,不包含其他资料。
Git 可以使用四种主要的协议来传输资料:
- 本地协议(Local)
- HTTP 协议
- SSH(Secure Shell)协议
- Git 协议
借助一个名为 git-shell 的受限 shell 工具,你可以方便地将用户 git 的活动限制在与 Git 相关的范围内。配置服务器
- git diff --check:检查空白错误
- git log [-g]:查看当前分支提交历史
- git rev-parse [branch]:查看分支SHA-1
- git reflog:列举 HEAD 和分支引用所指向的历史。
- 每当你的 HEAD 所指向的位置发生了变化,Git 就会将这个信息存储到引用日志这个历史记录里。
- git show HEAD@{n}:查看当前分支第n次commit详细信息
- HEAD^==HEAD~,HEAD~n
- git log [refA]..[refB]:查看refB还没有合并到refA的commit,等价于
- git log ^refA refB
- git log refB --not refA
- 被 refA 或 refB 包含的但是不被 refC 包含的提交
- git log refA refB ^refC
- git log refA refB --not refC
- git log origin/master..HEAD:查看即将合并的commit
- git log [--left-right] [refA]...[refB]:查看被两个引用中的一个包含但又不被两者同时包含的提交
当你在项目的一部分上已经工作一段时间后,这时你想要切换到另一个分支做一点别的事情。 不想仅仅因为做了一半的工作创建一次提交。 针对这个问题的答案是 git stash 命令。
- git stash:存储更改
- git stash list:查看存储的更改
- git stash apply [stashid]:恢复存储的更改
- git stash drop:删除存储
- git stash pop:应用并删除存储
- stash save --keep-index:不要储藏 git add 命令已暂存的东西。
- git stash [-u|--include-untracked] 存储包含未跟踪的文件
- git stash branch:从存储创建分支
- git clean -f -d:直接清除当前更改,并删除未被跟踪的文件
- git clean -d -n:清除演习,查看即将删除什么内容
- git clean -x:同时移除被 .gitiignore 或其他忽略文件中的模式匹配的文件
- git stash --all:将所有更改放入存储
如果你从因特网上的其他人那里拿取工作,并且想要验证提交是不是真正地来自于可信来源,Git 提供了几种通过 GPG 来签署和验证工作的方式。
- Git Grep
- git grep -n [keyword]:查找keyword并显示所在行号
- git grep --count:仅仅包括哪些文件包含匹配以及每个文件包含了多少个匹配。
- git log -S[keyword] --oneline:显示包含keyword的commit
全局修改邮箱地址
在你开始工作时忘记运行 git config 来设置你的名字与邮箱地址,或者你想要开源一个项目并且修改所有你的工作邮箱地址为你的个人邮箱地址。 任何情形下,你也可以通过 filter-branch 来一次性修改多个提交中的邮箱地址。 需要小心的是只修改你自己的邮箱地址,所以你使用
git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="[email protected]";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
这会遍历并重写每一个提交来包含你的新邮箱地址。 因为提交包含了它们父提交的 SHA-1 校验和,这个命令会修改你的历史中的每一个提交的 SHA-1 校验和,而不仅仅只是那些匹配邮箱地址的提交。
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。
索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的“暂存区域”,这就是当你运行 git commit 时 Git 看起来的样子。
工作目录会将它们解包为实际的文件以便编辑。 你可以把工作目录当做 沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。
reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:
- 移动 HEAD 分支的指向 (若指定了 --soft,则到此停止)
- 使索引看起来像 HEAD (若未指定 --hard,则到此停止)
- 使工作目录看起来像索引
假如我们运行 git reset file.txt (这其实是 git reset --mixed HEAD file.txt 的简写形式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft 或 --hard),它会:
- 移动 HEAD 分支的指向 (已跳过)
- 让索引看起来像 HEAD (到此处停止)
它有 取消暂存文件 的实际效果,正好和 git add 相反。
- git reset eb43bf file.txt:从某个commit拉取文件
- git reset --soft [commit]:可以通过此方法来压缩提交
和 reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。
不带路径时
- checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 它会在工作目录中先试着简单合并一下,这样所有_还未修改过的_文件都会被更新。而 reset --hard 则会不做检查就全面地替换所有东西。
- 第二个重要的区别是如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。
下面的速查表列出了命令对树的影响。 “HEAD” 一列中的 “REF” 表示该命令移动了 HEAD 指向的分支引用,而 “HEAD” 则表示只移动了 HEAD 自身。 特别注意 WD Safe? 一列——如果它标记为 NO,那么运行该命令之前请考虑一下。
HEAD | Index | Workdir | WD Safe? | |
---|---|---|---|---|
Commit Level | ||||
reset --soft [commit] | REF | NO | NO | YES |
reset [commit] | REF | YES | NO | YES |
reset --hard [commit] | REF | YES | YES | NO |
checkout [commit] | HEAD | YES | YES | YES |
File Level | ||||
reset (commit) [file] | NO | YES | NO | YES |
checkout (commit) [file] | NO | YES | YES | NO |
有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
Git 可以将它的数据“打包”到一个文件中。 这在许多场景中都很有用。 有可能你的网络中断了,但你又希望将你的提交传给你的合作者们。 可能你不在办公网中并且出于安全考虑没有给你接入内网的权限。 可能你的无线、有线网卡坏掉了。 可能你现在没有共享服务器的权限,你又希望通过邮件将更新发送给别人,却不希望通过 format-patch 的方式传输 40 个提交。
这些情况下 git bundle 就会很有用。 bundle 命令会将 git push 命令所传输的所有内容打包成一个二进制文件,你可以将这个文件通过邮件或者闪存传给其他人,然后解包到其他的仓库中。
replace 命令可以让你在 Git 中指定一个对象并可以声称“每次你遇到这个 Git 对象时,假装它是其他的东西”。 在你用一个不同的提交替换历史中的一个提交时,这会非常有用。
例如,你有一个大型的代码历史并想把自己的仓库分成一个短的历史和一个更大更长久的历史,短历史供新的开发者使用,后者给喜欢数据挖掘的人使用。 你可以通过用新仓库中最早的提交 replace 老仓库中最新的提交来连接历史,这种方式可以把一条历史移植到其他历史上。 这意味着你不用在新历史中真正替换每一个提交(因为历史来源会影响 SHA 的值),你可以加入他们。
在.git目录中有如下文件:
$ ls -F1
HEAD # 指示目前被检出的分支
config # 包含项目特有的配置选项
description # 仅供 GitWeb 程序使用
hooks/ # 包含客户端或服务端的钩子脚本(hook scripts)
info/ # 包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns)。
objects/ # 存储所有数据内容
refs/ # 存储指向数据(分支)的提交对象的指针
Git 是一个内容寻址文件系统。Git 的核心部分是一个简单的键值对数据库(key-value data store)。
可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容。
# hash-object 命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
# -w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键值。 --stdin 选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径。
# 校验和的前两个字符用于命名子目录,余下的 38 个字符则用作文件名。
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
# cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示格式友好的内容:
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
Git 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。
一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
树对象的内容大致如下: