【VCS】Git之无尽探索

本文是关于Git探索的一篇文章,阐述了Git的大部分命令和使用方式,并列举了几个典型的使用场景以供参考和体会。

多host环境
clone,remote,fetch,pull,push
gitignore
add
stash
commit
status,log,diff,blame,grep
branch,checkout,merge
tag
cherry-pick
checkout,reset,revert,rm
rebase
mv,repack,bisect
Other Case

对于Git这个分布式的VCS,从链表的角度来看待是最容易理解的:
一次commit相当于添加一个节点,节点由hash标识,内容就是所做修改的索引;每个分支都是一条链,有一个指向头结点的指针HEAD

Git配置

下载地址:点我跳转下载

添加环境变量:

以Windows为例:

%GIT_HOME%\bin;%GIT_HOME%\usr\bin;

生成SSH密钥:

ssh-keygen -t rsa -C "[email protected]"

会提示输入路径,建议:
/c/User/用户名/.ssh/id_rsa_abc

添加多Host环境

添加多host环境,对于在公司使用自己的机器是很有必要的,公私分明嘛~
文件位置/C/User/用户名/.ssh/config,没有就新建一个:内容如下

# 配置git.oschina.net 
Host git.oschina.net 
    HostName git.oschina.net
    IdentityFile C:\\Users\\用户名\\.ssh\\id_rsa_oschina
    PreferredAuthentications publickey
    User oschinaUserName
    
# 配置github.com
Host github.com                 
    HostName github.com
    IdentityFile C:\\Users\\用户名\\.ssh\\id_rsa_github
    PreferredAuthentications publickey
    User githubUserName
    

测试连接:
连通后会生成 /C/User/用户名/.ssh/known_hosts 文件:

ssh -T [email protected]
ssh -T git.oschina.net

这样就可以把公钥 id_rsa_xxx 添加到git服务器上的 SSH Keys 中去了。

最后可按需要,顺带配置用户信息:
全局配置:

git config --global user.name 姓名
git config --global user.email 邮箱

为当前仓库设置用户信息:

git config --local user.name 姓名
git config --local user.email 邮箱

一般仓库可以采用SSH方式关联,但必须以HTTP/HTTPS方式关联远程仓库时,可以考虑记住密码:在用户目录下生成文件.git-credential记录用户名密码的信息。

# 默认会话时长(据说是15min):
git config --local credential.helper cache		# linux/mac
git config --local credential.helper wincred	# windows

# 自己定义会话时间:
git config credential.helper 'cache --timeout=3600'		# linux/mac
git config credential.helper 'wincred --timeout=3600'	# windows
# 不过期会话时间:
git config --local credential.helper store

至此,Git配置结束。

Git操作

Git相关的命令如果记不太清楚可以使用提示:

git --help
git 具体某条命令 --help

仓库(clone,remote,fetch,pull,push)

在当前目录初始化一个仓库:

git init

克隆一个远程仓库:

git clone <URL> [--branch <branch> --depth=1] [./dir]

# 准备后续免密push的操作(字符需要Escape方式编码,git remote -v可看到密码)
git clone http://账号:密码@git.ops.test.com.cn/root/puppet.git

添加子module,这样子文件夹会做文件处理:

git submodule add <URL> <./submodule>

远程仓库关联:

git remote [-v]
git remote add <name> <URL>
git remote set-url <name> <URL>
git remote remove <name>

拉取本地仓库没有的内容(新增分支,远程分支修改等):

git fetch [origin <branch>]

拉取本分支的更新并合并:pull = fetch + merge,默认拉去所有合并到对应分支,当指定远程分支时默认合并到当前分支:

git pull origin <remote_branch>[:<local_branch>]

准备推送本分支的修改到远程仓库前可以先查看本地分支和远程分支的关联情况:

git branch -vv

已经关联远程分支可以直接push:

git push

远程分支不存在:

git push [-u] origin <local_branch>:<remote_branch>
	# -u 表示添加关联

只关联不push:

git branch --set-upstream-to <origin/remote_branch> <local_branch>

忽略追踪(.gitignore)

编辑仓库中的 .gitignore 文件,内容格式如下

vim .gitignore
    # 忽略目录
    build/
    # 忽略yml后缀文件
    *.yml
    # 不忽略a.yml
    !a.yml

gitignore只能忽略untracked文件,对于stracked文件需要删除索引:

git rm --cached [-r] <file>

Git中的文件状态

Git仓库中文件大概有如下几种状态,并可通过相应的操作切换:

git-file-status

