Git 日常操作

words: 4.2k    views:    time: 16min
git


官方文档:https://git-scm.com/book/zh/v2

主要整理了一下Git基本的工作机制,以及一些常用操作。如果想了解git的各种命令,推荐访问:https://learngitbranching.js.org/?locale=zh_CN

Git 特点

  • 记录快照,而非比较差异

早期版本控制系统,大部分以文件变更列表的方式存储信息,比如SVN,它们将存储的信息看作是一组基本文件和每个文件随时间逐步累积的差异,通常称作基于差异(delta-based)的版本控制。

而在Git中,每当提交更新或保存项目状态时,它基本上是对当前全部文件创建一个快照并保存这个快照的索引。考虑效率,如果文件没有修改,那么Git不再重新存储该文件,而是只保留一个链接指向之前存储的文件,因此Git对待数据更像是一个快照流。

  • 本地操作

在Git中,绝大部分操作都可以在本地完成,不需要来自网络上其它计算机的信息。所以大部分操作看起来瞬间完成,而且就算离线也没有影响,等到有网络连接时再推送服务就行。相对SVN等版本控制系统,这个解决了一个很大的痛点,因为在使用SVN时,离线情况下是无法进行代码提交的。

  • 分支切换

Git的分支非常轻量,它只是简单地指向某个提交纪录而已。所以创建再多的分支也不会造成存储上的开销,并且是按逻辑分解工作到不同的分支,这要比维护那些特别臃肿的分支简单得多。如果使用过SVN,应该能体会到拉分支的代价,拉分支就相当于拷贝,分支太多容易将磁盘撑爆。

  • 保证完整性

Git对所有的数据在存储前会计算校验和,然后以校验和来引用,所以不可能在Git不知情的情况下更改任何文件或目录内容。其计算校验和的机制是基于SHA-1散列,即hash,结果为40位由十六进制符组成的字符串。

  • 只添加数据

Git操作只会往Git数据库中添加数据,也就是说Git几乎不会执行任何可能导致文件不可恢复的操作,所以你提交快照到Git中,很难丢失数据。

Git 配置

git config是Git自带的一个工具,用来对Git进行配置,其配置有三个保存位置:

git config --system :针对所有用户及他们仓库的通用配置,对应配置文件为:/etc/gitconfig
git config --global :针对当前用户进行配置,对应配置文件为:~/.gitconfig
git config :针对当前仓库进行配置,对应配置文件为:.git/config

常用配置:

1
2
3
4
5
git config -l   # 列出配置项

git config --global credential.helper store # 记住密码
git config --global user.name "shanhm1991" # 用户名
git config --global user.email "shanhm1991@163.com" # 邮箱

另外对于当前仓库的配置,经常也会修改或增加其远程仓库地址,这些配置对应下面的git fetch和git pull操作

