Total Pageviews

Saturday, 21 October 2017

Git的用户信息设置

全局设置

在日常的学习工作中,经常需要使用Git来管理项目和代码。为了方便,很多人都会通过执行:
yee@ubuntu:~$ git config --global user.name "user"
yee@ubuntu:~$ git config --global user.email "user@users.noreply.github.com"
来设置全局用户信息,以免每个项目还要单独操作。
但是这样会带来一个副作用:一般来说工作代码需要使用公司账号信息,这样每当初始化工作项目的时候,务必还得修改对应设置,否则提交上去发现错了还得放开远程仓库权限做 force push ,非常麻烦。该如何解决呢?

1. Don’t guess my identity

Git的开发社群里也有相同的困扰,于是在2.8版本中增加了 user.useconfigonly 设置项来强制每个Git项目单独设置git user,避免上面所说的不小心使用个人邮箱提交公司代码的情况发生。

使用方法:

  1. 执行
    yee@ubuntu:~$ git unset --global user.email
    yee@ubuntu:~$ git unset --global user.name
    yee@ubuntu:~$ git config --global user.useconfigonly true
    
  2. 在各个项目里单独设置用户信息
没有执行第2步的话 git commit 时就会报错,避免错误的提交。

2. Conditional Configurations

从2.13版本开始,Git能够通过条件设置,方便的为工作项目全局配置git user为公司邮箱,来更好的解决这个问题。
当前只能对指定git目录设置
务必确保个人设置放在公司设置前面!!

使用方法:

  1.  ~/.gitconfig 全局设置文件里增加:
    [includeIf "gitdir:~/personal/"]
      path = .gitconfig-personal
    [includeIf "gitdir:~/go/src/go.company.com/"]
      path = .gitconfig-work
    [includeIf "gitdir:~/work/"]
      path = .gitconfig-work
    
  2. 添加 ~/.gitconfig-work 工作信息文件:
    [user]
      name = company
      email = company@meican.com
    
  3. 添加 ~/.gitconfig-personal 个人信息文件:
    [user]
      name = personal
      email = personal@gmail.com
    
通过以上设置,在 ~/go/src/go.company.com/  ~/work/ 目录下的项目会使用 .gitconfig-work的配置,其它则会使用 .gitconfig-personal 的配置了。

后悔药

如果真不小心忘记修改就提交了,可以使用以下脚本修改仓库中的错误用户信息
#!/bin/sh

yee@ubuntu:~$ git filter-branch --env-filter '

OLD_EMAIL="personal@gmail.com"
CORRECT_NAME="company"
CORRECT_EMAIL="company@meican.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_AUTHOR_NAME="$CORRECT_NAME"
    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' -f --tag-name-filter cat -- --branches --tags
需要临时解除master保护状态,执行 git push --force
---------------

Git常用命令备忘

Git配置

git config --global user.name "robbin"   
git config --global user.email "fankai@gmail.com"
git config --global color.ui true
git config --global alias.co checkout
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.br branch
git config --global core.editor "mate -w"    # 设置Editor使用textmate
git config -l  # 列举所有配置
用户的git配置文件~/.gitconfig

Git常用命令

查看、添加、提交、删除、找回,重置修改文件

git help <command>  # 显示command的help
git show            # 显示某次提交的内容
git show $id

git co  -- <file>   # 抛弃工作区修改
git co  .           # 抛弃工作区修改

git add <file>      # 将工作文件修改提交到本地暂存区
git add .           # 将所有修改过的工作文件提交暂存区

git rm <file>       # 从版本库中删除文件
git rm <file> --cached  # 从版本库中删除文件,但不删除文件

git reset <file>    # 从暂存区恢复到工作文件
git reset -- .      # 从暂存区恢复到工作文件
git reset --hard    # 恢复最近一次提交过的状态,即放弃上次提交后的所有本次修改

git ci <file>
git ci .
git ci -a           # 将git add, git rm和git ci等操作都合并在一起做
git ci -am "some comments"
git ci --amend      # 修改最后一次提交记录

git revert <$id>    # 恢复某次提交的状态,恢复动作本身也创建了一次提交对象
git revert HEAD     # 恢复最后一次提交的状态

查看文件diff

git diff <file>     # 比较当前文件和暂存区文件差异
git diff
git diff <$id1> <$id2>   # 比较两次提交之间的差异
git diff <branch1>..<branch2> # 在两个分支之间比较 
git diff --staged   # 比较暂存区和版本库差异
git diff --cached   # 比较暂存区和版本库差异
git diff --stat     # 仅仅比较统计信息

查看提交记录

