git的仓储(repository)分为远程repository和本地repository。个人通过本地的repository进行开发和版本管理,本地包括working directory, staging area, local repo;多人开发的工程可通过远程repository进行协作(如github网页可以查看的远端git,或者本地服务器搭建的gitlab),整体的关系如下图所示:

生成gitlab令牌 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分支模型(建分支的意义)

下图引自网络

生成gitlab令牌 gitlab命令行_工作区_02

这个模型适合大型的工程:

  • 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。

详见:

https://medium.com/we-are-madewithlove/pointers-and-tips-dispelling-the-magic-of-git-merge-4c8bb3e227ac

把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。