Git踩坑指南

最近新的项目需要抛弃svn使用git,之前对git一脸懵逼仅限于会拼写,趁这几天不算忙学习记录下。

常用命令

git clone urlclone项目代码到本地
git add file_name添加修改的文件到暂存区
git commit -m "command"将暂存区的修改提交到本地仓库
git push origin branch_name将本地仓库的某个分支更新到远程仓库
git status查看当前所在分支的状态
git reset --soft/--hard/--mixed commit_id回退到某次提交的状态(多人协作开发不建议使用)
git revert commit_id回退某次提交的修改,会产生一个新的commit,不改变原来的commit记录
git branch branch_name新建一个分支
git checkout branch_name切换到某个分支
git branch -b branch_name创建并切换到该分支
git merge branch_name将某分支合并到该分支
git branch -a查看本地和远程的所有分支
git branch -d branch_name删除本地分支
git push origin -d branch_name删除远程分支
git log --oneline --decorate --graph --all查看分支详细信息
git submodule init第一次clone带有子模块的项目时需要进行子模块配置的初始化
git submodule update --remote更新子模块

基础

git与svn最大的一个区别就是对于提交文件的存储方式,对于每次提交,svn是存储文件的差异,而git是存储文件的快照,用官网的两张图可以更清楚的看出区别:
svn:
svn存储方式
git:
git存储方式

这样子做有啥子好处呢?官网有具体的说明,但是我也没有理解到底有啥好处…感受就是对于同一份项目代码,从svn上clone下来花的时间比git clone下来的时间慢的多,不知道是我司对git服务器有加速?还是这种存储方式导致的。

首先需要明确的一个概念是:对于一个repository,可以说有两个存储仓库,一个在本地,一个在远程服务器,每次修改的都是本地存储,只有在执行push指令之后才会将本地的存储推(更新)到远程存储,所以只要不push,什么add,commit可劲造,不会影响到别人,造完记得reset还原就行。

还有一个概念也要提一下,每次修改文件到提交到本地仓库,其实这个文件经历了三个存储区域(三种状态)

  • 第一个是工作区域,就是存储原始文件的地方,对代码的各种修改都是在这个区域下完成的。
  • 第二个是暂存区域,修改之后必须要经过暂存区域才能将文件存储到本地仓库,可以将其理解为一个缓冲。修改之后执行git add指令就会将指定文件放到暂存区域了,我一般直接在根目录下git add .它会递归的在这个目录下寻找所有修改过的文件都添加到暂存区域。当然如果有一些文件你不想提交就要挨个add了。官方的说法这个区域是确实存在的,格式是一个文件,里面存放了add过(待提交)的文件列表信息,默认这个文件存放在git仓库目录中。
  • 第三个就是你的本地仓库,git commit就可以将文件从暂存区域提交到本地存储了。只有将改动提交到本地存储,你才能推到远程仓库。

还有一个非常实用的命令git status,可以很清楚的看出你当前的文件都处于一个什么状态,当然你从修改完到add到commit到push一气呵成,这个指令确实没什么用。但是如果一气呵不成流程被中断了,这个指令就比较有意思了。

  1. 当新建了一个test.txt文件,没有add时,运行git status,git bash会显示:
    1219212224
    untracked表示没有跟踪,对于跟踪的概念官网有官方的解释,可以简单粗暴的认为,没有跟踪的就是新建的文件。这时候咋整,add->commit->push。大白话来说就是这个文件git在现有仓库中没有见过,啥叫见过,clone之后git会在本地建一个数据库,以文件内容或目录结构进行一个SHA-1校验和的计算,将这个值作为该文件的索引。

  2. 当改了文件内容,保存了没有add,或者add了没有commit,此时git status显示:
    1219213738
    changes not staged for commit就是改了没有add到暂存区域,add->commit->push。
    changes to be committed就是说已经add到暂存区域,可以commit提交了,commit->push。
    如果你不想提交add过的修改咋整,看括号git restore --staged,改过但是没有add,git restore会将这个文件还原成上一次提交的状态,相当于一键ctrl-z,git提示简直不能太友好啊有木有!

  3. 当commit了但是没有push,git status显示:
    1219215155
    很直观,有一次提交,还没有push到远程。

那如果说这个时候你提交过的有问题,或者还需要加东西要撤销本次提交,不想push到远程祸害别人,不要慌,git reset了解下。有三个常用参数

  • --soft只是撤销这次commit,改过的内容不会恢复,这个时候可以重新git commit进行提交,或者在这个代码基础上新增改动,然后add->commit。
  • --hard就比较粗暴了,直接将代码完全还原成上一次提交的状态
  • mixed这是默认的参数,作用是移除暂存区,相当于到达add之前的状态,同样不会改变工作目录的代码。
    但是在和别人合作完成项目的时候不建议使用git reset,因为它会使HEAD指针前移,覆盖之后的提交记录。直白的说就是会删除提交历史记录,这样会导致一个严重的问题,没办法恢复撤销。常用的撤销是git revert,它会撤销你做出的某次修改,但是会将这次撤销作为一个commit进行提交,而不改变提交历史记录,常用的参数:
  • git revert HEAD撤销上一次的提交
  • git revert commit_id撤销某次的提交
    实验中发现,如果是跳跃的撤销连续增加的情况就会有冲突问题需要解决,比如说我这里提交了3次,分别是增加了1111,2222,3333:
    12222204006
    如果我想撤销2222的那次提交,执行git revert 2e6b6e430cfaf444e2a6b6191bf8b9a72292dd96就会出现冲突提示:
    1222204221
    需要手动处理下冲突。
    当涉及文件重命名的时候也比较有意思,如果直接在工作目录下进行重命名,git status会发现状态是将原有文件删除,新增一个untracked未跟踪文件,你需要将改名前的文件和改名后的文件都add一下然后commit。这里有一个更好的方式是直接在git命令行中运行git mv old_filename new_filename,再git status就会发现状态是renamed,直接add该文件就可以提交了。

