git

本文总阅读量

1.开发流程

每个公司都有自己的一套git流程, 如果没有则推荐使用git-flow备完清单

2.log

优雅的输出git日志

1
git log --decorate --oneline --graph

3.tag

项目每次发一个版本可以打一个tag, 利于代码版本管理, tag 的作用是对某个提交点打上标签,发布版本后打 tag,便于以后回滚特定版本,而不需要 revert。

  • $ git tag -a '<tag_name>' -m '<description>' 提交一个tag
  • $ git tag -n1 显示一个 tag
  • $ git push origin --tags 上传tag
  • $ git pull origin --tags 下载合并tag
  • $ git push origin :refs/tags/<tag_name> 删除远程 tags

4.第一次提交使用git push –set-upsteam

在第一次提交时, 建议使用–set-upsteam, 后续在push时就可以直接使用git push, 比如有个项目名叫demo:

  • 使用push的情况:
    1
    2
    3
    4
    # 第一次push
    git push demo
    # 后续push
    git push demo
  • 使用--set-upsteam的情况:
    1
    2
    3
    4
    # 第一次push
    git push demo --set-upsteam
    # 后续push
    git push
  • 简写:
    1
    2
    3
    4
    # 第一次push
    git push -u demo
    # 后续push
    git push

    5.rebase/master

    5.1rebase与master的区别

    git mergegit rebase 都可以整合两个分支的内容,最终结果没有任何区别,但是变基使得提交历史更加整洁。

假设先在dev分支提交一次, 然后在master提交一次, 然后在dev分支执行rabese/merge操作,之后有两点不同:

  • 提交点顺序:
    • git merge后,提交点的顺序都和提交的时间顺序相同,即master的提交在dev之后。
    • git rebase后,顺序变成被rebase的分支(master)所有提交都在前面,进行rebase的分支(dev)提交都在被rebase的分支之后,在同一分支上的提交点仍按时间顺序排列。
  • 分支变化
    devrebase master后,由原来的两个分岔的分支,变成重叠的分支,看起来dev是从最新的master上拉出的分支。

    5.2何时使用rebase

    假设现在有dev分支, 由于有功能开发, 基于dev分支创建了feature分支。
    如果dev分支要合并feature, 则都在dev分支调用git merge feature, 反过来feature要更新dev的内容时,使用git rebase dev
    假设场景:从 dev 拉出分支 feature-a。那么当 dev 要合并 feature-a 的内容时,使用 git merge feature-a;反过来当 feature-a 要更新 dev 的内容时,使用 git rebase dev。

    5.3git merge –no-ff

    在使用merge时, 如果分支没有冲突, 则会把被merge分支的指针指到merge的分支。而使用git merge --no-ff则是在新的分支上进行合并, 合并时会保留当前分支的信息。

    5.4git pull –rebase

    理论上, 任何时候执行git pull时, 都应该变为git pull --rebase.
    当某个分支可能与其远程分支发生分离, 在使用git pull后就会变成本地分支和远程分支合并, 就会变成类似git merge的情况, 而我们拉取分支只是要同步线上的功能, 通过5.2可以知道, 应该使用类似git merge来更新功能。

6.commit规范

每个项目的都应该有个一统一的commit规范, 不同公司的提交规范会不一致, 我个人常用的规范是:

1
git commit -m"<issue_id>:<file change>:<operating>:<info>"
  • issue_id: 代表一个issue的id, 在准备写功能或者修复一个bug时,都应该先提一个issue,然后在针对这个issue提交代码
  • file change: 代表文件的变化, 如增加, 删除, 修改;也有人使用+,-,*来分别代表增加, 删除, 修改
  • operating: 代表本次代码变化, 具体有如下几种
    • feat:新功能
    • fix:修复bug
    • doc:文档改变
    • style:代码格式改变
    • refactor:某个已有功能重构
    • perf:性能优化
    • test:增加测试
    • build:改变了build工具 如 grunt换成了 npm
    • revert:撤销上一次的commit
  • info: 简要的说明本次提交信息

更加规范的参考示例见: https://www.conventionalcommits.org/en/v1.0.0/

7.cherry-pick

git cherry-pick即应用某些已有提交所引入的更改, 通常是将某个分支的提交应用到另外一个分支, 常用于紧急修复bug或者从放弃的分支中挑出有用的commit。他的优点非常明显,只合并分支中的一个提交,而不是所有提交,如下, 可以单独挑出master分支的c提交合并到new_feature中:

1
2
[master] a -> b -> c -> d
[new_feature] e -> f -> g

gir cherry-pick常见使用命令如下:

1
2
3
4
5
6
7
8
9
10
11
# 应用某个commit
git cherry-pick <commit-id>
# 保留提交人信息, 不带x则使用cherrt-pick的执行者信息
# 如果是-e则是可以重新写上新的提交信息
# 如果是-n 则只更新工作区和暂存区, 不提交(相当于git add)
git cherry-pick -x <commit-id>
# 应用一段连续的commit,注意是左开右闭
git cherry-pick <start-commit-id>…<end-commit-id>
# 应用一段连续的commit, 注意是左闭右闭
git cherry-pick <start-commit-id>^…<end-commit-id>
#

8.git pull 强制覆盖本地

一般我们需要同步线上分支时,需要重新强制拉取线上分支覆盖到本地

1
2
3
4
5
6
7
8
# 从远程拉取所有内容
git fetch --all

# reset 本地代码
git reset --hard origin/master

# 重启拉取对齐
git pull

9.回退到某个分支

很多时候我们经常需要回退到某个分支,这时就需要git reset,为了保险起见,需要确保在执行git reset前,能知道当前分支的commit或者查看reflog的commit,确保有后悔药可以吃。

