Git是一款免费、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。

如果想正式一点做一个项目,还是需要系统学习一下 Git

Git简介

Git是一个版本管理工具,自然最重要的就是版本管理.在Git中,每一版本存为一个commit,以单向链表的形式存储,每一个结点都存储了一个版本,父结点存储的是它的前一个版本.
显然,最前面的头指针指向的就是最新的版本结点.

而每个版本又存在不同的区域:

  • working tree
  • index(staged file)
  • HEAD

working tree就是我们正常在文件浏览器里能够看到的文件
index是缓存区,标记了本版本和上一版本之间的差异
HEAD则相当于在整个Git里的位置指针,记录了当前的位置

而文件则存在如下几种状态

  • Untracked files
    新建的文件
  • Changes not staged for commit(Changed but not updated)
    更改的文件
    • modified
      修改的文件
    • deleted
      删除文件
  • Changes to be committed(staged)
    已被add的文件(暂存状态)

文件的改动无非删除,修改,新建三种
而这三种外还有暂存区(staged)
这个就是当前版本记录下的改动

git 命令

git init

<path> 默认为当前目录

初始化git库(本地库)

这个是本地库,能够在文件管理器里方便地操作文件

git init <path>

初始化git库(服务器端)

这个是远程库(remote),不能直接操作文件

git init --bare <path>

git clone

这个指令可以将别人的库直接 clone 下来
如果只需要最新版本,可以增加 --depth=1

git clone <url> <path>

git add

将文件提交到暂存区

相应, git add 有如下用法

git add <filename   //提交指定文件
git add .             //提交untracked files和changes not staged for commit
git add -u <filename//提交changes not staged for commit (tracked file)
git add -A <filename//提交所有的变化

其中,在Git version 1.x中, git add . 不包括删除的文件
在Git version 2.x中, git add . 包括删除的文件,另加了 git add --ignore-removal . 替代原用法


另外可以使用 git status 查看当前三种状态的文件
或者 git add -i 分类查看各个状态的文件

本地库操作

git commit

git commit 是极为重要的一部分
这里记录了你每次更改文件的意义
因此保持一个比较好的格式非常重要

git commit                  //调用文本编辑器输入commit信息
git commit -m "<message>"   //直接输入信息
git commit -a               //自动add tracked files 然后调用编辑器输入commit

Commit message 的格式

每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。

<type>(<scope>): <subject>

<body>

<footer>

其中,Header 是必需的,Body 和 Footer 可以省略。
不管是哪一个部分,任何一行都不得超过72个字符(或100个字符)。这是为了避免自动换行影响美观。

Header

Header部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)。
(1)type
type用于说明 commit 的类别,只允许使用下面7个标识。

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动
    如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。其他情况(docs、chore、style、refactor、test)由你决定,要不要放入 Change log,建议是不要。

(2)scope
scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

(3)subject
subject是 commit 目的的简短描述,不超过50个字符。
以动词开头,使用第一人称现在时,比如change,而不是changed或changes
第一个字母小写
结尾不加句号(.)


Body

Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. Further paragraphs come after blank lines.- Bullet points are okay, too- Use a hanging indent
有两个注意点。
(1)使用第一人称现在时,比如使用change而不是changed或changes。
(2)应该说明代码变动的动机,以及与以前行为的对比。


Footer

Footer 部分只用于两种情况。
(1)不兼容变动
如果当前代码与上一个版本不兼容,则 Footer 部分以BREAKING CHANGE开头,后面是对变动的描述、以及变动理由和迁移方法。

BREAKING CHANGE: isolate scope bindings definition has changed.
    To migrate the code follow the example below:
    Before:
    scope: {
      myAttr: 'attribute',
    }
    After:
    scope: {
      myAttr: '@',
    }
    The removed `inject` wasn't generaly useful for directives so there should be no code using it.

(2)关闭 Issue
如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue 。

Closes #234
也可以一次关闭多个 issue 。

Closes #123, #245, #992
revert
还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以revert:开头,后面跟着被撤销 Commit 的 Header。

revert: feat(pencil): add 'graphiteWidth' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body部分的格式是固定的,必须写成 This reverts commit <hash>.,其中的 hash 是被撤销 commit 的 SHA 标识符。
如果当前 commit 与被撤销的 commit,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的Reverts小标题下面。

git branch

分支就类似平行宇宙,在这一刻你可以选择用python来解决一个问题,也可以选择用java解决这个问题
于是开了两个分支,python和java,你可以随便更改任意一个分支的内容而不影响另一个分支的内容
当你发现python能更好解决问题时,就可以删掉java那个分支,让python与主分支合并

查看分支

当前分支前有*标记

git branch      //列出本地分支
git branch -r   //列出远程分支
git branch -a   //列出本地和远程分支

创建分支及切换分支

默认情况下,我们是在主分支 master
如果需要切换分支,需要 git checkout 指令

