Git 基础用法

Git 是一个免费的开源分布式版本控制系统,旨在快速高效地处理从小型到大型项目的所有内容。Git 的使用要比 Svn 复杂很多,Git 命令也很多,这里做个简单记录,方便查阅。

你要相信,机会不来是因为你还没准备好。

git 基础

  • 文件状态: 未跟踪(untracked)、已提交(committed)、已修改(modified)、已暂存(staged)
  • 工作区域: 远程仓库、工作目录、暂存区域
  • 在工作目录修改文件 –> committed ot modified
  • 将文件快照放入暂存区域 –> modified to staged
  • 提交更新 –> staged to commited

配置

git 配置有三个作用域,其应用优先级:local > global > system

  • --system 配置文件为 /etc/gitconfig 下,所有用户的配置
  • --global 配置文件为 ~/.gitconfig~/.config/git/config 下,当前用户的配置
  • --local 配置文件为 proj/.git/config,当前仓库的配置

查看配置

  • 查看所有配置项 git config [--system|--global|--local] --list
  • 查看特定配置项 git config [--system|--global|--local] user.name

如果不指定作用域,则按优先级获取,即在 local 取不到该项的值再从 global 取,还是取不到再从 system 取,如果 system 中也没配置,则返回空。

设置配置

1
2
3
4
5
git config --global user.name xxx
git config --global user.email xxx@example.com
git config --system core.editor vim
git config --local core.ignorecase false
git config --system core.commentchar *

如果不指定作用域,则默认为 --local

删除配置项

1
2
git config --global --unset user.name
git config --local --unset user.email

如果不指定作用域,则默认为 --local

短命令

有些命令经常使用,如果可以使用更短的命令代替会方便很多,git alias 提供了给命令设置别名的功能,打开系统配置文件 /etc/gitconfig,添加下面的配置项。

1
2
3
4
5
6
7
8
9
10
11
12
13
[alias]
co = checkout
st = status
ci = commit
br = branch
bm = branch -m
bd = branch -d 
cb = checkout -b
df = diff
ls = log --stat
lp = log -p
plo = pull origin
pho = push origin

忽略文件

.gitignore 中指定哪些文件不需要跟踪。空目录默认不会跟踪。

  • *.[oa] 所有以 .o.a 结尾的文件
  • *~ 所有以 ~ 结尾的文件
  • build/ 所有 build 目录及目录下所有文件
  • /build/ 根目录下的 build 目录及目录下所有文件
  • build 所有 build 文件
  • /build 根目录下的 build 文件
  • doc/*.txt doc 根目录下的所有文本文件
  • doc/*/.txt doc 目录下的所有文本文件

查看帮助

1
2
3
git help [verb]
git [verb] --help
man git-[verb]

git 仓库

本地仓库

本地仓库就是本地的一个文件夹,在这个文件夹下生成一个 .git 目录,这个文件夹就变成了一个本地 git 仓库。创建一个本地仓库有两种方式,第一种是在该目录下执行 git init 即可,这种方式创建创建的本地仓库不与任何远程仓库关联。

1
2
cd xxxdir
git init

第二种是使用 git clone 直接拉取一个远程仓库到本地,会自动根据远程仓库名在本地创建一个同名的文件夹,且文件夹下有 .git 目录,这种方式创建的本地仓库默认会与拉取的远程仓库关联。

1
git clone git@github.com:xxx/xxx.git

远程仓库

远程仓库是指放在远程服务器上的 respository,通常是放在使用 Git 远程版本控制的软件源代码托管平台上,比如 Github, Gitlab, Gitee 等等网站。远程仓库需要我们到托管平台上创建。

关联远程仓库

  • git remote add <remote-name> <url>

通过 git remote add 命令可以将本地仓库关联到远程仓库,一个本地仓库可关联多个远程仓库。remote-name 是我们给远程仓库取的名字,之后对该远程仓库的所有操作都使用这个名字,url 则是远程仓库的地址。

使用 get clone 拉取远程仓库到本地,默认就会关联这个远程仓库,使用的是默认名字 origin

取消关联远程仓库

  • git remote remove/rm <name>