.git/config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "mac"]
url = http://localhost:8083/common/spring-fom.git
fetch = +refs/heads/*:refs/remotes/mac/*
[remote "origin"]
url = https://github.com/shanhm1991/spring-fom.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

当然一般我们不需要直接编辑文件,可以通过可视化工具来管理,有小乌龟、IntelliJ IDEA、Sourcetree等

比如在IntelliJ IDEA中管理远程仓库:Git -> Manage Remotes

然后在推送时便可以选择目标仓库和分支,但是反过来fetch拉取是不能选择远程和分支的,然后默认推送也是推到当时检出的远程分支上,它们之间有跟踪关系,除非通过命令git branch -u <remote-branch> <local-branch>强行修改远程跟踪分支的关系,但通常并不需要这样

Git 提交/撤销

Git中有三个区域,对应能够更容易理解Git的流程机制:

  • Working Tree: 工作区域
  • Index/Stage: 暂存区域,git add xx,可以将xx从工作区域添加到暂存区域
  • Repository: 提交历史,git commit提交后的结果

下面简述一下文件提交Repository的流程:
1.刚开始working tree、index、repository(HEAD)里面的內容都是一致的
2.当git管理的文件夹中内容出现变更时,working tree就会与index和repository(HEAD)不一致,而git知道是哪些文件被改动过(Tracked File),于是将文件状态设置为modified(Unstaged files)
3.当执行git add后,会将改变的文件內容加入index中(Staged files),此时working tree与index是一致的,但他们与repository(HEAD)不一致
4.接着执行git commit后,git索引中变动的文件提交至Repository中,建立新的commit节点(HEAD),此时working tree、index与repository(HEAD)又重新保持一致

  • 示例

可以在本地非常方便地初始化一个git仓库,新建一个空目录,执行命令:git init

此时仓库是空的,可以查看仓库状态,通过git status,可以查看各个区域之间的差异,友好的git还会提示如何提交或撤销这些差异

1
2
3
4
5
6
shanhuiming@MacBook-Pro test$ git status
On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)

然后我们可以在添加两个文件后,再次查看状态,提示有2个未被跟踪的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
shanhuiming@MacBook-Pro test$ touch a.txt
shanhuiming@MacBook-Pro test$ touch b.txt
shanhuiming@MacBook-Pro test$ git status
On branch main

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)
a.txt
b.txt

nothing added to commit but untracked files present (use "git add" to track)

git add添加a.txt文件到暂存区,再次查看状态,提示有1个文件未跟踪,1个文件未提交

1
2
3
4
5
6
7
8
9
10
11
12
shanhuiming@MacBook-Air test1$ git status
On branch main

No commits yet

Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: a.txt

Untracked files:
(use "git add <file>..." to include in what will be committed)
b.txt

然后便可以提交a.txt了,再次查看状态,提示还有1个文件未跟踪

1
2
3
4
5
6
7
8
shanhuiming@MacBook-Air test1$ git commit -m "first commit"
shanhuiming@MacBook-Air test1$ git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
b.txt

nothing added to commit but untracked files present (use "git add" to track)

继续修改下a.txt文件,然后再查看状态,提示有1个文件未跟踪,还有一个已跟踪的文件发生了修改,但是没有添加到缓存区

1
2
3
4
5
6
7
8
9
10
11
12
13
shanhuiming@MacBook-Air test1$ vim a.txt 
shanhuiming@MacBook-Air test1$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: a.txt

Untracked files:
(use "git add <file>..." to include in what will be committed)
b.txt

no changes added to commit (use "git add" and/or "git commit -a")
  • git reset/revert

相对提交修改,git reset用来撤销,其有三个模式,分别对应上面介绍的三个区域:

1
2
3
soft: 撤销修改,但是保留工作区域和暂存区域的改动;
mixed:撤销修改(默认),并擦除暂存区域的改动,但是保留工作区域的改动;
hard: 撤销修改,并擦除工作区域和暂存区域的改动

当发现提交的内容有误,并希望撤销时,可以通过命令git reset HEAD^(^相对引用,表示指向上一次提交记录)。如果提交记录已经推送远程服务,并希望也撤销远程的记录,则需要使用git push -f来强制推送。但是要慎用,因为可能本地当前版本已经落后于远程仓库,强制推送的话可能会删除别人的推送记录。而且如果别人已经拉取了你的提交,即便你现在强制回退了,很可能在后面又被队友给推送回来。

所以在团队多人合作时,最好使用git revert来撤销修改。它是在当前版本上撤销掉之前的改动,但是会向前产生一个新的版本号,这样就不需要版本回退也不会影响队友了。

还有一种git reset场景,在打包环境上,为了防止环境上的文件被人有意或无意的修改(这些改动肯定是不希望打到包里的),我们希望拉取最新的代码并强制忽略掉本地的修改

1
2
git fetch --all
git reset --hard origin/master

Git 分支

上面已经介绍过git分支的特点,就是新建一个指向提交记录的指针,所以以下命令的效果就相当于

1
2
3
## git branch bugFix
## git checkout bugFix
git checkout -b bugFix

  • git merge/rebase

git merge用于合并指定分支到当前分支,使用merge合并两个分支时会产生一个特殊的提交记录,它有两个父节点。如果要合并的记录继承自当前节点,则git什么都不用做,只需移动到要合并的记录即可

1
2
3
4
5
git branch -b bugFix
git commit
git checkout master
git commit
git merge bugFix

git rebase合并的办法是取出一系列的提交记录,然后在另外一个分支逐个的放进去。Rebase的优势在于可以创造更线性的提交历史,让代码库的提交历史变得更清晰。rebase是将后面分支(默认当前分支)上的记录追加到前面分支中,同样如果要合并的分支记录继承自当前节点,也只要移动下指针。

比如下面如果将bugFix分支rebase到新的master上

1
2
3
4
5
6
7
git branch -b bugFix
git commit
git checkout master
git commit

git checkout bugFix
git rebase master

但此时是处于分支bugFix上,master分支还停留在之前的记录上,可以如下

1
2
git checkout master
git rebase bugFix

Git 标签

分支很容易被人改变,当有新的提交时,它就会移动。如果希望永久的指向某个提交记录,那么可以使用tag。

HEAD是一个对当前检出记录的符号引用,也就是指向正在其基础上进行工作的提交记录,默认是指向当前分支上最近一次提交记录,即指向分支名,但是也可以使其指向具体的提交记录,称为分离HEAD,比如:git checkout c4

Git 远程

  • git clone

git clone会在本地建一个分支,一般是origin/master(远程仓库名/分支名),记为远程分支,其目的是为了反映远程仓库的状态。对应的还会创建一个远程跟踪分支,用来记录本地的修改,这样不会影响远程分支。当我们通过远程分支来检出一个本地分支时,它们就自动建立的跟踪关系。比如:

1
git checkout -b master origin/master

  • git fetch

git fetch会从远程仓库下载本地仓库中缺失的提交记录,并更新远程分支,使其指向最新的提交记录。但它并不会修改本地仓库的状态,也不会修改你磁盘上的文件,只是将所需的所有数据都下载了下来。不带参数则按照跟踪关系进行拉取,也可以如下指定参数。

1
git push <repository> <remote-branch>:<local-branch>
  • git pull

git pull相当于先git fetch更新了远程分支,然后默认进行git merger合并远程分支到本地分支上,当然也可以git pull —rebase 指定合并方式

  • git push

git push负责将本地commit记录推送到远程仓库,并在远程仓库上合并提交记录。不带参数则按照跟踪关系进行推送,当然也可以如下指定参数,很多时候,我们可能有多个远程仓库,比如我们拉取A仓库的分支进行了一些修改,然后也想把这些修改推送到B仓库中去,这时就需要指定了,因为默认只会推送到A仓库中被跟踪的远程分支上。

1
git push <repository> <local-branch>:<remote-branch>

这里记一个github push 遇到的问题:Connection was reset, errno 10054,可以如下解决

1
2
git config --global http.sslVerify "false"
git config --global --unset http.proxy

FAQ

  • branch/tag日常命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
git branch -D v1.2.0            #删除本地分支
git push origin --delete v1.2.0 #删除远程分支
git push origin v1.2.0 #推送指定分支到远程
git push --all origin #推送所有分支到远程

git tag -l #查看本地tag
git show v1.2.0 #查看tag信息
git tag v1.2.0 #创建本地tag
git tag -d v1.2.0 #删除本地tag

git ls-remote --tags origin #查看远程tag
git push origin :v1.2.0 #删除远程tag
git push origin v1.2.0 #推送指定tag到远程
git push origin --tags #推送所有tag到远程
  • 修改commit msg
1
2
3
4
5
6
7
8
9
## 修改最近一次commit log
git commit --amend

## 修改历史log
git log --oneline #先定位到对应的提交记录 xxx
git rebase -i xxx #将对应记录前面的pick改为edit,退出
git commit --amend #修改提交日志
git rebase --continue
git push -f #如果已经push过了,则需要-f强制推送
  • 只拉取子目录

如果希望只拉取仓库中的一个子目录,可以通过如下方式,这点不如SVN方便

1
2
3
4
5
git clone –n <repo> <local-directory-name>
cd <local-directory-name>
git config core.sparsecheckout true
echo some/sub-folder/you/want >> .git/info/sparse-checkout
git checkout <branch-name>
  • github拉取推送慢问题

由于github是外网,在国内访问域名会被DNS限制,会异常的慢,所以可以修改本地host文件,查询ip地址:https://www.ipaddress.com/ip-lookup

主要就下面两个域名,经过长时间的实践,它们的ip一般不会超过下面这些选项,如果访问还是慢,可以换个ip再试一下

/etc/hosts
1
2
3
4
5
6
7
199.232.69.194  github.global.ssl.fastly.net
#140.82.112.3 github.com
#140.82.112.4 github.com
#140.82.113.3 github.com
#140.82.113.4 github.com
140.82.114.3 github.com
#140.82.114.4 github.com
  • 工作场景

在实际工作中,可能遇到出差需要两个异地团队合作开发的场景,而且由于保密协议,是两个内部局域网。这种场景使用使用git很好处理,可以保证所有的提交记录都能被跟踪并且不丢失。具体先在两地分别准备一个gitlab环境,这里使用docker部署(启动后修改下config/gitlab.rb中的external_url)

docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: "3"

services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
ports:
- "8082:443"
- "8083:80"
- "8084:22"
environment:
GITLAB_SKIP_UNMIGRATED_DATA_CHECK: "true"
volumes:
- ./config:/etc/gitlab
- ./logs:/var/log/gitlab
- ./data:/var/opt/gitlab
- /etc/localtime:/etc/localtime
privileged: true

假设是两地是南京和北京,那么给仓库创建两个分支,然后在南京开发的同学使用分支:branch-NJ,在北京开发的同学使用分支:branch-BJ
如果A同学从南京到了北京,那么先将本地的branch-NJ分支推送到北京仓库,然后切换本地当前分支到branch-BJ,并拉取更新,最后再将分支branch-NJ合并到branch-BJ上,这样后面就在branch-BJ分支上进行开发
如果A同学又从北京回到了南京,就反过来先将本地的branch-BJ分支推送到南京仓库,然后切换本地当前分支到branch-NJ,并拉取更新,最后再将分支branch-BJ合并到branch-NJ上,然后又回到branch-NJ分支上开发

为了避免混乱以及合并麻烦,最好再加个约束:在北京的开发禁止向branch-NJ提交,同样在南京的开发禁止向branch-BJ提交,这样避免了再第一步推送仓库时出现冲突。

其实在git中比较推荐的做法是关闭master分支的提交操作,然后每个开发人员拉一个自己的开发分支,开发在自己分支上自测通过后再申请向mater分支合并,最后由代码评审人来进行审批和代码合并。只是在实际的项目实施中,大多时候我们会省掉这些步骤,毕竟有开发进度要求,只要效果能达到并最终能管理好就行。


参考:

  1. https://www.jianshu.com/p/c2ec5f06cf1a