git

基本概念

Git是一个分布式版本控制软件,Linus Torvalds在2005年用十天时间编写出第一个git版本。相比于集中式版本控制软件,不需要服务端软件就能实现版本控制。 Git最出色的是其合并追踪能力,我在实际使用中发现,借助Emacs编辑器magit插件可以非常优雅的处理rebasemerge过程中产生的冲突。

日常操作中,需要熟练的功能主要有:环境配置,远程管理,分支管理,错误追踪等。

推荐在Emacs下利用magit插件做基本仓库管理,相比于直接用命令行,利用插件更加能够方便把代码和版本管理合二为一。例如差异区和代码之间跳转,选择性提交差异区等操作都比直接用命令行快捷很多。

每一个仓库都有一个.git目录,该目录下常见文件及功能如下表所示:

文件 功能
branches/ 分支信息,该目录已不再使用
config 配置信息
description 描述信息
HEAD 头指针
hooks/ 用于在特定事件下触发的脚本
info/exclude 用于指定要忽略的文件
objects/ 数据对象,包括commits、trees、blobs、tags等
refs/ 包括分支指针,远程指针,标签指针等

基本使用

环境配置

  • 初始化配置
    git init                                # 初始化仓库
    git init --separate-git-dir <gitdir>    # 初始化分离仓库
    git config [--global] user.name "Micky Ching"
    git config [--global] user.email "mickyching@gmail.com"
    
    separate-git-dir
    对于重要项目,我个人比较喜欢用分离仓库,这样可以较好的防止对代码批处理时错误的修改仓库数据库。
    global
    使用该选项会将配置信息写入用户配置文件中,不使用则将配置信息写入当前仓库配置文件中。
    user.name
    由于提交需要提交者信息,所以配置用户名和邮箱是很有必要的。

    另外往往需要配置哪些文件需要忽略,这通过.gitignore文件来记录要忽略的文件或规则。

  • 提交代码

    仓库一旦初始化就可以添加文件并提交了,更高级的可以选择添加一个diff区或一行修改等等。

    git add <files>                         # 添加文件/文件夹
    git add -i                              # 交互式添加文件
    git commit                              # 提交,启动编辑器编辑提交信息
    git commit -m <message>                 # 提交,直接写入提交信息
    git commit --amend                      # 提交,修改上一次提交
    

    仓库中的文件有三种状态,其转换关系如下图所示:

    git-status.png

    在首次提交前,添加了错误文件时,由于没有HEAD,不能使用git reset HEAD撤销,此时可以使用如下命令来撤销添加。

    git rm -r --cached <files>              # 撤销add添加的文件
    
  • 信息查看
    git status                              # 查看项目状态
    git log                                 # 查看日志
    git diff <commit>                       # 查看相对于指定提交的更改
    git ls-files                            # 查看当前目录下被跟踪的文件
    git ls-tree -r HEAD                     # 查看当前目录下被跟踪的文件
    # 查看所有被跟踪过的文件,包括已经被删除的文件
    git log --pretty=format: --name-only --diff-filter=A | sort - | sed '/^$/d'
    

    对于日志显示,通常需要在配置文件中指定显示格式,以便阅读,需要注意的是--graph选项可能会影响速度,尤其对于大型仓库。我定义了如下两个alias命令来查看日志:

    ll = log --pretty=format:'%Cred%ad %h%Cgreen%d%<|(100,trunc) %Creset%s %C(cyan)%an <%ae>'\
       --color=auto --date=short --abbrev-commit
    lg = log --pretty=format:'%Cred%ad %h%Cgreen%d%<|(100,trunc) %Creset%s %C(cyan)%an <%ae>'\
       --color=auto --date=short --abbrev-commit --graph
    

    下图展示了用于查看不同节点间差异的方法。

    git-diff-status.png

远程管理

  • 远程配置

    创建ssh密钥以避免每次上传下载都要输入密码:

    ssh-keygen -t rsa -C "mickyching@gmail.com" -f ~/.ssh/micky-gmail
    ssh-add ~/.ssh/micky-gmail                    # 添加密钥
    ssh -i ~/.ssh/micky-gmail -T git@github.com   # 验证测试
    

    如果不添加密钥,可能会出现如下错误提示:

    Agent admitted failure to sign using the key
    

    如果要配置多个远程仓库帐号,需要编辑配置文件~/.ssh/config指定登录信息。

    Host github                             # 主机名,可任意命名
        HostName github.com                 # 登录地址
        User git
        Port 22
        IdentityFile ~/.ssh/micky-gmail     # 证书路径
    

    最后在远程服务器上设置好公钥,就可以无需输入密码实现上传和下载了。

  • 远程操作
    git remote add <repo-name> <repo-url>   # 添加远程仓库地址
    git remote -v                           # 查看远程仓库地址
    git remote set-url <repo-name> <url>    # 更改仓库地址
    git pull <repo-name> <branch>           # 拉取远程仓库分支到当前分支
    git fetch <repo-name>                   # 下载远程仓库
    git fetch <repo-name> <a>:<b>           # 将远程分支a下载为分支b
    git push <repo-name> <branch>           # 将当前分支发送到远程分支
    git push <repo-name> <a>:<b>            # 将分支a发送到远程分支b
    git push <repo-name> :<b>               # 删除远程分支
    git push --tags                         # 推送tag
    git remote set-head origin --auto       # 可用于修复remote/HEAD失效
    