git log
git log <file>      # 查看该文件每次提交记录
git log -p <file>   # 查看每次详细修改内容的diff
git log -p -2       # 查看最近两次详细修改内容的diff
git log --stat      # 查看提交统计信息

tig

Mac上可以使用tig代替diff和log,brew install tig

Git 本地分支管理

查看、切换、创建和删除分支

git br -r           # 查看远程分支
git br <new_branch> # 创建新的分支
git br -v           # 查看各个分支最后提交信息
git br --merged     # 查看已经被合并到当前分支的分支
git br --no-merged  # 查看尚未被合并到当前分支的分支

git co <branch>     # 切换到某个分支
git co -b <new_branch> # 创建新的分支,并且切换过去
git co -b <new_branch> <branch>  # 基于branch创建新的new_branch

git co $id          # 把某次历史提交记录checkout出来,但无分支信息,切换到其他分支会自动删除
git co $id -b <new_branch>  # 把某次历史提交记录checkout出来,创建成一个分支

git br -d <branch>  # 删除某个分支
git br -D <branch>  # 强制删除某个分支 (未被合并的分支被删除的时候需要强制)

分支合并和rebase

git merge <branch>               # 将branch分支合并到当前分支
git merge origin/master --no-ff  # 不要Fast-Foward合并,这样可以生成merge提交

git rebase master <branch>       # 将master rebase到branch,相当于:
git co <branch> && git rebase master && git co master && git merge <branch>

Git补丁管理(方便在多台机器上开发同步时用)

git diff > ../sync.patch         # 生成补丁
git apply ../sync.patch          # 打补丁
git apply --check ../sync.patch  # 测试补丁能否成功

Git暂存管理

git stash                        # 暂存
git stash list                   # 列所有stash
git stash apply                  # 恢复暂存的内容
git stash drop                   # 删除暂存区

Git远程分支管理

git pull                         # 抓取远程仓库所有分支更新并合并到本地
git pull --no-ff                 # 抓取远程仓库所有分支更新并合并到本地,不要快进合并
git fetch origin                 # 抓取远程仓库更新
git merge origin/master          # 将远程主分支合并到本地当前分支
git co --track origin/branch     # 跟踪某个远程分支创建相应的本地分支
git co -b <local_branch> origin/<remote_branch>  # 基于远程分支创建本地分支,功能同上

git push                         # push所有分支
git push origin master           # 将本地主分支推到远程主分支
git push -u origin master        # 将本地主分支推到远程(如无远程主分支则创建,用于初始化远程仓库)
git push origin <local_branch>   # 创建远程分支, origin是远程仓库名
git push origin <local_branch>:<remote_branch>  # 创建远程分支
git push origin :<remote_branch>  #先删除本地分支(git br -d <branch>),然后再push删除远程分支

Git远程仓库管理

git remote -v                    # 查看远程服务器地址和仓库名称
git remote show origin           # 查看远程服务器仓库状态
git remote add origin git@github:robbin/robbin_site.git         # 添加远程仓库地址
git remote set-url origin git@github.com:robbin/robbin_site.git # 设置远程仓库地址(用于修改远程仓库地址)
git remote rm <repository>       # 删除远程仓库

创建远程仓库

git clone --bare robbin_site robbin_site.git  # 用带版本的项目创建纯版本仓库
scp -r my_project.git git@git.csdn.net:~      # 将纯仓库上传到服务器上

mkdir robbin_site.git && cd robbin_site.git && git --bare init # 在服务器创建纯仓库
git remote add origin git@github.com:robbin/robbin_site.git    # 设置远程仓库地址
git push -u origin master                                      # 客户端首次提交
git push -u origin develop  # 首次将本地develop分支提交到远程develop分支,并且track

git remote set-head origin master   # 设置远程仓库的HEAD指向master分支
也可以命令设置跟踪远程库和本地库
git branch --set-upstream master origin/master
git branch --set-upstream develop origin/develop
----------

git总结

前言

本文其实是软工课程的一次作业,算是对自己之前使用 Git 的一些心得体会的总结吧。

基础概念

Git 是什么

在计算机或者编程相关的社区有一个词 RTFM(Read The Fucking Manual) 很常见,意思是学习一个东西先去看它的手册,这对于 Git 来说也是一样的。我觉得学习 Git 最好的办法就是参考 Manual,比如 Manual 的第一页:
Git is a fast, scalable, distributed revision control system with an unusually rich command set that provides both high-level operations and full access to internals.
可以看出 Git 的定位:Git 是一个强大的分布式版本控制工具。
这里值得强调的一点是分布式——其意义就在于每个人都可以拥有仓库完整的一个副本,并且相互独立的在各自副本上提交更改,因此即使有一天 GitHub 突然消失了,只要每个人把自己代码仓库的副本拿出来就能创建第二个 GitHub,这就是分布式的优势。

