Git操作实践

速览

仓库

# 在当前目录新建一个Git代码库
$ git init

# 新建一个目录,将其初始化为Git代码库
$ git init [project-name]

# 下载一个项目和它的整个代码历史
$ git clone [url]

配置

# 显示当前的Git配置
$ git config --list

# 编辑Git配置文件
$ git config -e [--global]

# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"

增加/删除文件

# 添加指定文件到暂存区
$ git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
$ git add [dir]

# 添加当前目录的所有文件到暂存区
$ git add .

# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]

# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

代码提交

# 提交暂存区到仓库区
$ git commit -m [message]

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

分支

# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

标签

# 列出所有tag
$ git tag

# 新建一个tag在当前commit
$ git tag [tag]

# 新建一个tag在指定commit
$ git tag [tag] [commit]

# 删除本地tag
$ git tag -d [tag]

# 删除远程tag
$ git push origin :refs/tags/[tagName]

# 查看tag信息
$ git show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]

查看信息

# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

远程同步

# 下载远程仓库的所有变动
$ git fetch [remote]

# 显示所有远程仓库
$ git remote -v

# 显示某个远程仓库的信息
$ git remote show [remote]

# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]

# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]

# 上传本地指定分支到远程仓库
$ git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force

# 推送所有分支到远程仓库
$ git push [remote] --all

撤销

# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

其他

# 生成一个可供发布的压缩包
$ git archive

设置Git用户名

在 Git 中设置自己的用户名是非常必要的一个步骤,因为在提交代码的时候,会记录下提交者的信息,这样就可以很清晰的看到某时某刻是谁推送了代码。

为一个仓库设置 Git 用户名

  1. 打开 Git Bash。

  2. 将当前工作目录更改为您想要在其中配置与 Git 提交关联的名称的本地仓库。

  3. 设置 Git 用户名以及联系方式:

    $ git config user.name "Mona Lisa"
    $ git config user.email "email@example.com"
    
  4. 确认您正确设置了 Git 用户名以及联系方式:

    $ git config user.name
    > Mona Lisa
    $ git config user.email
    > email@example.com
    

为计算机上的每个仓库设置 Git 用户名

  1. 打开 Git Bash。

  2. 设置 Git 用户名:

    $ git config --global user.name "Mona Lisa"
    $ git config --global user.email "email@example.com"
    
  3. 确认您正确设置了 Git 用户名:

    $ git config --global user.name
    >Mona Lisa
    $ git config --global user.email
    > email@example.com
    

获取代码 (Fetch、Pull)

如果我们有一个远程 Git 分支,例如 Github 上的一个 master 分支,那么远程分支可能有本地分支没有的提交!也许是另一个分支合并了,也许是你的同事推送了一个 BUG 的修复,等等。

如果想要同步这些最新的提交,可以使用两种方式,一种是 git fetch,另一种是 git pull

git fetch 会将远程数据下载至本地,不过也仅仅是下载至本地而已。
git pull 会将远程数据下载至本地,除此之外,它还将下载的数据与本地代码合并。

git-merge-dev.gif

通常的,开发人员会使用 git pull 来获取远程 Git 仓库上的最新提交,以达到更新本地代码的目的。其实,git pull 命令行就相当于 git fetch + git merge origin/master。一般来说,更新代码,使用 git pull 就好了。

合并代码 (Merge)

合并代码分为两种方式:Fast-forward (--ff)、No-fast-foward (--no-ff)。在多分支的开发环境下,合并代码 (git merge) 也是较为常用的一个命令。

Fast-forward (--ff)

假设 master 主干要合并 dev 分支上的更改,而 master 相对于 dev 来说没有任何变动,那么 git 会使用 Fast-forward 方式进行一次快速合并。该方式是 git 的“懒”操作,这样不需要 master 主干生成任何提交记录,仅需复用 dev 分支的提交记录即可。

$ git merge dev
Updating 7173edd..7d959a2
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)
$ git push

git-merge-dev.gif

No-fast-foward (--no-ff)

假设 master 、dev 两个分支的开发是并行开展的,该情况下进行代码的合并,git 就会主动提示开发人员进行必要的 commit 操作,此时的 git merge dev 操作就相当于 git merge --no-ff dev。该情况下的提交记录会与上边的 Fast-forward (--ff) 有差异, 因为合并之后,会在 master 与 dev 顶端生成一个新的提交记录。

$ git merge dev
Merge branch 'dev'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

git-merge-no-ff-dev.gif

合并冲突 (Merge Conflicts)