staging area(add)

  • 保存(new-untracked)和(modified),不包括(remove-untracked):

    git add .
    
  • 保存(remove-untracked)和(modified),不包括(new-untracked):

    git add -u
    
  • 保存所有变化:

    git add -A
    
  • 部分提交,后续会有选择,舍弃的修改将留在working directory:

    git add -p filename
    	# y - 存储这个hunk 
    	# n - 不存储这个hunk 
    	# q - 离开,不存储这个hunk和其他hunk 
    	# a - 存储这个hunk和这个文件后面的hunk 
    	# d - 不存储这个hunk和这个文件后面的hunk 
    	# g - 选择一个hunk 
    	# / - 通过正则查找hunk 
    	# j - 不确定是否存储这个hunk,看下一个不确定的hunk 
    	# J - 不确定是否存储这个hunk,看下一个hunk 
    	# k - 不确定是否存储这个hunk,看上一个不确定的hunk 
    	# K -不确定是否存储这个hunk,看上一个hunk 
    	# s - 把当前的hunk分成更小的hunks 
    	# e - 手动编辑当前的hunk 
    	# ? - 输出帮助信息
    

暂存(stash)

这样可以保持分支的工作现场干净,操作对象是整个staging area

# push
git stash

# pick
git stash apply

# pop
git stash drop

# pop & pick
git stash pop

repository(commit)

提交:

git commit [filename] -m "annotation"

提交working directory,相当于:git add -u + git commit -m "boom~"

git commit -am "boom~"

撤销HEAD并提交staging area补充到HEAD提交:

git commit -amend -m "make up"

查看(status,log,diff,blame,grep)

查看staging areaworking directory的状态:

git status

查看提交记录:

git log --online --graph --all
	# all: 所有分支(默认当前)
	# oneline: 简易hash, --pretty=oneline
	# graph: 画出合并切出图

操作记录,含reset

git reflog --oneline

文件对比:

  • 对比 (staging area | repository) 和 working directory

    git diff [branch1 branch2] <file>
        # 加branch会对比两个分支中的文件
    
  • 对比 repositoryworking directory

    git diff HEAD <file>
    
  • 对比 repositorystaging area

    git diff --cached <file>
    

查看修改人:

git blame <file>

查找内容:

git grep '[123]\{1,\}'

分支(branch,checkout,merge)

分支可以理解为链表,链表中的元素是commit
查看分支:

git branch [-a]
	# -a 包含远程分支

切出分支:

git checkout [-b <new_branch>] origin/release|$hash$|tags/v1.0

合并developmaster

  1. 合并前检查是否存在冲突:

    git diff master develop --stat
    
  2. 开始合并,并解决冲突:

    (master)$ git merge [--squash] develop
    	# --squash 融合提交记录,参见rebase
    
    # 对于无法自动合并的冲突需要手动处理
    	<<<<<< HEAD 
    	# 当前更改
    	======
    	# 传入的更改
    	>>>>>> dev 
    
  3. 提交前检查冲突是否处理完毕:

    git diff --check
    	# 如果没问题就可以 commit -am 了
    
  4. 如果冲突太多太麻烦,想放弃解决了,可以终止:

    git merge --abort
    

删除分支:

# -r表示一并删除origin里面分支,这只是删除了一个本地追踪,下次不会再fetch它:
git branch -d [-r] <branch_name>
# 删除远程的分支还需要push过去删除
git push origin -d <remote_branch>

标签(tag)

Fetch仓库TAG:

git fetch origin --tags

查看TAG:

git tag
git tag -l 'v1.0.*'
git show v1.0.0

在当前分支HEAD打TAG:

git tag v0.1
# 在某个commit上打tag,并加附注
git tag -a v0.2 -m "version 0.1 released" $hash$

push tag:

git push origin v1.0
# push 本地所有tag
git push origin --tags

删除tag

# 删除本地tag
git tag -d v1.0
# 删除远程tag
git push origin --delete tag v1.0

cherry-pick

使用cherry-pick可以把其他分支的一部分commit提交到当前分支:

git log --all --online
# cherry-pick
git cherry-pick [-n] $hash$ $hash$
	#-n,表示先不commit,把commits保留到staging area

撤销(checkout,reset,revert,rm)

checkout:用staging area覆盖work directory

git checkout -- <file>

reset:用repository覆盖staging area,若working directory有修改,则丢弃staging area

git reset HEAD <file>
  • soft, 把repository的($hash$,HEAD]切出到staging area
    git reset --soft HEAD~1|$hash$
    
  • mixed, 把repository的($hash$,HEAD]切出到working directory
    git reset --mixed HEAD~1|$hash$
    
  • hard, 把repository的($hash$,HEAD]抛弃:
    git reset --hard HEAD~1|$hash$
    

案例:找回reset掉的内容:

  1. 查看操作记录:
    git reflog --oneline
    
  2. 找到某次reset操作(reset记录会以reset前的HEAD做hash标识),把HEAD指向它:
    git reset --hard $reset_hash$
    # 或者
    git rebase --onto HEAD HEAD $reset_hash$
    

revert:提交逆修改,HEAD继续前进,而resetHEAD回退:

git revert HEAD~1|$hash$

rm --cached:删除追踪索引,不能操作work directory的文件;该操作会把文件从staging area|repository 移到work directory并置为untracked;此外如果该文件曾commit过,会在staging area生成一个deleted:<file>记录:

git rm --cached [-r] <file>

rm:删除repository中的文件,并在staging area添加一个deleted:<file>

git rm <file>

rm -f:删除untracked文件和文件夹:

git rm <file> -f [-r] [-q] 
	# -r 表示递归
	# -q 无删除回显

