git的仓储(repository)分为远程repository和本地repository。个人通过本地的repository进行开发和版本管理,本地包括working directory, staging area, local repo;多人开发的工程可通过远程repository进行协作(如github网页可以查看的远端git,或者本地服务器搭建的gitlab),整体的关系如下图所示:
我们先讲解如何创建local repository以及进行代码管理,随后,讲解本地repo和远端repo如何进行互动操作(如何下载remote repository到local repository,以及如何将本地repository贡献到远程repository)。
查看帮助格式如下:
git clone --help
git commit --help
一、本地repsitory管理
1. 概述
熟练进行git代码管理需要了解git的一些一本概念:
首先,了解git的data model, 包括blob object (文件), tree object(结构)和commit object(时间)。
- blob object表示具体的文件的对象,在每次通过git add将working directory的文件添加到staging area时生成,实际就是对应着文件的实体 ;
- tree object记录着每个文件夹里包含的blob object,即目录的层级关系,每个目录对应着一个tree object;
- commit object为每次提交产生的对象,记录着提交指向的tree object以及commit的parent节点(即上次提交生成的commit object)。
备注:当一个文件或目录结构发生更新时,不仅会被更新的blob object和tree object会新建,commit后其祖先节点上的所有tree object都会生成新的tree object(因为tree object记录了包含的对象的checksum,一个object的改变会导致其checksum改变,进而改变其父节点的tree object记录的object的checksum的改变,而tree object记录的信息的改变也就改变了tree object本身的checksum,以此类推)。
object文件统一存储在.git/objects中。
详见:https://medium.com/hackernoon/https-medium-com-zspajich-understanding-git-data-model-95eb16cc99f5
为了查看时间维度的commit object的checksum值,可以使用如下命令:
git log
通过如下命令可以查看commit object中记录的内容(包含指向的tree object的checksum值):
git cat-file commit [commit id in git log]
通过commit object记录的tree object id可以进一步查看tree object记录的内容:
git ls-tree [tree object id]
结合tree object指向的blob id,可以通过如下命令查看blob object的内容。
git cat-file blob [blob id]
随后,掌握git的三个区域:working directory, staging area, and the repository。注意:这个三个区域并不对应着三个不同的实体区域来存储不同版本的blob object\tree object实体。真实的情况是:所有不同版本的blob object\tree object都存储在.git/objects中,同时,使用使用.git/index文件来记录working directory, staging area, and the repository中对应的blob object\tree object的版本(checksum值),基于index文件记录的object版本的信息到.git/objects中找对应版本的object来快速构建不同的版本工程。
详见:
https://konrad126.medium.com/understanding-git-index-4821a0765cf
为了显示working directory <=> staging area <=> 本地repository之间的差异,可以使用如下命令:
git status
2. 配置本地git
全局配置:
git config --global user.name "YLY"
git config --global user.email "yly@163.com"
生成本地repository:
cd your_folder_to_upload
git init //本地仓储初始化,在文件夹中生成了 “.git”隐藏文件,用于接下来存储git的信息。其中,.git/object/里面包含了blob object, tree object, commit object;.git/refs/包含branch的信息;./git/index存储working directory, staging area, and the repository的文件版本信息
3. working directory/staging area/local repo之间的操作
将working directory文件同步到staging area:
git add . //同步working directory的文件到staging area, “.”表示添加文件夹下的所有文件
另一方面,若开发过程中发现工作区本版出现问题了,想撤销工作区文件的修改,可以把文件恢复到staging area的状态,方法是:
git checkout -- <file>
staging area文件更新到本地repository:
git commit -m "Initial commit" // 文件存储到版本库区 (Commit History), -m表示输入提交的信息
备注:修改最新的commit message:
git commit --amend
使用commit模板参考:Git - Git Configuration
如果修改了working directory的文件,需要同步到本地repository,再分别执行
git add your_changed_file //向本地repository中的staging area添加文件:如果只想添加特定文件夹或文件,直接写 git add your_file_name; “.”表示添加文件夹下的所有新建的文件和被修改的文件(不包含被删除的更新); git add -u 表示提交所有修改的文件和被删除到数据staging area
git commit -m "Changed files" // 将更改同步到本地repository (commit表示修改),此步骤不可忽略,因为远端repository是从本地repository进行同步的,所以必须先把修改同步到repository;如果修改没有同步到本地repository,直接push到远端,则会显示“Everything up-to-date”
4. 版本回退
git reset <commit id>
--soft:staging area和working directory都不受影响
--mixed:staging area更新到历史的commit的状态,working directory不受影响(reset的默认选项)
--hard:staging area和working directory都更新到指定的历史commit
git reset --soft HEAD~1 //repository变为HEAD指向的commit,但是working directory和staging area保持不变,这里HEAD~1表示撤销上一次commit
git reset --mixed HEAD~1 //repository变为HEAD指向的commit,staging area也同步更新,但是working directory的内容保持原来不变
git reset --hard HEAD~1 //repository,staging area和working directory都变为HEAD指向的内容。
reset更详细的介绍可参见:https://medium.com/hackernoon/reset-101-ba05d9e3f2c7
这种方法会造成历史版本的永久丢失,更推荐的方法是通过revert版本反做实现回退,即在后面添加过去的版本的新的commit:
git revert HEAD~1
二. 分支管理
1. git分支模型(建分支的意义)
下图引自网络
这个模型适合大型的工程:
- feature的处理和bug的处理方式是不一样的:feature开发是从develop分支上拉取新的分支,随着feature开完完成合入develop后,该feature分支可以自动删除;bug可以直接在release分支上开发,然后merge back会develop分支,如果是影响生产的bug需要从master中拉出hotfixs分支,修复bug并merge到master分支,同时merge back到develop分支中;
- 大型工程不会在master上直接开发;
- develop和release分支在大型工程中是分开的,新的版本feature合入develop后会再从develop分支合入release分支,可以任务release分支时develop分支和master分支的缓冲带,develop分支更新会非常快,master分支合入会比较慢,如果没有release分支可能会出现develop在合入master的过程中(该过程可能比较长需要反复review修改),develop有发生了很多变化,不是每次release分支发版本都需要合入master,master的更新往往落后于release;对于中型工程,develop和release分支一般仅使用一条(仅保留develop分支);
- feature分支无需引入bugfix(除非影响feature的开发)。
参考:
2. 分支的原理
理解branch (实际是ref)的概念。每个commit object的parent会指向上一个历史的commit object,因此,众多的commit object形成了一个DAG(有向无环图)。从最后一个commit object可以追溯出每个分支的所有commit object,因此,确定一个分支只需要找到该分支的最后一个commit,实际上分支存储的就是一个指向该分支最后一个commit的指针,可以在.git/refs/heads文件夹下找到所有的分支,每个分支实质是一个存储着该分支上最后一个commit的checksum的文件。为了确定当前所在的分支,引入另一个指针HEAD,HEAD指向当前所在分支,查看上.git/HEAD文件可以看到里面记录的是ref:refs/head/master类似的信息。
详见:https://medium.com/hackernoon/understanding-git-branching-2662f5882f9
3. 分支的操作
对分支的基本操作:
git branch //查看所有分支和当前所在分支
git branch <branchname> //创建分支
git checkout <branchname> //切换分支
git checkout -b <branchname> //创建+切换分支 For example, git checkout -b v2.0
git branch -d <branchname> //删除分支 delete
checkout切换分支时也会改变当前的工作区、索引区的内容。如果当前工作区的修改存在未commit的工作,checkout会提示
error: Your local changes to the following files would be overwritten by checkout:
****
Please, commit your changes or stash them before you can switch branches.
Aborting
可以将修改commit,随后,切换分支(工作区也会变成切换到的分支对应的代码);或者stash工作区的修改,工作区随着分支切换变为切换到的分支对应的代码后,再stash pop把工作区变为之前的代码。
git stash #本地工作区的代码暂存到git栈中
git checkout <another_branch> #工作区内容更新为另一个分支的内容
git stash pop #之前本地工作区的代码再更新到工作区
Detached HEAD:
上文中提到HEAD一般指向一个分支(ref),一个分支实际指向的是该分支最后的commit。有时如果为了快速查看某一历史commit时的状态,会直接checkout到一个历史commit,此时HEAD指向的不再是一个分支(ref),而是指向具体的一个历史commit,这就是Detached HEAD。
使用git log查看commit的版本号,例如:
commit 5624dfsfsdfdsfsdfljsdpfpsdfsdlfjosdfi9c
Author: yly <yly@163.com>
Date: Thu Jan 21 15:05:35 2021 +0800
随后使用checkout到一个历史commit:
git checkout <SHA1> //HEAD切换到具体的一个commit,但会导致git status时提醒HEAD detached at <SHA1>, 如果需要回到可被追踪的分支可以选择git checkout <branchname>
git checkout 5624dfsfsdfdsfsdfljsdpfpsdfsdlfjosdfi9c
可以基于这一commit查看代码,进行修改或commit,如果不想保存,可使用如下命令即可切换回分支:
git checkout <branch-name>
如果需要保存这些更新的commit,可以
git branch <new-branch>
随后,
git checkout <new-branch>
则创建了新的分支,并将HEAD指向了该分支。
详见:
当前,建议的更好的操作是创建一个临时分支,进行处理后再删除临时分支:
git checkout -b tmp_branch 5624dfsfsdfdsfsdfljsdpfpsdfsdlfjosdfi9c //进行历史版本查看或修改
git checkout master
git branch -d tmp_branch
Cherry-pick:
对于不想要另一个整个分支的更新,只想取其中部分commit的情况,可以查看需要的另一分支的commit id后,在当前分支,进行cherry-pick,即在最后的commit后添加指定的commit。
git cherry-pick <commit id>
Merge:
掌握Merge的原理。两个分支merge,生成一个新的commit,新的commit内容按照如下方法确定:是先找到两个分支公共的commit,将两个分支最后commit版本的文件和公共commit的版本对比,a. 若两个分支最后commmit版本和两个分支公共commit版本相同,则merge后使用该版本的文件;b.若某个分支上最后commit版本的进行了更新,另一个分支最后commit版本与公共commit版本对应文件相同,则使用更新的commit的文件作为merge后的文件;c. 若两个分支上的最后commit版本的文件相比于公共commit对应的文件都进行了更新,则由用户解决merge confict。
详见:
把master分支合入到feature分支:
git checkout feature
git merge master
Fast-forward是merge的一个特例情况,是一个分支的commit单纯地落后于另一个分支,相比于快的分支,慢的分支没有额外的不同的commit。此时进行merge,git倾向于不再用生成一个新的commit,而是直接把快的分支的commit写给慢的分支。
对于大型工程,一般使用Merge合入,很好地保留多个分支的历史记录。
Rebase:
rebase原理:找到两个分支的公共commit,把所在分支的特有更新整体接到rebase的分支的commit之后。
git checkout feature
git rebase master
三、本地repo和远端repo操作
下载网页remote repository到local repository
1. 首次下载
git clone https://192.168.8.66/YLY/test.git
默认将remote repository下载到本地运行上述命令的路径下。也可以选择将工程下载到指定文件夹名下:
git clone https://192.168.8.66/YLY/test.git <assigned_folder>
此时查看本地分支:
git branch --list #也可省略--list
发现只显示master分支。可通过如下命令查看所有local & remote分支:
git branch -a
clone时已经将remote repo中全部分支的代码下载到本地,并在本地创建了分支,本地的分支和跟踪的远程分支分别存储在:
- .git/refs/heads/[本地分支]
- .git/refs/remotes/origin[正在跟踪的分支]
若想切换到其他分支,只需要运行:
git checkout <远端分支名>
会显示本地生成一个branch来track远端的该分支。此时再运行git branch可以看到本地添加了新的分支。
以上命令是在本地创建master分支并tracking远端的master,若需要直接创建一个关联远端某一非master的分支的branch,可使用命令:
git clone https://192.168.8.66/YLY/test.git -b <分支名>
2. 更新local repo
把远端更新同步到本地repo中的跟踪的远端分支:
git fetch
git fetch会把remote repo的所有更新写入本地repo中的.git/refs/remotes/origin,包括remote repo新创建的branch、以及所有之前已经在跟踪但更新了的branch,但local repo中的.git/refs/heads/[本地分支]并没有改变。
要想更新本地分支,还需要执行merge,首先,checkout到想要更新的本地的分支:
git checkout master
随后,把跟踪的远端分支更新合入到本地:
git merge origin/master
注意,git fetch是将所有更新同步到local repo中跟踪的远端分支,git merge则是只能针对某一本地分支更新。
也可以使用git pull,其作用等同于git fetch + git merge。
参考:
若本地修改了代码,会报错:
error: Your local changes to the following files would be overwritten by merge:
Please, commit your changes or stash them before you can merge.
Aborting
解决方案:
git stash #本地工作区的代码暂存到git栈中
git pull #远程工程更新到本地
git stash pop #之前本地修改的代码再更新到工作区
本地文件上传到github/gitlab上(远端网络repository):
具体命令如下:
git remote add origin https://192.168.8.66/YLY/test.git //添加远端网络repository, origin可以看为https://192.168.8.66/YLY/test.git的别名,即远程仓库在本地的别名
git push -u origin master // 将本地master分支推送到origin主机的master分支。第一次加了flag -u(即--set-upstream,指定默认主机,建立当前分支与远程分支之间的追踪关系)后,以后即可直接用git push(后面无需参数)
若推送到远程的非master分支,语法:git push <远程主机名> <src-branch>:<dst-branch>
其中,src-branch是本地的分支名,dst-branch是remote branch。
查看已经添加的远端网络repository地址的命令:
git remote -v
push时提示:
error: failed to push some refs to 'https://192.168.8.66/YLY/test.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
其他人向同一远端repository推送了代码,导致本地不是最新的。
解决方案:
git pull origin master
若远端repository更新的文件与本地冲突,可能需要手动在本地merge。
本地repo和远端repo代码merge:
使用pull request或merge request。
对于小型的工程(比如5人以下),可以在远端仅使用一个master分支,所有人的合入都基于rebase,用户看到的是单链,比较清晰。
在开发前拉了最新的master的commit,本地更新了commit,同时远端也有个人更新了commit并已经合入master了,可使用如下命令将自己的commit接入远端的commit之后:
git pull --rebase
该命令会把本地当前分支里的每个提交(commit)取消掉并把它们临时保存为补丁(patch)(这些补丁放到".git/rebase"目录中),然后把本地当前分支更新为最新的"origin"分支,最后再把保存的这些补丁应用到本地当前分支上。
若Rebase过程中产生冲突,手动解决冲突后继续rebase:
git add -u
git rebase --continue
如果想要放弃rebase,让分支回到rebase前的状态:
git rebase --abort
使用rebase方法合并分支,git rebase和git merge的区别可参见:
push是将本地分支合并到远程分支,pull是将远程分支合并到本地分支(fetch+merge)。
如果多个分支未对同一个文件修改,则会自动merge,不会产生冲突。否则需要手动解决冲突。
可以把分支变成master,即点击merge request请求,随后master的管理员可以选择同意Merge,则该分支的更新合入master。
另外,如果当前的分支处于merge冲突待解决的状态时,是无法checkout到其他分支的,如果想放弃merge回到之前的状态,可以使用:
git reset --merge
2.重新配置
2.1 删除本地repository:使用terminal删除文件夹下的 .git文件夹(隐藏文件夹)。
2.2 删除远端repository: 网页中setting->general->advanced->remove project。
2.3 添加新的远端地址或修改远端地址
例如,修改本地工程对应的远端,即上传到另一个远端repository上,直接添加新的远端地址会出现错误:
git remote add origin https://192.168.8.66/YLY/test_updated.git
显示:
fatal: remote origin already exists.
解决方案:
git remote rm origin
随后再添加新的远端地址:
git remote add origin https://192.168.8.66/YLY/test_updated.git
或者直接修改:
git remote set-url origin https://192.168.8.66/YLY/test_updated.git
bisect: https://konrad126.medium.com/keep-calm-and-git-bisect-1610c6bced29
四、打tag
创建本地标签:
git tag <tagname> //轻量标签lightweight
git tag -a v0.1 -m "my version 0.1" //附注标签annotated, 即加一条-m后的注释
对历史commit打标签:
git log --pretty=oneline //显示commit id
git tag -a v0.1 sdfdsfoidsmoj43nwn5434324l -m "my version 0.1" //选择需要的commit id打标签
查看tag信息:
git tag //查看本地所有tag
git show v1.0 //查看某一个tag的详细信息
将所有标签推送到远端:
git push origin --tags
五、其他问题
1. clone时出现TCP connection reset by peer
Cloning into 'FCOS'...
fatal: unable to access 'https://github.com/tianzhi0549/FCOS.git/': TCP connection reset by peer
解决方案:将网址中"https"修改为git便能正常下载。
git clone git://github.com/tianzhi0549/FCOS.git
2. 远程仓库pull代码到本地时:
PS: 下载过程中如果出现证书错误,即
Cloning into 'test'...
fatal: unable to access 'https://192.168.8.66/YLY/test.git/': server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
解决方案是在terminal中输入:
export GIT_SSL_NO_VERIFY=1
本地代码push到远程仓库时:
执行命令
git push -u origin master
时可能出现
remote: HTTP Basic: Access denied
fatal: Authentication failed for 'https://192.168.8.66/YLY/test.git/'
解决方案:
在terminal中输入
export GIT_SSL_NO_VERIFY=1
该命令仅在当前的terminal有效。
另一种可能的证书错误bug:
fatal: unable to access 'https://192.168.8.66/YLY/test.git/': server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
解决方案:
git config --global http.sslverify "false"
进行操作,完成后再回置设置:
git config --global http.sslverify "true"
git配置ssh免密操作:
git支持https协议和git协议,使用https协议时每次pull, push都会提示要输入密码;使用git协议,通过ssh密钥,可以避免每次都输密码。
方法:先生成密钥,使用ssh-keygen生成密钥对:
ssh-keygen -t ed25519 -C "your-email"
生成的密钥对在~/.ssh路径下,id_ed25519存储的是私钥,id_ed25519.pub是公钥,把id_ed25519.pub文件中的内容粘贴到github网页中的ssh key里。
参考:
另外,团队代码审查可以使用Gerrit。Gerrit是在git基础上打造的基于web的代码审查工具。
CICD可以使用Jenkins。