Repository 仓库

在 git 中项目的基础单位是 Repository,一般中文翻译称为“仓库”。
创建一个仓库很简单,只要:
1
git init .
就创建完成了。可以注意到的是此时本地出现了一个 .git 文件夹,git 就用它来存储版本信息。
当然我们也可以获取一个远程仓库,比如:
1
git clone https://github.com/wtdcode/tun2socks
就可以把一个远程仓库克隆到本地。这里有一个细节是 git 会根据 URL 自动选择合适的协议,除了 https,git 还支持 http 和 ssh。

Add 添加

作为一个版本控制工具,首先我们得告诉它哪些文件是属于我们项目需要管理的,比如我们在项目根目录下创建了一个 Readme.md 希望用 git 去管理可以:
1
git add Readme.md
把它添加到版本库。

Commit 提交

仅仅 git add 的话 git 只是知道项目有哪些文件了,但是 git 还没有开始跟踪文件的更改。
比如我们向 Readme.md 中写入一行 Hello world 后希望提交这次更改就可以:
1
git commit Readme.md -m "My first commit!"
然后 git 会输出类似的内容:
1
2
3
[master (root-commit) e1db2a9] My first commit
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.md
这里就创建了一次提交,它的标记是 e1db2a9,这个标记是独立的,从某种意义上可以理解为是一个版本。
此外 git 要求每次提交必须写 commit message,因此可以用 -m 参数写明 commit message。
在 git 中,只要被提交过的代码就一定可以找回,而且 git 每次只会记录提交之间文件的变化,因此不会占用太多空间。

工作区、暂存区、版本库

add 和 commit 只是 git 给我们提供的抽象,只有理解这两个操作背后的实现才能更好的理解 git 的工作方式。
第一个概念是工作区。工作区就是 .git 所在的目录但是不包括 .git 本身,比如在上面的例子中 Readme.md 就在工作区中。
第二个概念是暂存区,所有被 add 添加的更改都会放在暂存区中,当 commit 的时候更改就会从暂存区写入仓库。
第三个概念是版本库,版本库其实包括暂存区和仓库,也可以说就是 .git 这个目录本身。
所以现在应该理解,其实 add 和 commit 就分两步把我们的更改提交到了仓库,那么暂存区的存在意义是什么?
考虑这样一个场景:为了完成功能A,修改了文件 a 并且添加到了暂存区,接着突然要修复 Bug B修改了文件 a 和 b,那么问题来了现在文件 a 被修改了两次但是理由是不一样的,所以 a 应该被分段提交,此时暂存区就体现出来了。
由于文件 a 的第二次修改还没进入暂存区,因此可以方便的对文件 a 进行提交后再添加新的修改到暂存区再提交,这样就可以分段提交了,防止两次不相关的修改被合并在一个commit。

pull 和 push

前面提到 git 是分布式的,但是为了协作我们往往需要让本地的仓库和远程仓库同步。
其中把本地仓库的更改同步到远程仓库可以使用:
1
git push
而反过来从远程仓库拉取更改可以使用:
1
git pull
其中注意的一点是 git pull 实际上就是 git fectch 后立即 git merge,其中 git fetch 用于拉取更改而 git merge 用于合并潜在的冲突。
同时如果远程仓库和本地仓库有冲突,可以用 git push -f 来让远程仓库强行和本地仓库保持同步,但是这是非常不被推荐的,因为其他协作者在 pull 的时候会出现诸如冲突分歧等问题。

工作流

用 git 的最终目的还是要管理项目,而用好 git 的关键我个人认为是要建立一个良好的工作流,也就是高效的协作。
下面结合我对工作流的理解介绍一下 git 的一些进阶用法。

revert VS reset