git branch <branchName>       //创建分支(不切换)
git checkout <branchName>     //切换到已有分支
git checkout -b <branchName>  //创建分支并且切换到新分支

新建分支都是在当前所在的分支基础上克隆一份,commit只会影响当前分支的内容

并且分支是共用暂存区的,也就是说如果在分支1上仅 add 而不 commit,实际暂存区中已经记录该次修改
哪怕后续切换到分支2上再进行 commit 也是有效的操作

因此,切换分支前一定要确保commit

重命名分支

git branch -m <OldName> <NewName> //重命名分支
git branch -M <OldName> <NewName> //强制重命名分支(忽略重名)

删除分支

需要注意不能删除当前所在的分支(不能删除当前所在的世界)

git branch -d <branchName>      //删除分支
git branch -D <branchName>      //强制删除分支(忽略分支未merge部分)
git branch -d -r <branchName>   //删除远程branchname分支

通过分支进行多人协作

master 分支作为最终发布的版本,只有在更新到最终版时,将各分支 merge 到该分支
dev 作为大家个人分支的合并分支,用于代码合并
而个人分支就是自己维护自己的部分即可

git checkout

对于整个Git而言,他是一个有向链表,由于存在 branchmerge 操作,因此局部会存在有向无环图(DAG)
链表的每一个结点都是一个 commit
HEAD 指向的就是当前所在的 commit
那么 HEAD^ 是上一个提交,HEAD^^ 是上上一次
前十次提交可以用 HEAD^^^^^^^^^^ 也可以用 HEAD~10

checkout 的功能就是切换到任意一个 commit

git checkout <commit_HASH>

当切换到某个分支时,默认时切换到该分支的最新状态

而对于想把某个文件恢复到上个 commit 后的状态,可以使用

git checkout -- <filename>

这样可以清除该文件在工作区的修改

git merge

合并两个分支

合并分支是Git的一个重要功能
可以直接使用改命令将一个分支合并当前分支

git merge <branchName>

其中有选项

  • --no-ff 三方合并并提交修改(默认)
  • --ff-only 判断当前分支可否根据目标分支快速合并

冲突处理

但是对于将待合并分支合并到当前分支的情况,需要注意有冲突的情况存在
如果待合并分支是在当前分支的基础上修改而来的,那么显然直接将当前分支更新到待合并分支状态即可
如果待合并分支当前分支在某个时间点分开后各自有了更新,就会产生冲突
这时使用 git merge 会提醒你存在冲突,需要手动解决

使用 git status 查看冲突的文件,然后手动进去修改
需要注意的是,这时冲突文件是两个版本文件的综合体

<<<<<<< HEAD
aaaaa
=======
bbbbb
>>>>>>> feature1

使用这样的方式来标记不同
合并后就可以删除掉分支了

取消合并

如果merge一半不想merge了,就用下面的代码来回滚merge操作

git merge --abort

git rebase

git rebase 也是将两个分支合并到一起的操作
由名字可以看出来,rebase会将待合并分支记录加到当前分支前面,当作初始状态,然后再按照当前分支的提交进行合并(会丢失原本的commit)

git mergegit rebase 和区别有以下几点

  • merge 是一个合并操作,会将两个分支的修改合并在一起,默认操作的情况下会提交合并中修改的内容
  • merge 的提交历史忠实地记录了实际发生过什么,关注点在真实的提交历史上面
  • rebase 并没有进行合并操作,只是提取了当前分支的修改,将其复制在了目标分支的最新提交后面
  • rebase 的提交历史反映了项目过程中发生了什么,关注点在开发过程上面

git revert

该命令是指新建一个commit,内容撤销某个commit(中间的错误路线保留)

git revert <commit_HASH>

如果log如下

commit 3 add c
commit 2 add b
commit 1 add a

现在我在3这个状态,当我 git revert 2
记录变为

commit 4 revert b
commit 3 add c
commit 2 add b
commit 1 add a

同时文件变为 a c (撤销了生成 b 的操作)

git reset

该命令是直接回到指定commit,之前的错误commit全部舍弃(假装自己没有错过)

git reset [--mixed] <cimmit_HASH> //只更改HEAD和index
git reset --soft <commit_HASH>    //只把HEAD回退回去,index和working tree都不变
git reset --hard <commit_HASH>    //修改index和working tree,忽略所有改动文件

看上去可能比较难以理解,举个例子
如果使用 --mixed 那么还原后,会以目标commit为基础分析文件,这样你看到的就是一群改动等待被commit,可以把需要恢复的挑出来
如果使用 --hard 那么就是原汁原味的目标commit的样子,文件全部都是当时的状态
如果使用 --soft 那么目录内的文件没有被修改,也没有文件在暂存区
但是这三种都有一个问题:HEAD回到了过去,并且之后的commit都扔掉了
那么就有一个尴尬的问题了,--soft会导致有的文件在git里并没有生成的记录,但是它就是存在了

