git using tips

主要介绍了常见的git分支的组织方式,git使用时候常用到的命令行的操作方式,自己平时使用git的时候遇到的一些常见的情况和处理方式,以及自己使用中的感想。

让git分支有章可循

具体的代码组织方式是因人而异的,但是总是有一些“最佳实践”或者是“过来人的经验”。实际上大家总是因为偷懒,不按照这些经验来操作,都喜欢“简单粗暴”的方式,然而这些所谓的“最佳实践”,才是真正的通往所谓的专业人员的道路。自己应该多想想这些最佳实践的由来,它们为什么是这样规定,下面是一种比较常见的组织方式:

|--- master
|--- hotfix
|--- develop
|--- feature
|--- feature/a
|--- feature/b
|--- feature/c
...

实际的组织应该是和公司的生产开发流程有关的,以上这个算个解基本例子,主要的分支包括master,developer,以及feature分支。master是线上的环境,develop对应的是stage的环境,feature是具体的功能开发时候相关的分支。每次开发的时候,开发人员从feature分支上fork出新的分支,之后开发新功能点,merge到feature分支,各种测试调整之后,merge到develop分支,之后develop分支定期merge上线到master分支。有紧急的bug可以从master分支直接fork出hotfix修复。feature分支不可以越过develop来merge到develop。

这样做最明显的一个好处就是冲突发生的问题,因为master的来源仅仅是develop,所以也不会出现很多的冲突问题,所以不会频繁的对master分支进行更新,仅仅是develop测试好,已经确定版本升级的时候才更新。

自己的项目可以省略develop分支,但是feature分支必须有,这样才能有章可循,每个独立的小功能必须有独立的commit以及详细的描述。虽然说起来都没有什么难的,但是实际总不容易做到。

总之不论是一个人的项目还是多个人的项目,最核心的一个原则就是不要在master上直接就commit,而是要fork出来之后再进行修改(切记切记 千万不要直接pull main。就算是一个人的项目,也千万不要直接git pull,而是fetch main之后,再checkout到上面,然后将自己的branch merge到main上面),最后将自己的修改merger到本地的master上然后push master,这样比较不容易出现conflicts的问题。或者自己的代码要早在不同的platform上运行,这个时候每个platform上的代码就是单独的分支,每次update之前先从master/main上进行check然后开发,开发好之后再merge到master上。

用于“生存”的相关命令

操作每一个命令的时候,要清楚自己操作的是本地仓库中的分支,还是远程仓库中的分支。本地分支可以有多个,远程分支也有多个,其中部分是相互对应的。

信息查看

git config -l 可以展示本地git仓库的配置信息

git remote -v 可以查看当前配置文件中已经配置的远程分支有哪些,有一些远程分支设置了别名,比如最常见的origin。

git branch -l 可以查看本地所有的分支,包括本地有的,以及从远程仓库上同步下来的。

具体操作

对应于第一部分中介绍的分支结构大概的操作流程是,先从远程仓库把代码clone下来,然后从feature分支上创建一个自己的分支(特别关键的一步)然后修改完之后,提一个commit,之后手动到git仓库中查看提一个pr(pull request),希望将修改merge feature分支中,之后自己以及其他的一起合作的开发者检查完pr中修改代码,发现修改的地方没有冗余,测试通过,没有问题之后,确认merge到feature分支中。好的pr习惯可以记录整个程序开发的流程,确保每个功能都是实实在在的,每次升级都有记录,不会造成冗余以及整个代码的“失控”。以上操作涉及到的git命令:

git branch <new branch name> 创建新的分支,具体分支的删除,以及更细节的查看可以通过branch的子命令来进行

git checkout <new branch name> 切换到新创建的某个分支

git status 查看有哪些文件被修改

git add <文件名> 把希望被git记录下来的修改文件加入其中,一般为了方便直接git add .这样的一些不好的地方就是一些二进制文件可以也被记录进来,可以采用.gitignore的方式将类似二进制文件的不需要记录的内容放在特定的目录下。

git commit -m "commit message" 创建commit并添加修改信息。

git commit --amend 这样会将当前的修改append到上次的commit中,同时还可以对之前的commit信息进行多次的修改。注意区别 -f--force-with-lease 两种方式。一般amend之后再使用-force-with-lease是比较安全的操作,具体可以参考这个

git push <远程项目的地址> <本地分支名>:<远程分支名>具体在push的时候可以使用对应的一些参数,这里的主机名可以使用最原始的,也可以使用别名。比如这个操作git push -f http://10.10.102.101:8000/gitlab/jiaweizhou/k8apitransform.git wangzhe:wangzhe 强制用本地的wangzhe分支覆盖掉了远程主机上某个具体项目中的wangzhe分支。

之后都是按照这个过程操作,一个功能开发完成之后,在本地就可以把对应的分支删除掉了。

查看区别

除了通常个git diff命令之外,还可以用这个有界面的工具

git difftool --tool=tkdiff

关于gitlab的 credential