分支

不管你信不信,官网是吹爆了git的分支机制,称为git的必杀技。嗯,我还没有悟透,除了拉分支秒完成以外还没有发现它牛X在哪。
当从远程clone下来之后,git会在本地创建一个master分支,跟踪到远程仓库的master分支(origin/master),可以直接在master分支上进行开发,但是git不建议这样,因为master一般存放的是稳定可交付的一些提交,多人协作开发时你提交了一些测试状态的代码到master上对别人会有影响。建议自己在本地拉一个dev分支,在这个分支上进行开发,开发完成测试ok之后再merge到本地仓库的mater中然后push到远程仓库。这里其实不需要提交dev分支到远程仓库,这个前提是你有一个环境可以拉取到本地dev分支的代码并且可以运行。

当创建一个分支时,git只是创建了一个可以移动的新的指针,使用git branch branch_name即可创建新的分支,此时新分支与master分支的指针均指向当前最后一次提交对象的位置,git还有一个特殊的指针HEAD来区分此时在哪一个分支
1220202229
如果想切换到某个分支,需要执行git checkout branch_name,这样HEAD指针就会指向你切换到的分支。也可以执行git branch -b branch_name,它表示创建一个分支并且切换到该分支上去,相当于git branch branch_name+git checkout branch_name。切换到指定分支后,所有代码修改提交都是在该分支上,每次提交当前分支指针后移,其他分支指针不动
1220202606
当开发完成自测通过后需要合并到master分支上,此时需要切换当前分支到master,然后执行git merge branch_name,这样就会把branch_name分支的改动合并到当前所在的master分支上,合并时可能会发生冲突,因为可能你某个时候改过master分支
1220203239
此时需要手动去处理冲突,处理完成后就是add->commit->push了。但是!但是!细想下这里是不是也可能有问题,你一直都在修改本地仓库,可能在你上一次git pull之后,别人又提交过新的修改到远程master分支了,这个时候怎么可能正常push。对头,这里我专门做了这个case的测试,发现git会报错
1220204127
黄色提示的很明白了,有人更新过,你需要再执行一次git pull才能正常push。但是!但是!这里是不是也可能会有冲突,别人提交的和你将要提交的冲突,pull会失败。亲测后发现是的,再手动解决冲突就好了
1220204434
所以要留心每次执行命令后的提示,有没有fail这个字眼。我用的vscode开发,发现安装各种git插件后简直香的飞起,冲突显示的也很友好,尤其是git提交日志,当前在哪个分支,远程分支是什么状态,改了哪些文件,分支合并状态一目了然。奥,还有add->commit也很方便。
1220205100
需要知道当前分支的提交状态或者树结构的话可以执行git log --oneline --decorate --graph --all,结果大概长这样(vscode真香)
1220205809
使用git branch可以看到当前的本地仓库都有哪些分支,我一般还会加-a参数,这样可以看到本地和远程的所有分支,--merged参数表示哪些分支已经合并了,对应的就有没有合并分支的参数--no-merged,合并过的分支如果不在需要就可以删除,删除本地仓库的分支使用git branch -d branch_name-D是强制删除,一般没有合并过的分支是不允许删除的,如果某个分支你只是用来测试或者瞎搞确实没什么用了就可以使用-D进行删除。删除远程仓库的分支使用git push origin -d branch_name

子模块

在项目中需要使用第三方库,这个库也是一个独立的项目时,最好在项目中引入git子模块。
在一个已经存在的git项目中需要添加子模块时,使用git submodule add project_url。此时会在当前目录下新建一个.gitmodules的配置文件,存储了拉取的项目存储到本地的目录与该项目的url地址的映射。

克隆含有子模块的项目时默认会包含该子模块目录,但是目录下没有任何文件,需要cd到子模块目录下执行git submodule init来初始化本地配置文件,然后git submodule update进行抓取子模块的所有数据。也有一种更简洁的方式,在clone时加入--recursive参数就可以省略后面的两步,它会自动初始化并更新仓库中的每一个子模块。

在使用子模块时,比较好的一个方法是将自己视为对子模块的代码只有只读权限,这也很好理解,你的项目引入的是一个第三方库,项目不止你一个人在维护,你改了第三方库对别人肯定会有影响。
我们当前的项目用到的子模块也是自己开发的,所以我一般在子模块初始化完成后的日常开发中只跑几条指令:

  • git submodule update --remote更新子模块
  • git status查看状态
  • git add filename
  • git commit -m "comand"
  • git push origin master
    push的时候如果有冲突了就git fetch->git merge origin/master一下,或者直接git pull。在这里我的位置是子模块对应的项目的开发者,当然要对子模块进行读写,如果你只是引入一个第三方库,就只需要第一条指令就可以,不要独自修改子模块的代码!