分支管理

  • 分支管理
    git branch [-vv]                        # 查看分支,冗余模式能显示跟踪对象
    git branch <branchname>                 # 创建分支
    git branch -d <branchname>              # 删除已合并的分支
    git branch -D <branchname>              # 强制删除分支
    git branch -dr <remote/branch>          # 删除本地下载的远程分支
    git remote prune <remote>               # 删除远程已经不存在的跟踪分支
    git checkout <branchname>               # 切换分支
    git checkout <commit> -- <filename>     # 检出指定文件
    git rebase <branch-b>                   # 基于指定分支: ours=branch-b
    git merge <branch-b>                    # 合并指定分支: ours=branch-a
    git reset <commit>                      # 复位到指定提交
    git revert <commit>                     # 反转提交
    

    通常来说,创建一个分支往往是为了单独实现一个较大的功能,在实现完成之后再合并到主分支。 rebasemerge的区别在于rebase之后只有一条主线,而merge之后仍然保留多条主线,只不过最后将其汇合为一点。仓库管理的基本原则是merge越少越好,所以提交之前需要在本地做好rebase再推送,因为当你打算推送的时候,远程可能已经被别人修改了,此时最佳策略就是将本地rebase到远程最新提交点再提交。

    同样的道理,用reset的方法废除一个提交不会在仓库中留下痕迹,但是revert实际上保存了两个提交。原则上仓库中revert越少越好,所以如果一个提交有问题,需要彻底废除,在未推送到远程之前,最佳做法是通过reset将其废除,如果已经推送,那么只能通过revert来废除原来的提交。

    其实对于提交也需要同样注意,尽量做到每个提交都很好的完成一个提交的任务,如果代码改动很大,就要做成多个独立的提交。简单的说,每个提交要保证其正确性、可用性、可读性,不要滥用提交。

  • 标签管理
    git tag <v0.1>                          # 添加tag
    git tag                                 # 列举tag
    

错误追踪

如果要查询一个文件在何时由谁引入,或者文件中某一行何时由谁引入,可以用如下命令:

git blame <filename>                    # 查看每一行在何时引入
git blame -L n,m <filename>             # 查看文件指定行何时引入
-L n,m
其中n表示起始行,m表示显示行数。

要查询某个错误在哪个提交引入的,可以尝试二分查找方法:首先标定一个起始位置,然后通过二分检出验证并标记状态的方式搜索。

git bisect start [<bad> <good>]         # 开始二分查找
git bisect reset                        # 结束二分查找,回到开始前的位置
git bisect good [<commit>]              # 标记当前为good
git bisect bad [<commit>]               # 标记当前为bad
git checkout bisect/bad                 # 切换到最终定位的bad
git bisect log                          # 查看二分日志,可以用重定向保存
git bisect replay <file.log>            # 从二分日志恢复进度
git bisect HEAD1 HEAD2                  # 标记查询区间

实用操作

  • 暂存管理
    git stash                               # 添加当前修改到暂存区
    git stash list                          # 列举暂存区
    git stash pop                           # 弹出暂存区
    
  • 仓库清理
    git clean -ndx                          # 显示要删除的文件列表
    git clean -fdx                          # 删除仓库之外的文件
    git gc                                  # 垃圾清理,建议少用
    

    所有的清理动作都应该慎重。

  • 选取提交
    git cherry-pick <commit>                # 选取指定提交放到当前位置
    
  • 仓库打包
    git archive --prefix="emacs/" -o emacs.zip HEAD
    git archive -o emacs-partial.tar HEAD lisp/ site-lisp/
    git archive --format=tar --prefix="emacsd/" master | gzip > emacsd.tar.gz
    

repo

基本用法

对于多个仓库的管理,git自带有submodule,但是并不好用。这里推荐repo来管理多个仓库,谷歌在Android项目中的使用已经展现了repo优秀的能力。

基本命令如下:

repo help                               # 获取帮助信息
repo help command                       # 获取指定命令的帮助信息
repo init -u URL                        # 初始化下载地址
repo init -u URL -b <branch>            # 初始化下载仓库的某个分支
repo status                             # 查看所有仓库的状态
repo branches                           # 查看每个仓库分支信息
repo sync                               # 同步所有仓库
repo sync [project-list]                # 同步指定仓库
repo update [project-list]              # 上传指定仓库
repo diff [project-list]                # 查看修改
repo start <branch>                     # 为每个仓库创建分支
repo prune <branch>                     # 删除已经merge的分支
repo abandon <branch>                   # 强制删除分支
repo manifest                           # 生成manifest文件
repo foreach [project-list] -c command  # 对每个仓库执行指定命令
repo forall -c command                  # 不限于git命令
repo version                            # 查看repo版本信息

在网络不好的情况下调用repo sync经常会中途失败,可以用如下脚本来多次同步:

repo sync
while [ $? -ne 0 ]
do
    repo sync
done

在调用repo status查看的时候前两个字符分别表示暂存区和工作区状态,具体如下表所示:

第1个字符 暂存区状态 第2个字符 工作区状态
- 没有文件被修改 - 未更改
A 有文件添加 m 已更改
D 有文件删除 d 已删除
M 有文件更改    
R 有文件重命名    
C 有文件被复制    
T 有文件模式被修改    
U 有冲突未处理