并且 git checkoutgit reset --hard 可以看成是差不多的,唯一的区别就是后面的log是否还保留(事实上尽管reset后看上去记录没了,但是git还是会保留下来的,除非去运行清理指令)
但是,如果你checkout回到之前的状态,但是当前分支仍然指向原来的commit,如果你在之前的状态又提交了commit,是不能算在该分支的,这个commit就游离在了git链表之外(因为每个commit只能指向它的前一个commit)

所以干什么的命令就是干什么的,虽然看上去可能能做一些自己职责之外的事情,但是还是不要作死乱用

git reflog

使用reflog找到丢失的commit

如果已经已经被删掉(合并)的 commit 有用怎么办???
存在游离的 commit 怎么办???
手贱 hard rebase 到了错误的地方把有用的记录删掉了怎么办???

git reflog 来救你!
git非常机智地留了一个回收站,你所有删掉的 commit 都在这里!

可以使用任意的方法重新拿到需要的数据(branch,checkout,rebase)

清除reflog

那么如何清空这个“回收站”呢?
运行下面的代码,你所有游离的commit都被释放掉了(这次再手误就真的没救了)

git filter-branch --index-filter "git rm -r --cached --ignore-unmatch path/to/your/file" HEAD  
git push origin master --force  
rm -rf .git/refs/original/  
git reflog expire --expire=now --all  
git gc --prune=now  
git gc --aggressive --prune=now  

git tag

标签可以代替commit id,方便查找特殊的记录

git tag                           //列出所有标签
git tag [-a] <tagName> [-m "<Message>"] [<commit_HASH>]   //给指定的commit(默认为当前状态)打标签,并附加说明

远程库操作

git remote

remote是git的远程分支
可以看做是把本地部分在网上的一个备份
除了使用命令行外,也可以在 '.git/config' 修改

remote对于每个远程库可以添加多个url(相当于多端备份)

git remote                                                  //查看所有的remote
git remote -v                                               //查看所有的remote详细信息
git remote add <remoteName> <url>                           //增加新的remote
git remote rm <name>                                        //删除指定的remot
git remote set-url --add [remoteName] [newUrl]              //为指定remote增加链接
git remote set-url --push <remoteName> <newUrl> [<oldUrl>]  //修改远程分支的url
git remote set-url --delete <remoteName> <Url>              //删除远程分支的指定url

git push

将本地分支推送到远程分支

如果省略本地分支,就是把一个空的分支push到远程分支(也即删除远程分支)

另外如果使用 -u 参数,相当于同时把当前的设置记录为默认,下次如果省略相应参数的话,会优先按照本地的进行提交

git push [-u] <remoteName> <localBranch>:<remoteBranch>  //将本地分支推送到远程分支

git push有两种模式

  • simple模式
    存在默认配置的情况下,按照默认配置提交
    git config --global push.default simple
  • matching模式
    将远程分支对应的本地分支推送上去(以远程分支为主,不会新建远程分支)
    git config --global push.default matching

然后很自然就会想到能不能把本地所有分支都推送到远程主机
自然是可以的
git push --all <remoteName>

如果远程分支比本地分支还要新,就会产生冲突,需要先合并差异
当然也可以用 --force 或者 -f 强制提交

默认情况下,push不会推送tag,需要增加--tags选项

git pull

pull 是和 push 完全相反的操作(对应的指令也恰好对应相反)

会将远程分支合并到本地分支
也即,pull是一个合并操作,那么自然就就存在mergerebase的区别
pull里,默认是merge,可以使用--rebase使用rebase的形式合并

git pull [--rebase] <remoteName> <remoteBranch>:<localBranch>

git fetch

git fetch 是拉取远程分支的更新

将远程分支拉取到本地的 <remoteName>/<remoteBranch> 分支
然后就可以进行合并等操作了

其中如果指定远程分支名,则只会拉取指定的远程分支

git fetch <remoteName> [<remoteBranch>]

对比fetchpull
可以发现 fetch + merge = pull

git modoule

当你的项目里需要用到别人的项目时,可能就会出项 git 嵌套 git 的情况
这时就需要使用子模块来管理 git

自己项目的子模块

git submodule add <url> <path> //新建子模块

新建后,目录下会生成一个 .gitmodules 记录该项目的所有子模块信息
当子模块有更改时,只会有一个标记记录了模块有更改(没有具体更改内容)
对于主项目来说,子项目只是一个 HEAD 标记子项目对应的时哪个 commit 状态

别人项目的子模块

而对于 clone 别人的项目时,如果存在子模块,需要单独初始化子模块部分

git submodule init    //初始化子模块
git submodule update  //更新到该项目中记录的 `commit`

如果项目在远程分支有更新,除了要使用 pull 或者 fetch 更新项目外,还要单独使用 git submodule update 更新子模块

删除子项目

git ls-files --stage | grep 160000
git rm --cached <path>

.gitignore

这个是存在于项目根目录的文件,用于记录需要忽略的文件(编译生成的中间文件,有敏感信息的文件)
一行一条需要忽略的文件路径信息

参考链接