有时候gitlab会使用自动的credential tool,比如使用了新的token之后,需要重置一下相关的credential的信息,这个时候可以参考如下的命令,具体可以参考这里.

git config --system --unset credential.helper

进阶使用

这一部分是不断更新的,有遇到新的情况就记录一下

fork了主库的代码之后如何更新

git remote -v

以上命令查看当前环境中的远程仓库的key:value信息,通常fork下来的库其中的信息类似于这样:

origin	https://github.com/<local repo> (fetch)
origin https://github.com/<local repo> (push)

此时如果想同时更新远程仓库的代码的话,需要将远程仓库的repo也添加到本地,通常用upstream关键字来标记:

git remote add upstream <addr of main repo>

添加完成之后再执行

git branch -r

就可以列出自己的仓库中以及远程的仓库中所有的分支信息,之后就可以根据需要将本地的分支check到对应的分支之后进行git pull操作拉取对应仓库的代码了。

冲突的处理

使用git merge处理冲突

最开始的时候遇到冲突也调整不好,最后操作就是重新来过,冲突处理相关的两个命令就是git mergegit rebase

比如在提交之前,别人已经将feature分支进行了更新,自己在提交之间,之后会落后一些commit,此时可能别人修改的内容你在自己的分支上也进行了修改,遵循先来后到的原则,既然别人已经先提交了,你就得在别人的版本上进行开发,首先和别人的版本达到一致,然后再进行提交。具体的操作如下:

git fetch <远程主机名> <分支名> 将最新的远程分支拿到本地,直接git fetch的话,会取回远程主机上所有的分支。

git status 查看一下当前自己所在的分支,是不是需要提交的那个分支,如果不是的话,记得checkout回来。

git merge <远程feature分支 比如像upstream/feature> 将远程的feature分支merge到当前的分支,如果出现冲突,手动修改,修改成最新提交的内容,把本地的内容修改成为别人最新提交的内容,修改完成之后,进行git add操,之后作再进行以上的merge操作,直到没有冲突出现,就可以提交。

关于git rebasegit merge的区别可以参考网上的其他资料,自己具体使用rebase较少,主要是分支历史记录上的区别。

使用git rebase处理冲突

最近遇到的处理冲突的是情况是这样的,我在很久之前提交了一个pr,这个pr一直在进行各种修改和验证,没有合入,直到最近要合并,结果出现了冲突,我用上面的git merge操作进行合并,处理完使用git log查看,记录类似下面这样:

fix conflicts
a's commit
b's commit
c's commit
my first commit

这样在我的pr的页面上就会显示出fix conflicts以及my first commit这两个的git log的信息。负责人在审核我的代码的时候,希望我能将这两个commit合并。这个时候就要用到git rebase了,通过rebase处理完冲突之后git log会像下面这样显示:

my first commit
a's commit
b's commit
c's commit
...

这正是我们想要的结果。具体的原理于细节网上的参考比较多,不再这里细致介绍了,具体操作如下:

git checkout 切换到提交my first commit的分支上(通过fork主库再向自己的库提交代码之后merge到主库的这种场景)

git fetch upstream mainbranch 这里的mainbranch是主库上的发生冲突的分支

git rebase upstream mainbranch 这里会进行一些自动的merge操作,有冲突的地方会标记出来

之后手动将冲突部分修改成最新的内容,之后git add

然后继续执行 git rebase –continue 就可以将整个的rebase操作完成

最后实现的结果就是

my first commit
a's commit
b's commit
c's commit
...

最后通过push操作将对应的分支更行,就满足了需求。

间断commit合并

将间断的commit合并成一个的方式