查看关联的远程仓库

  • git remote
  • git remote -v/--verbose 显示 url
  • git remote show <name> 显示某一远程仓库的详细信息

重命名远程仓库

  • git remote rename <old-name> <new-name>

git 分支

分支简介

  • git 分支本质上仅仅是指向提交对象的可变指针,提交对象提交时分支会自动向前移动;
  • git init 会默认创建一个名为 master 的分支,这个分支和普通分支没什么区别;
  • HEAD 才是特别的分支,它指向当前所在的本地分支,可看作当前分支的别名。切换分支时 HEAD 会指向另一个分支,同时把本地目录的文件替换为另一个分支指向的文件。

分支创建

  • git branch <name>

分支切换

  • git checkout <name>
  • git checkout -b <name> 创建新分支并切换到该分支,等价于
    git branch <name>
    git checkout <name>

checkout 即查看的意思,如果参数带的是分支名,即表示查看这个分支的内容——切换到该分支;如果参数带的是文件名,即查看远程上该文件的内容,即检出该文件到本地。

查看分支

  • 查看所有分支:git branch
  • 查看所有分支最后一次提交:git branch -v
  • 查看所有已合并分支:git branch --merged
  • 查看所有未合并分支:git branch --no-merged
  • 查看所有分支包含远程分支和已删除分支: git branch -a

删除分支

  • git branch -d <name> 只能删除那些已经合并的分支
  • git branch -D <name> 能删除已合并或未合并的分支
  • git push <remote> --delete <branch> 删除远程分支

重命名分支

  • git branch -m <old-name> <new-name> 如果 <new-name> 分支已存在则重命名不成功
  • git branch -M <old-name> <new-name> 强制重命名,即使分支 <new-name> 已存在

分支合并

  • git merge <other>

other 分支合并到当前分支,合并后 other 分支的状态就是 merged,可放心删除。合并时,先切换到要合并到的分支,再 merge 要合并的分支,比如要将 hotfix 分支合并到 master 分支,则

1
2
git checkout master
git merge hotfix

合并的时候有两种情况,一是要合并到的分支 master 是要合并的分支 hotfix 的直接祖先,如下图所示,这种情况 merge 的时候会出现 Fast-forward(快进) 的提示,这种情况的合并很简单,不需要任何额外的处理,因为不会有冲突,hotfix 本身就是从 master 分化出来的,内容只会比 master 新,不存在两个分支同时修改的内容。

fast-forward

第二种情况是要合并到的分支不是要合并的分支的直接祖先,如下图一所示,把分支 iss53 合并到分支 master,这两个分支在某个时间点出现了分叉,无直接关系,只有共同的祖先 C2 快照。这种情况 Git 会自动拿 C2, C4, C5 三个快照进行合并生成一个新的快照 C6,然后让 master 分支指向 C6 快照,如下图二所示。

maybe-conflict
after-merge

merge 操作是 Git 自动完成的,通过快照对比修改本地文件,如果没冲突则自动将修改过的文件加入到暂存区,文件状态变为 staged;有冲突的文件则会为 conflict,需要我们手动处理冲突,处理完成后标记为 modified,然后手动添加到暂存区变为 staged。merge 之后只是我们本地的文件做了修改,所以还需要执行 git commit 将修改提交到远程。

关联远程分支

本地分支在两种情况下会自动关联远程分支,一是使用 git checkout <branch> 从远程检出分支到本地,检出的本地分支自动与远程分支关联,二是在本地新创建的分支,默认是没有与远程分支关联的,当有 push 新的 commit 到远程的时候,就会将本地分支关联到远程的同名分支,git push <remote> <branch> 会将 <branch> 关联到 <remote>/<branch>,如果远程分支不存在,则会创建一个。

有时候需要修改本地分支关联的远程分支,比如修改了远程仓库的地址,从 remote1 变为了 remote2,原来的 master 分支关联的是 remote1/master,需要将其变更为 remote2/master 才能正常进行 push 或 pull 操作。

手动关联远程分支:git branch --set-upstream-to=origin/<branch> <branch>

1
2
3
git remote remove origin
git remote add origin <new-url>
git branch --set-upstreasm-to=origin/master master

同步远程分支