前面提到过,只要是提交到 git 的更改就一定不会丢失,那么这是怎么做到的呢?比如我们希望回到第一次提交 Hello world 时候的版本就可以:
1
git reset e1db2a9
其中 e1db2a9 就是当时提交后返回的版本号,可以通过 git reflog 或者 git log 查看。
此外值得一提的是 git reset 也支持向后回溯,只要 commit 的版本号还存在版本库中。
但是在版本回退的时候还有一个指令是 git revert,比如我们又进行了一次提交
1
2
3
echo 1 >> Readme.md
git add Readme.md
git commit -m "Test"
并且产生了一个新的 commit 版本号 6af2cef,那么为了回退到最初 Hello world 的状态我们就可以
1
git revert 6af2cef
但是和 git reset 不同的是 git revert 会产生一个新的 commit 表明撤回了哪些 commit。
从效果上 git revert 和 git reset 都达到了版本回退的效果,那么怎么选择呢?仍然是先看 manual:
Note: git revert is used to record some new commits to reverse the effect of some earlier commits (often only a faulty one). If you want to throw away all uncommitted changes in your working directory, you should see git-reset(1), particularly the --hard option.
所以对于一个工作流来说,我个人的理解是:
  • 如果 commit 已经同步到了远程仓库,那么只选 git revert 绝对不要 git reset 尤其不要 git push -f(忽略冲突强行同步)
  • 如果 commit 还停留在本地仓库,但是要回退掉的版本是有意义的,比如一个错误决策下的 feature,应该选择 git revert 保留必要的信息(比如回退的理由)。
  • 如果 commit 还停留在本地仓库,但是要回退掉的版本是无意义的,比如一次错误的提交,那么应该选择 git reset 来避免污染工作流。
总之核心思想是,每次提交都应该对工作流是有意义的,应该避免错误的或者无意义的提交。

branch 和 checkout

在 git 中一个重要的概念是分支。一个仓库默认分支是 master 分支。
假想这样一个情景,在产品开发的时候同时需要两个功能 A 和 B,如果两个人同时在 master 分支上开发难免会有各种各样的冲突,为了解决这个问题就可以:
1
git checkout -b featureA
来建立一个新分支,这里其实等价于两步
1
2
git branch featureA
git checkout featureA
其中 git branch 用于管理分支而 git checkout 实际上是用于在分支之间切换。
这样只要两个人分别在 featureA 和 featureB 上独立开发完后再合并到 master 就可以保持一个干净的工作流了。

理解分支

但是到这里还没有结束,我认为要想完全理解分支,用对分支必须要理解的是 head。
什么是 head?其实 head 就是一个引用或者说一个指针,默认每个分支都会有一个 head 指向当前分支最新的提交,比如单个分支下 master 作为一个 head 指向最新的提交 C:
1
2
3
4
          master
            |
            |
A --> B --> C
但是另外一个概念是 HEAD,它特指当前的 head,比如在上图中就是:
1
2
3
4
5
6
7
          master
            |
            |
A --> B --> C
            |
            |
           HEAD
创建分支其实就是简单的增加了一个新的 head,比如创建 featureA 后现在 master 和 featureA 都指向最新的提交 C:
1
2
3
4
5
6
7
      master, featureA
            |
            |
A --> B --> C
            |
            |
           HEAD
但是如果我们在 featureA 上完成了一次新的提交 D 情况就有变化了:
1
2
3
4
5
6
7
8
9
10
11
          master
            |
            |
A --> B --> C
             \   featureA
              \     | 
               \    |
                --> D
                    |
                    |
                   HEAD
可以看到 master 没有变化,但是 featureA 和 HEAD 都移动到了最新的提交 D 上。
这时候如果我们再用 git checkout master 移动到 master 分支就变成了:
1
2
3
4
5
6
7
8
          master
            |
            |
A --> B --> C
            | \   featureA
            |  \     | 
           HEAD \    |
                 --> D
然后如果我们再在 master 上进行一次提交 E 的话就变成了:
1
2
3
4
5
6
7
8
            master, HEAD
                  |
                  |
A --> B --> C --> E
              \   featureA
               \     | 
                \    |
                 --> D
所以分支和提交实际上就是指针操作,理解了 head 和 HEAD 就能完全理解分支。

merge VS rebase

刚才我们已经提到了,虽然分成了不同分支,但是最后还是要合并的,那问题来了:怎么合并?我认为这是贯穿版本控制的核心问题,也是做好工作流的关键。
上面说过 git pull 其实是 git fetch + git merge,实际上还有一个选项是 git pull -r,它等于 git fetch + git rebase,这里就代表了两种不同的合并方式。
在了解合并之前,一个重要的概念是冲突。
冲突是什么?其实正如其字面意思,两个 commit 之间发生了冲突,比如在 featureA 和 featureB 上同时修改了一个文件的同一个位置,这时候合并就会冲突,因为 git 也不知道到底谁做的修改是使用者想要的。
解决冲突的方法是一样的,git merge 和 git rebase 区别在于合并的方式和对工作流的影响。

fast-forward

首先值得一提的是 fast-forward,也就是对于下面这种工作流:
1
2
3
4
5
6
7
8
          master
            |
            |