就是先把当前要提交的branch 比如 cbranch 重命名成cbranch_old 之后切换到最新的分支上 比如 upstream/master 之后从最新的分支上重新checkout出来cbranch 最后把cbranch_old merge 到这个新的分支上,整体的思路是把旧的branch给merge到新的branch上来 具体可以参考这个(https://stackoverflow.com/questions/6248231/git-rebase-after-previous-git-merge)

git checkout my-branch
git branch -m my-branch-old
git checkout master
git checkout -b my-branch
git merge --squash my-branch-old
git commit

注意

如果feature分支上,某个修改需要多个commit才完成,这个时候在使用上面的方法之前,应该先将这个新的分支上的commit都 rebase 成为一个单独的commit,之后再使用上面的方法进行merge。脑子中要有一个映像就是git merge时候的比较是基于分支进行的,比如当前的feature分支与master最新的分支差了n个commit,然后feature分支又有m个commits,这个时候进行merger比较的次数是m*n,很容易产生冲突且导致混乱,如果feature分支先进行rebase之后再merge到master上,这个时候就是m个比较,相对容易些。为了让这个m在数值上尽可能小,最好是在开发的过程中,多rebase到最新的master上(如果开发的周期比较长的话)如果是很小的feature的话,切记在开发之前先从最新的master上checkout分支出来。

合并多个commit

比如在某次修改中提交了多个commit,之后再提pr的时候,希望将这些commit合并成一个,此时需要用到git rebase的操作,关于rebase的操作,这一篇介绍的很细致,这里就不再赘述了,注意每次编辑的时候只需要把最上面的一个commit设置pick操作,剩余的都设置squash操作即可。

追踪文件的历史变化

按照顺序思维考虑,可以知道具体某个commit于其他的commit相比哪些部分的代码都进行了改动,但是换一个角度如果想要知道当前的某个文件中的某一部分代码是通过哪个commit所引入的?这时需要如何操作?

比如在每次进行了更改之后,在git add 之前希望再确认以下更改是不是正确,可以通过

git diff FILE_NAME

查看文件的更改记录。

git blame FILE_NAME

上面这个命令可以显示出具体文件的某一行的最后一次的改动。具体的显示格式为

commit ID | 代码提交作者 | 提交时间 | 代码位于文件中的行数 | 实际代码

获取到对应的commit id之后就可以通过

git show commit_id

查看具体的某次commit行为都修改了哪些文件。对于类似的比较操作最好还是能有一个图形界面来显示,这样操作起来会更容易,特别是对于比较两次版本提交的代码的时候。

还有一种使用方式是查看某个文件自诞生以来的所有的变化情况:

git log --follow <文件名>

这样会显示出所有的对于这个文件进行更改的log信息,可以看到这个文件都发生过哪些变化,通过commit的信息深入地了解代码发生变化的原因。

.gitignore文件

.gitignore文件的作用就是告诉hgit当前目录中那些文件夹是不需要被追踪的,这些文件夹中可能放着一些二进制文件,或者用于本地测试的临时文件,具体语法的细节可以参考最后列出的网站。

多个git repo 使用不同ssh key

不同于以往,github目前一个repo需要设施不同的 ssh key,比如在一个device上要同时操作和更新多个remote repository 这个时候应该怎么设置private 和 piblic key 是比较关键的问题。

具体参考这个:

https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys

注意使用ssh-keygen工具的时候,不同的key要通过文件的名字体现出来。比如这里列出了我当前的 .ssh/config文件的部分内容

Host visperfstudy
Hostname github.com
IdentityFile=/Users/zhewang/.ssh/id_rsa
Host ucv
Hostname github.com
IdentityFile=/Users/zhewang/.ssh/id_ed25519_ucv
Host hexoblog
Hostname github.com
IdentityFile=/Users/zhewang/.ssh/id_ed25519_hexoblog

这个device上一共维护三个repo,使用的 private key 分别是三个对应的不同的key。当push code 的时候,git remote 中的origin可以按照如下模式设置:

origin  git@ucv:wangzhezhe/UCV.git

这时,如果是更新ucv的code的时候,git就会自动的使用文件中指定的/Users/zhewang/.ssh/id_ed25519_ucv 这个key。在git remote repo中设置access key的时候,对应设置的应该是id_ed25519_ucv.pub中的内容。

Using git to sync the code

For most of the times, we can use the ssh plugin in the vs code to connect to the remote env and update code, or we can also use some tools such as the beyond compare to sync the code between the local env and the dedicated env (such as the specific device that have all kinds of access restrictions).

We can also use the git to do that, for exmaple, every time we udpated the code in local env, we can execute

git add .
git commit --amend --no-edit
git push origin ubuntu_env:ubuntu_env --force-with-lease

we need to properly set the first commit well here (for example, a specific function name, for the example described above, we might also need to delete old ubuntu_env and checkout a new one from the latest main branch). After that, at the server side, we can execute:

git fetch --all
git reset --hard origin/ubuntu_env

Just remember to always use the local env as the base of the code (we execute the git add, commit and push in the local env)

submodule使用

开源项目中的fork操作

参考

git tag

remote repo (upstream repo) 中更新了tag之后,local repo 并不会自动更新,即使local branch是从remote 的main branch check out 出来,push到remote的my repo中,这个时候tag也不会自动更新,tag的列表是单独维护的,这个时候需要git push --tag myrepo 这样的操作之后, remote myrepo中的tag 列表才会更新到最新的。tag 不一致的时候可能会引入一些build相关的问题。

Permission issues

If the token is expired or updated, maybe try this:

git config --system --unset credential.helper

Looking at this question to get more details. Even the ssh -T git@github/gitlab.com can not work at this time.

其他

熟悉了命令行的操作之后,使用界面的操作可以提高效率,主要是涉及到代码修改地方的比较。
mac上 source tree,linux上可以使用 smart git。

设置了ssh key之后如果使用https仍然需要使用username以及password,使用ssh protocol的话就不需要再输入用户名和密码了,这个总是容易忘记,具体可以参考这个.

之前在本地修改编辑然后把代码上传到remote server的时候都习惯使用beyond compare, 最近发现使用github来同步也是一个比较好的操作,在local machine上git push 之后在remote server上git pull 一个好处就是修改可以按照commit的方式进行,比如一次没改好的话,使用commit –amend的方式可以在原来的commit基础上再进行修改,这样每次修改的目的都变得比较明确了。

相关资源

比较全的介绍git命令的网站

https://git-scm.com/docs

推荐文章