git remote show origin 可以查看远程的所有分支以及本地分支和远程的关联,如下图所示。

show-remote

可以看到远程的分支现在有两种,一种是 tracked(已跟踪),即有用的分支,另一种是 stale(陈旧),即远程已删除的分支,但本地还保留着,可使用 prune(精剪) 命令来删除。

git remote prune origin 在本地删除陈旧的远程分支。

分支变基

分支采摘

git merge 可以把一个分支的所有最新改动合并到另一个分支,而如果想只采摘一个分支的几个提交到另一个分支,则可以使用 git cherry-pick

git cherry-pick <commit> 把某个 commit 应用到当前分支,产生一个新的 commit;
git cherry-pick <branch> 把某个 branch 的最新一次 commit 应用到当前分支,产生一个新的 commit;
git cherry-pick <commit1> <commit2> 依次采摘两个 commit;
git cherry-pick <commit1>..<commit2> 采摘 (commit1, commit2] 之间的所有 commit,不包括 commit1;
git cherry-pick <commit1>^..<commit2> 采摘 [commit1, commit2] 之间的所有 commit,包括 commit1;

查看分支树

使用 log

使用 git log --graph 可以通过查看 log 的方式来查看分支的关系树。

git log –graph –decorate –oneline –simplify-by-decoration –all

  • -–oneline 每条 commit 一行显示,一般这是必须的,否则整个树会十分庞大,不方便查看
  • -–decorate 显示每条 commit 的引用,如分支、tag 等
  • -–simplify-by-decoration 只显示被分支或 tag 引用的 commit
  • -–all 显示所有的分支

使用 gitk

gitk –-simplify-by-decoration –-all 也可以使用 gitk 工具来查看 log 信息,可以更直观的看分支的关系树。gitk 默认在 Git bash 安装的时候就会安装,在 git cmd 输入 gitk 即可打开 gitk 工具。

常用命令

检查文件状态

  • git status
  • git status -s/--short

-s 只列出所有差异文件及其状态,状态以符号的形式显示在文件前面,?? 表示未加入暂存区的新文件,A 表示已加入暂存区的新文件,M 表示已修改,D 表示已删除。符号为红色表示未添加到暂存区,绿色表示已添加到暂存区。其中 ?? 只会显示为红色,A 只会显示为绿色。

跟踪文件

git add <file> 添加文件到暂存区

  • 跟踪新文件,从 untrackedstaged?? to A(红变绿);
  • 跟踪修改的旧文件,从 modifiedstagedM to M(红变绿);
  • 跟踪删除的旧文件,从 modifiedstagedD to D(红变绿);
  • 当然也可以跟踪未修改的旧文件,只是没意义而已,跟踪之后也不会有什么变化;
  • 每次文件修改后,提交更新前都要重新跟踪,因为提交更新时是暂存区的版本,而不是本地目录的版本。

取消跟踪

git reset HEAD <file> 将文件从暂存区移除,保留文件状态,如果是新文件则从已跟踪重新变成未跟踪,旧文件已修改仍是已修改,已删除仍是已删除。

HEAD 可以省略,但如果是是取消跟踪已删除文件则不能省略,因为在本地找不到该文件,也可以用 -- 来代替 HEAD

删除文件

删除文件 git rm 及下面介绍的移动文件 git mv 是对 Linux 命令的“扩展”,如果文件未跟踪,即在版本中不存在,是本地新增的文件,则用 Linux 命令即可,而且只能用 Linux 命令,用 git 命令会报找不到文件的错误。

1
2
3
$ touch test
$ git rm test
fatal: pathspec 'test' did not match any files

git rm <file> 先使用 Linux 命令删除本地文件,再跟踪删除的文件,即添加到暂存区,相当于下面几条命令。

1
2
$ rm package.json
$ git add package.json
  • 文件未跟踪 rm <file>
  • 文件未修改未删除 git rm <file>
  • 文件已修改 git rm -f <file>

    强制删除,无论文件原来是否在暂存区,都从本地删除并重新加入暂存区

  • 文件已修改 git rm --cached <file>

    从版本中移除,保留在本地目录成为未跟踪文件

  • 文件已删除,未加入暂存区 git rm <file>

    加入暂存区,相当于只执行 git rm 的第二步操作

  • 文件已删除,已加入暂存区,无法再进行任何删除操作