git log是查看分支的变根情况, 会根远程分支同步, 而git reflog的功能是查看本地操作记录,可以看到本地的commit, merge, rebase等操作记录
git reset简单粗暴,就是把索引指向某个commit,并做对应的操作,使用方法

1
git reset <commit>

此外还可以指定可选参数, 具体参数定义如下:

1
2
3
4
5
--soft 回退后分支修改的代码被保留并标记为add的状态(git status 是绿色的状态)
--mixed 重置索引,但不重置工作树,更改后的文件标记为未提交(add)的状态。默认操作。
--hard 重置索引和工作树,并且a分支修改的所有文件和中间的提交,没提交的代码都被丢弃了。
--merge --hard类似,只不过如果在执行reset命令之前你有改动一些文件并且未提交,merge会保留你的这些修改,hard则不会。【注:如果你的这些修改add过或commit过,merge和hard都将删除你的提交】
--keep --hard类似,执行reset之前改动文件如果是a分支修改了的,会提示你修改了相同的文件,不能合并。如果不是a分支修改的文件,会移除缓存区。git status还是可以看到保持了这些修改。

如果要reset到上个提交,可以直接写

1
git reset --hard HEAD^

10.移除git的某些commit

执行该命令,会撤销对应的提交,然后再重新提交一个新的提交(该操作可以撤销,如果使用reset则会直接回退到某个commit,除非有reflog记录或记得某个commit id,不然无法撤销).
再执行git revert前,必须执行

1
2
git diff <commit1> HEAD
git diff <commit1>..<commit2>

查看要撤销的代码是否正确,再使用 git revert 可以撤销指定的提交,要撤销一串提交可以用 .. 语法。 注意这是一个前开后闭区间,即不包括 commit1,但包括 commit2。

1
2
3
git revent <commit>
# 可以指定一个前开后闭的区间被移除
git revent <commit>...<commit>

11.另外一种移除git的某些commit方法

假设有commig v1,v2,v3,v4,v5共5个commit, 下面的操作会移除v3这个commit

1
2
3
4
5
6
# 从 v2 切分支出来
git checkout -b fixing v2
# 合并 v4,保持代码不变
git merge -s ours v4
# 合并 v5(也就是当前罪行的分支)
git merge master

12.git将一个分支完全覆盖另外一个分支

如下代码,把线上的master分支覆盖本地的develop分支

1
2
3
4
5
6
# 切换到develop
git checkout develop
# 指向origin/master
git reset --hard origin/master
# 强拉
git push -f

13.git hooks文件指定

git hook文件一般为bash.sh文件, 只要把文件放置于项目的.git/hooks下即可让该项目的git命令享用hook套餐

如果觉得每个项目都需要配置一次很麻烦, 则可以创建一个文件夹存放hooks文件,并调用全局命令git config --global core.hookspath xxx指定.

如果自己的电脑上有自己的项目和公司的项目, 想用两套hook文件, 也还是有办法的:

  • 1.首先自己的项目和公司的项目是在不同的文件夹里面, 比如我自己的项目在/home/so1n/github, 公司的项目在/home/so1n/xxx_gitlab下面.
  • 2.修改~/.gitconfig
    1
    2
    [includeIf "gitdir:~/xxx_gitlab/"]
    path = .gitconfig-xxx
  • 3.创建一个与上面path一样的文件~/.gitconfig-xxx,并写入如下配置, 指定该项目需要的hook文件
    1
    2
    [core]
    hookspath = ~/.git-hooks

13.1git禁止在master分支push和commit

本小节来源

作为管理者,在远端将master分支设为保护分支,可以从根源上杜绝直接推送到master的问题, 每个平台的选项有所不同.
作为开发者,在本地的git hook中加配置可以做到在commit和push操作时做对应的检查.

13.2禁止在master分支上Commit

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh

# 指定受保护的分支
protected_branch='master'

# 获取当前的分支
current_branch=$(git rev-parse --symbolic --abbrev-ref HEAD)

# 判断当前分支是否为受保护的分支, 如果是则打印警告, 并且退出
if [ "$protected_branch" == "$current_branch" ]; then
echo ".git/hooks: Do not commit to $current_branch branch"
exit 1
fi

13.3在master分支上Commit时提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh

protected_branch='master'
current_branch=$(git rev-parse --symbolic --abbrev-ref HEAD)

if [ "$protected_branch" == "$current_branch" ]; then
# 打印提示 并等待操作...
read -p "You're about to commit to master, is that what you intended? [y|n] " -n 1 -r </dev/tty
echo
# 如果是y则通过, 否则退出
if echo "$REPLY" | grep -E '^[Yy]$' >/dev/null; then
exit 0
fi
exit 1
fi

13.4禁止推送到master分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh

protected_branch='master'
remote_branch_prefix="refs/heads/"
protected_remote_branch=$remote_branch_prefix$protected_branch

while read local_ref local_sha remote_ref remote_sha
do
if [ "$protected_remote_branch" == "$remote_ref" ]; then
echo ".git/hooks: Do not commit to $protected_branch branch"
exit 1
fi
done

exit 0

13.5推送时如果commit消息包含WIP则禁止推送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

protected_branch='master'
remote_branch_prefix="refs/heads/"
protected_remote_branch=$remote_branch_prefix$protected_branch

while read local_ref local_sha remote_ref remote_sha
do
if [ "$protected_remote_branch" == "$remote_ref" ]; then
read -p "You're about to push master, is that what you intended? [y|n] " -n 1 -r < /dev/tty
echo
if echo $REPLY | grep -E '^[Yy]$' > /dev/null
then
exit 0 # push will execute
fi
exit 1 # push will not execute
fi
done

exit 0
查看评论