clean -fd:清理工作区,递归删除所有untracked的文件和文件夹:

git clean -fd

好了,重头戏来了。

衍合(rebase)

这个翻译其实有些抽象,rebase的作用是对一段线性提交做一些操作,操作后分化出一个新的分支。
这里我对提交的理解是:若干个commit以hash标识成点,形成一个commit链;其中HEAD是指针变量,分支名是指针常量(链表头结点,或者理解成一维数组名)。
下面的语法中,endpoint是可选的,默认为HEAD。如果发现endpoint常量指针(即分支名),则分化出的分支自动应用到该分支并checkout过去。

语法:

git rebase [-i] [--onto] [base_branch] startpoint [endpoint] 
	# 其中线性提交段是 (startpoint, endpoint],前开后闭

rebase合入分支最新修改(merge按时间排序commit,rebase根据分支排序),reabase把冲突处理交给提MR的人处理,提交记录比merge更干净:

git rebase origin/master [HEAD]
	# solve conflicts
git add <conflict_files>
git rebase --continue

编辑分支上的一段线性提交(reword,squash,reorder,drop):

git rebase -i startpoint [endpoint]
	# p, pick = 使用提交
	# r, reword = 使用提交,但修改提交说明
	# e, edit = 使用提交,但停止以便进行提交修补
	# s, squash = 使用提交,但和前一个版本融合
	# f, fixup = 类似于 "squash",但丢弃提交说明日志
	# x, exec = 使用 shell 运行命令(此行剩余部分)
	# d, drop = 删除提交

下面分析4个rebase典型案例,假设当前分支状态如下图所示:
[外链图片转存失败(img-odPtFdEq-1562834637827)(https://i.loli.net/2019/03/14/5c8a3fbc6f7d9.png)]

案例1-并集:next基于master;此时得知master有新修改,next需要同步master的最新修改。

git rebase master next

# 命令完成后自动切到next分支
# 此时next = [master + next#HEAD]

案例2-差集:topic基于next,next基于master;此时得知next有重大bug,要求topic中属于next部分的提交都得去掉。

# 差集计算:topic = master + (topic - next)
git rebase --onto master next topic

# 多段差集:topic = master + (topic#HEAD~4, topic]
git rebase --onto master HEAD~4 topic

案例3-修改commit:编辑(reword,reorder,edit,drop)一段Commit:

git rebase -i HEAD~4 HEAD

案例4-融合commit:融合(squash)操作也属于修改,但这里单独拿出来示例,因为它可以多人合作完成,也可以个人独立完成:

  1. 方式一:由A同学解冲突。 A同学利用squash合并commit,B同学直接合并。
    # A同学
    git rebase -i next topic
    
    # B同学
    (next)$ git merge topic
    
  2. 方式二:由B同学解冲突。 B同学自己融合A同学提交的MR。
(next)$ git merge --squash topic
git commit -m "merged topic to next"

其他命令(mv,repack,bisect)

# 重命名/移动staged的文件后自动处于staged
git mv oldfile newfile
git mv file folder

# repack 把松散对象打包以提高git运行效率
git repack -d

# 二分查找有问题的提交
git bisect start master f608824
git bisect run make test
	# test脚本为判断是否有问题的依据
	line = gets
	exit 1 if line != "15\n"

一些技巧

  1. 浅克隆:对于比较大的仓库,本地不想要那么多无关的分支,无关的历史记录,可以考虑浅克隆。

    # 以浅克隆的方式获取库
    git clone <URL> --branch <master> --depth=1
    
    # 添加origin中的分支
    git remote set-branches origin <branch_new1> <branch_new2>
    
    # 以浅克隆的方式fetch origin中的分支
    git fetch --depth 1 origin [<branch_new1>]
    
    # 深化origin 中的克隆深度为 N
    git fetch origin --deepen N
    
    # 删除分支
    git branch -rd <branch_name>
    git push origin -d <branch_name>
    
    
  2. 拓扑序:对于提交日志,默认是按commit时间排序,拓扑序可以把相关联的多个提交放在一起显示。

    # 默认按时间排序
    git log --date-order --oneline
    
    # 指定拓扑排序
    git log --topo-order --oneline
    
  3. 二分查找:有时候无法从final版本代码定位问题,可能需要回滚代码定位。可以考虑log(N)复杂度的二分查找。

    # 开始二分查找(reset的时候会回到这个HEAD)
    git bisect start
    # 告诉git,当前的HEAD提交是有问题的
    git bisect bad
    # 告诉git,某个提交是OK的
    git bisect good $hash$
    # 这时git会reset到中间的commit,此时编译验证,并告知结果
    git bisect <bad/good>
    # 这样git会不断划分区间查找,最终会告诉你哪个commit有问题。找到后退出查找模式
    git bisect reset
    

以上。

此外,如题,笔者也在不停探索,如果有错误欢迎指出斧正~

最后感谢这个游戏:Githug

以及这个通关攻略:「Githug」Git 游戏通关流程

还有这个在线练习平台:Git在线练习

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章