移动文件

git mv <old-name> <new-name> 文件重命名并将修改加入暂存区,其实就是将旧文件删除,然后建一个新文件,再将两个文件添加到暂存区,等价于下面几条命令。

1
2
3
$ mv <oldfile> <newfile>
$ git add <oldfile>
$ git add <newfile>

git rm 一样,如果是版本中不存在的新文件,不能使用 git mv 来重命名(也没意义),直接使用 Linux 命令 mv 即可。

推送到远程仓库

  • git push <remote> <branch>

当前 <remote>-<branch> 没人抢先推送时才能推送成功,否则必须先拉取远程分支内容合并后再推送。

从远程仓库拉取

  • git fetch <remote> 拉取远程仓库的所有分支的引用,不会进行合并,必须手动合并分支;
  • git pull <remote> <branch> 拉取某个远程分支的内容并合并到本地分支。

查看 diff

  1. 查看未暂存文件的修改信息 git diff
  2. 查看已暂存文件的修改信息 git diff -cached or git diff -staged
  3. 查看距离某次 commit 的修改信息 git diff <commit>
  4. 查看单个文件的修改信息 git diff -- <file>
  5. 查看单个距离某次 commit 的修改信息 git diff <commit> -- <file>
  • 如果要查看某个文件的修改信息,直接带上文件名即可 git diff <file>,不带文件名则查看所有有更改文件的 diff;
  • 只列出有更改的行及上下文,删除的行在最前面加上 - 并显示为红色,新增的行在最前面加上 + 并显示为绿色;

提交更新

  1. 在编辑器中编辑提交信息 git commit
  2. 直接输入提交信息 git commit -m <statement>
  3. 跳过暂存区域直接提交 git commit -a

查看提交日志

git log

  • -p 显示每次提交的内容差异
  • -3 只显示最新的 3 次提交
  • --stat 显示简略的统计信息
  • --pretty 指定显示的格式
  • --author=[xxx] 按提交作者筛选

默认查看所有提交日志,如果要查看某个文件的提交日志,带上文件名即可 git log -- <file>

  • git shortlog 列出所有提交记录,只显示 commit 说明,按作者分类;
  • git shortlog --author=[xxx] 只列出某位作者或某几位作者的日志,这个命令比较常用,比如查看自己的提交日志。

储藏文件

有时候在本地做了一些修改后,想切换到其它分支或者从远程拉取最新代码,而本地的修改暂时又不想提交,则可以将本地修改储藏起来,储藏之后本地又恢复成干净状态(即无修改状态),这时候 pull 代码或 checkout 分支都不会有冲突出现。之后需要的时候再从储藏中将文件恢复到本地,恢复的时候如果修改的文件有更新,则可能会出现冲突,所以一般是想更新远程较新文件且这些文件在本地没修改,本地修改的是远程无更新文件,才会使用储藏操作。

  • 储藏本地修改文件 git stash
  • 查看储藏列表 git stash list
  • 应用储藏 git stash apply

    如果要恢复指定的储藏,带上名字即可,比如 git stash aaply stash@{1},默认从最近一次的储藏进行恢复,等价于 git stash apply stash@{0}

  • 取消储藏 git stash show -p | git apply -R

    git 没有提供 stash unapply 的命令,但是可以通过取消该储藏的补丁达到同样的效果,如果要取消指定的储藏,带上名字即可,git stash show -p stash@{1} | git apply -R

  • 移除储藏 git stash drop

    stash apply 只是应用储藏,应用之后储藏的内容还在栈中,需要手动移除,drop 默认移除最近一次的储藏,git stash drop stash@{1} 移除上上次的储藏。

  • 应用并移除储藏 git stash pop

    stash pop 应用储藏的同时将其从栈中移除,等价于 stash apply + stash dropgit stash pop stash@{1} 可应用并移除上上次的储藏。

  • 移除所有储藏 git stash clear

撤销操作

撤销暂存

git reset HEAD <file>

撤销修改

git checkout -- <file>