A --> B --> C
            | \   featureA
            |  \     | 
           HEAD \    |
                 --> D
由于其本质还是线性的,因此无论是 git merge featureA master 还是 git rebase featureA master 都会首先尝试 fast-forward 方法来合并,因为只需要把 master 和 HEAD 移动到 featureA 即可:
1
2
3
4
5
6
             master, HEAD
                  |
                  |
A --> B --> C --> D
                  |
               featureA
但是对于下面这种工作流就有区别了:
1
2
3
4
5
6
7
8
             master, HEAD
                  |
                  |
A --> B --> C --> E
              \   featureA
               \     | 
                \    |
                 --> D

merge

无论是手工还是自动的,假设已经成功的解决了冲突,那么 git merge 会产生一个新的提交来表示这次合并:
1
2
3
4
5
6
7
8
9
10
11
                      master, HEAD
                            |
                            |
A --> B --> C -----> E ---> F
              \            /
               \          /
                \        /
                 --> D --
                     |
                     |
                 featureA
其中提交 F 有两个祖先 E 和 D 代表它是由一次 merge 得到的。

rebase

但是如果是 git rebase featureAgit 会首先找到两个分支的公共祖先 C,然后暂存 C 之后所有的提交(这里只有E),接着把 featureA 上 C 之后所有的的提交(这里只有 D)放到 master 上然后再把之前暂存的提交放在其后。根据具体 rebase 的情况,我们很可能得到这样的工作流:
1
2
3
4
5
6
                  master, HEAD
                        |
                        |
A --> B --> C --> D --> E'
                  |
               featureA
可以看到跟 merge 最大的不同就是,工作流是完全线性的。

比较

直观来看 rebase 似乎要更优一些,因为它可以产生一个完全线性的历史,好处包括不限于
  • 方便 git log 查看提交历史
  • 减少了无意义的合并提交
  • 满足强迫症
但是万物都有两面,rebase 一个致命的问题是它其实重写了中间所有的提交历史,在上图中我用 E' 强调了这一点(因为 E' 和 E 的祖先不同)。
这会导致什么问题呢?如果 master 分支是跟远程仓库同步的,那么现在其他仓库的 git 会察觉到从 C 开始 master 分支在远程和本地出现了 diverge,也就是分歧,为了解决这个分歧有两个解决方案
  • git push -f 这是最直接的解决方案,前提是没有人在 master 分支工作。
  • git merge 如果有人在 master 分支工作,这是可选的方案,但是会导致一个额外的合并提交和一些无意义的更改信息。
无论哪个解决方案都会对工作流带来致命的影响,所以使用 rebase 的法则就是:如果分支(比如上述的 master)在别处有副本(比如有其他人在协作),那么绝对不应该被作为 rebase 的对象。
但是反过来,把 master 分支 rebase 到 featureA 分支上是常见的操作,有利于保持线性的历史和同步开发进度。
总之 merge 和 rebase 实际上是对工作流的选择,但是如何合并这个问题没有银弹,需要结合实际情况来看。

一些失败案例

因为我学习使用 git 也有很长一段时间了,所以现在查看之前 git 仓库的一些提交的时候可以发现很多问题。

git 管理二进制文件

之前在做软工项目的时候,我最后为了省事采用 git 直接管理了 docx 和 ppt 等文件,现在来看是非常不合适的,原因有以下三条
  • 二进制文件在版本库中不是以 diff 而是原样存储的(不考虑压缩),每次修改都会产生一个新的副本,会让 .git 快速膨胀。
  • 一旦出现冲突,二进制文件基本不可能自动合并必须手动合并。
  • 极大的增加了每次 git clone 的时间。
后来的解决方法是用石墨或者 Google docs 来单独做文档的协作。

merge 和 rebase 滥用

同样是软工项目,当初因为没有理解 merge 和 rebase 因此出现非常多的滥用。
其实就是几个 feature 在并行开发,但是由于没有 rebase 导致了非常多的无用合并提交。

舍不得提交和提交写了一半的代码

同样也是软工项目,当时协作的时候有队友经常提交写了一半的代码或者攒了几百行才提交一次。
这两个问题其实可以归结为一个问题就是对 git 的设计哲学理解不清。
我认为 git 的种种设计都是鼓励开发者勤提交,因为一旦出现问题可以非常方便的进行版本回退,同时大量有质量的提交也有助于其他人理解项目发展和设计理念。

总结

git 作为一个优秀的版本控制工具,的确值得我们好好学习和实践,但是在使用的同时,我认为理解 git 的设计哲学和一些背后的实现有助于我们更好的管理项目。

参考资料