合并代码发生冲突是很常见的场景,例如,在某两个分支中,对相同文件的相同位置做了不同的修改,该情况下合并代码,git 是无法自主决定代码的合并的,因为无法确定哪个分支的更改才是最有效的。

假设有A、B程序员以及一个 README.md 文件
A 程序员:编辑 master 主干 README.md 里的第一行文字,更改为:Hey!
B 程序员:编辑 dev 分支 README.md 里的第一行文字,更改为:Hello!
此时合并代码将发生冲突:

$ git merge dev
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

该情况下需要人为的解决冲突,首先编辑 README.md 文件:

vi README.md

可以发现 README.md 的内容会有所更改,因为 git 自动填充了两个分支的不同之处,以方便开发人员进行比较,如下所示:

<<<<<<< HEAD
Hey!
=======
Hello!
>>>>>>> dev

此时只需决定需要保留的内容即可,如保留 Hey!而非 Hello!

Hey!

最后重新提交代码即可,本次合并的冲突就解决啦。

git-merge-comflicts.gif

变基 (Rebase)

在之前的操作中可以了解到,git merge 可将更改从一个分支应用于另一个分支。而 git 还有一种类似的操作 git rebase,也可实现将更改从一个分支添加到另一个分支。

git-rebase.gif

如上图所示, git rebase 不同于 git merge 之处,在于 rebase 会重写提交树的结构,以使 master 分支成为当前分支的一部分。

个人理解:使用 rebase ,可以使得当前开发的分支是基于 master 或者其他重要分支进行延伸,而如果当前分支的代码要被 master 或其它分支合并时,可以顺利的完成一次 (Fast-forward) 合并!

恢复 (Reset、Revert)

如果在开发过程中对代码做了不理想的更改,比如存在 BUG、功能调整等,那么在推送代码之前,我们还是可以对这些文件内容或者状态进行回滚、恢复、重新编辑等。

Soft reset

假设在本地的 dev 分支分别提交了 style.css 和 index.js 文件的更改,生成了两条记录 change background-colorfixed index logic,如下图所示:

git-reset-test

查看当前 git 的状态:

$ git status
On branch dev
Your branch is ahead of 'origin/dev' by 2 commits.
  (use "git push" to publish your local commits)

如果想要撤销这两次提交记录,但是需要保留当前更改的内容,那么可以通过执行 git reset --soft HEAD~2 来实现 (HEAD 指针后退两步 )。此时观察 git 状态,会显示 index.js、style.css 都处于 modified 状态,等待着新的 commit (Changes to be committed)。

$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   index.js
        modified:   style.css

相应的,git 的历史记录也有所改变:

git-reset-test-2

Mixed reset

reset --soft 效果一致,唯一不同的是 reset --mixed 会将本地暂存区的缓存删掉。

$ git reset --mixed HEAD~2
Unstaged changes after reset:
M       index.js
M       style.css

所以如果要重新 commit 文件,需要先对文件进行 git add 操作。

Hard reset

reset --hard 命令相对更加的严格,使用 hard 不仅会删除本地暂存区的缓存,还会将实际编辑的文件也一并恢复原样。

$ git reset --hard HEAD~2
HEAD is now at 431f5f6 fixed something
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

观察文件可以看到,文件恢复成了更改前的模样,就好像什么都没发生过。

git-reset-test-3

revert

git 提供的另一个命令 revert 也可以实现代码的回退。与 reset 命令不一样,revert 不会删除暂存区数据、也不会回退 HEAD 指针,相反,revert 会要求新增对应的 commit ,来加以说明为什么要对文件进行恢复或者回退操作。

比如需要回滚以上 index.js 、style.css 两个文件的代码并且记录下原因,那么可以使用 git revert HEAD~2.. 来实现。

查看 git 的历史记录:

git-reset-test-3

git revert HEAD #撤销最新提交
git revert HEAD^ #撤销倒数第2次提交
git revert HEAD~2 #撤销倒数第3次提交
git revert HEAD~2.. #撤销最新两次提交
git revert commit-id #撤销指定的 commit-id

参考资料

  • GitHub, 在 Git 中设置用户名, GitHub help. 📄
  • 阮一峰, 常用 Git 命令清单, 阮一峰的网络日志, 2015. 📄
  • Rob Di Marco, Peter Mortensen, “When do you use Git rebase instead of Git merge?”, Stack Overflow, 2019. 📄
  • Lydia Hallie, 🌳🚀 CS Visualized: Useful Git Commands, DEV, 2020. 📄
  • GitHowTo. 📄
· last updated: ·