将文件修改为上次提交的样子或者刚放入工作目录时的样子,直接拿版本快照中的文件来覆盖本地,因此很危险。

撤销提交

git reset [--soft/--mixed/--hard] HEAD^

恢复到当前版本状态,--soft 只撤销 commit,文件仍在暂存区中,--mixed 撤销 commit 和 add,文件恢复到工作目录,--hard 最粗暴,不仅撤销 commit 和 add,还将本地修改直接撤销了,即完全恢复到上次提交的状态,要慎用。默认参数是 --mixed,也是最常用的参数。

重新提交

git commit --amend

第二次提交会覆盖上一次的提交,如果第二次提交没有新的修改,就会使用第一次的快照,只覆盖提交信息。

数据恢复

有时候会丢失一些 commit 信息,比如强制删除了一个没 merge 的分支,则这个分支上的 commit 就丢失了,或者 hard-reset 到某个版本,则这个版本之后的 commit 就丢失了。这些丢失的数据是可以找回的,因为这些操作并不是真正的删除,只是从 git 仓库中移除了,数据还保留在 .git 目录下;git 会不定时的执行 git gc 命令,将不被引用且存在数月的垃圾删除,所以如果是误操作丢失了数据,不要慌,只要不是太久都可以找回的。

找回 commit 其实很简单,比方在这个 commit 上创建一个新分支就把它恢复过来了,关键是得知道我们弄丢的是哪个 commit,git log 只会显示当前的所有 commit,不包含已删除的,这时候可以用 git reflog,可以查看所有分支的所有操作记录,包括已删除的 commit 和 reset 的操作,reflog 只显示简单的 commit 信息,这时可以再执行下 git log -g 来输出 reflog 的正常日志,这样就能看到我们要恢复的是哪个 commit。

1
2
3
git reflog
git log -g
git branch recover-branch [commmit]

reflog 数据保存在 .git/logs 目录下,如果这个目录被删了,则 git reflog 就帮不了我们了,这时候可以使用 git fsck 命令,该命令会检查仓库的数据完整性和一致性,如果指定选项 --full,该命令显示所有未被其它对象引用的所有对象。通过 git fsck --full 也能找到被删除的 commit 信息。

有时候会误删 stash,比如想应用并移除储藏的时候,脑子一热将 git stash pop 打成了 git stash drop,这时储藏没应用,却已经从栈中移除了。但别慌,只是从栈中移除了,stash 的数据还是在的,只是无法在 git stash list 中列出来,即没有 stash 名字而已,但我们仍可以通过 commit 信息来恢复。其实在移除储藏的时候,从移除的结果就可以看到这条储藏的 commit 信息,如果马上意识到自己误操作,这时想恢复十分简单,直接使用这条 commit 信息进行恢复即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git stash list
stash@{0}: WIP on mission-guide: 7c7e6fb6 #feature update info
$ git stash drop
Dropped refs/stash@{0} (6703c50483b213a0bad3cff09ed804515ff93b78)
$ git stash list

$ git stash apply 6703c50
On branch feature/mission-guide
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: scripts/data/guide.lua

这是在刚移除 stash 的时候恢复,这时我们知道 commit 信息,如果隔了一段时间不知道 commit 信息了,也可以通过 git fsck 来列出所有 commit,但列出来的 commit 是无序的,只能通过 git show [commit] 来一条条查看 commit 的 diff,找到需要的 commit 之后再进行恢复,相对会麻烦挺多。

规范

commit 规范

标题:#[type] message

一般使用 #[type] 来表示这次提交内容的类型,比如 #feature 代码功能开发,#bugfix 代表 bug 修复,或者在一个博客系统中, #newpost 代表新文章,#newpage 代表新页面,#newdraft 代表新草稿,#amend 代表文章修改,#feature 代表其它修改。

默认 # 为 commit 信息的注释符号,通过配置 git config core.commentchar * 将其设置为 * 或其它符号。

单一职责,一次 commit 只处理一件事,方便查看和回退,多提交几次 commit 并没有不好的。

分支命名规范

  • master 主干分支,一般不在这个分支上直接改东西;
  • feature/xxx 功能开发分支;
  • bugfix/xxx bug 修复分支;
  • release/xxx 发行分支。