Liu Shouda coder

git入门

2017-05-21

本文是给新人培训的材料,适合对版本控制有所了解,但对git不够熟悉的人群。

本文介绍git的基本用法。大家心里先有一个问题,github上一个项目几百个人到底应该怎么管理?

git 基本知识

git工作流

基本的 Git 工作流程如下:

  1. 在工作目录中修改文件。
  2. 暂存文件,将文件的快照放入暂存区域。
  3. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

最常用命令:

git add .  //暂存
git commit -m 'xxx' //提交
git pull //拉取更新

目前位置,这几个操作与svn类似。

每次提交会有一个commit id. 通过git log可以查看提交历史。输出如下。

commit 2c29b790d069a46f07cba6a38eb380f4ccbd7635
Author: sooda <liushouda@qq.com>
Date:   Mon May 22 14:47:29 2017 +0800

    fix merge

commit 0f2830daa6ac8dc3b681bbd53a16a58534a55b0d
Merge: 1d7a343 4a906a4
Author: sooda <liushouda@qq.com>
Date:   Mon May 22 14:36:08 2017 +0800

    Merge branch 'master' into div

有了这个commid id某次提交的校验码。用前面4,5个字符基本上就可以唯一确定一个提交了。

有了这个id,任何时候都可以回退,查看这次提交的内容。

git checkout commid_id 检出某次提交,可以用于简单测试该提交状态下代码有没有问题,也可以从该提交重新切分支
git reset --hard commid_id 重置到某次提交

远端

git在本地控制实际上就是一个.git文件夹。把这个.git文件夹删掉之后,就跟git没啥关系了。

git是一个分布式版本控制。从任何一个副本都可以恢复整个提交历史。一个本地仓库可以跟踪多个远程仓库,

  • 通过git remote add 添加。
  • git remote -v 显示远程信息。
  • git fetch remote_name来拉取该远端。
  • git merge remote_name/branch_name来合并该分支。
  • git push remote_name branch_name来推送到某个远端。

默认来说,本地分支是master。默认跟踪的远端是origin。 也就是说本地分支branch_name下操作pull/push等操作都是相对于origin的。

gitlab,github

这些只是提供一个远程管理功能。本质上还是git。git服务器可以自己搭建,不一定要gitlab或者github

常用命令

ps: 后文都会有使用样例。这里不做解释。

git status

git add

git rm

git commit

git push -u origin branch_name

git fetch

git checout -b branch_name <origin/branch_name>

git branch

git show commid_id

git diff

git diff –staged

git remote -v

git log

git remote add

git remote set-url

git branch -D branch_name

实战

创建仓库

mkdir git_tutorial

git init

编辑 README

this is a simple demo showing how to use git in team work.

git add README

git commit -m ‘init commit’

这样就完成了第一次提交。

开始开发

接下来写一些简单代码

编辑 main.cpp

#include <iostream>
using namespace std;

int main() {
    std::cout << "hello git " << std::endl;
    return 0;
}

g++ main.cpp -o git_demo

./git_demo

完成hello git了,好棒 :)

提交一下吧。

git add main.cpp

git commit -m ‘hello git’

再增加一些功能。

├── main.cpp ├── Makefile ├── README ├── util.cpp └── util.h

util.h

int add(int a, int b);

util.cpp

#include "util.h"
int add(int a, int b) {
  return a + b;
}

main.cpp

#include <iostream>
#include "util.h"
using namespace std;

int main() {
    int a = 3, b = 5;
    int c = add(a, b);
    std::cout << a << " + " << b << " = " << add(a, b) << std::endl;
    std::cout << "hello git " << std::endl;
    return 0;
}

Makefile

git_demo : main.cpp util.cpp
	g++ main.cpp util.cpp -o git_demo

PS: 对Makefile不熟悉的不必着急。不一定要用Makefile, 使用Makefile之后,直接make等同于执行g++ main.cpp util.cpp -o git_demo,也可以将源码直接加到vs等IDE中,直接run就好了。下同。后面我们修改的主要是util.h, util.cpp, main.cpp这三个文件。不再特别指明。

make之后执行一下:./git_demo 输出如下:

3 + 5 = 8
hello git

提交吧。先git status看一下当前目录情况。

正常提交的话,需要一次次git add每个文件。比较繁琐。有没有办法一次add 多个文件呢。

可以add 整个目录,但是这里git_demo是个可执行文件,显然不是我们想要的。可以先add,再把不要的文件reset掉,但是每个提交都会有这个文件,也是很烦的事情。

一劳永逸的方法是,直接ignore这个文件。

编辑 .gitignore

git_demo

现在再用git status查看,就不会再出现这个文件了。

git add .

把这些文件进行暂存。

可以用git diff –staged查看一下,确保提交的内容没有问题。

git commit -m ‘add function’

分支

现在加入我们想要开发一个复杂的功能减法。我们开一个分支。为什么要开分支?让我们看几种情况再回来考虑。

git checkout -b minus

编写代码..

int add(int a, int b);
int minusFunc(int a, int b);
#include "util.h"

int add(int a, int b) {
   return a + b;
}

int minusFunc(int a, int b) {
    return a - b;
}
#include <iostream>
#include "util.h"
using namespace std;

int main() {
    int a = 3, b = 5;
    int c = add(a, b);
    std::cout << a << " + " << b << " = " << add(a, b) << std::endl;
    std::cout << a << " - " << b << " = " << minusFunc(a, b) << std::endl;
    std::cout << "hello git " << std::endl;
    return 0;
}

git add .

git commit -m ‘minus function’

这时候用git branch显示分支有:

master
minus

现在测试通过,需要将minus合并到master

git checkout master

git merge minus

git log 查看

commit 87eaf1e9f2e82aaa5b29e1aaa4e6f736d7b56d7d
Author: sooda <liushouda@qq.com>
Date:   Sun May 21 18:03:47 2017 +0800

    minus function

commit 7956201e1de99b7b371c895b22345e02118d383d
Author: sooda <liushouda@qq.com>
Date:   Sun May 21 17:19:51 2017 +0800

    add function

commit 07460d8b806d8b1e6090785f93e802a490ecdc79
Author: sooda <liushouda@qq.com>
Date:   Sun May 21 16:12:39 2017 +0800

    hello git

commit ce0bae17393d4e0a10b9d2987aef2977c955c186
Author: sooda <liushouda@qq.com>
Date:   Sun May 21 16:12:04 2017 +0800

    init commit

现在master提交历史上,就好像只有一个分支一样。

远程

创建项目

添加远程仓库:

git remote add origin git@gitlab.avatarworks.com:shouda/git_tutorial.git

提交代码

git push -u origin master

协作

开发人员b

现在加入有另一位成员b加入进来,一起开发multiply

git clone http://shouda@gitlab.avatarworks.com/shouda/git_tutorial.git

git checkout -b multiply

int add(int a, int b);
int minusFunc(int a, int b);
int multiFunc(int a, int b);
#include "util.h"

int add(int a, int b) {
   return a + b;
}

int minusFunc(int a, int b) {
    return a - b;
}

int multiFunc(int a, int b) {
    return a * b;
}
#include <iostream>
#include "util.h"
using namespace std;

int main() {
    int a = 3, b = 5;
    int c = add(a, b);
    std::cout << a << " + " << b << " = " << add(a, b) << std::endl;
    std::cout << a << " - " << b << " = " << minusFunc(a, b) << std::endl;
    std::cout << a << " * " << b << " = " << multiFunc(a, b) << std::endl;
    std::cout << "hello git " << std::endl;
    return 0;
}

git add .

git commit -m ‘multiply’

git push -u origin multiply (没有加-u下次需要加set-upstream,根据提示操作即可)

在gitlab(github)上提交合并请求。


gitlab merge

项目维护员点击: accept merge request即可合并到master分支


开发人员a

用户a在开发除法

git checkout -b ‘div’

编写代码,

int add(int a, int b);
int minusFunc(int a, int b);
int divFunc(int a, int b);
#include "util.h"

int add(int a, int b) {
   return a + b;
}

int minusFunc(int a, int b) {
    return a - b;
}

int divFunc(int a, int b) {
    return a / b;
}
#include <iostream>
#include "util.h"
using namespace std;

int main() {
    int a = 3, b = 5;
    int c = add(a, b);
    std::cout << a << " + " << b << " = " << add(a, b) << std::endl;
    std::cout << a << " - " << b << " = " << minusFunc(a, b) << std::endl;
    std::cout << a << " / " << b << " = " << divFunc(a, b) << std::endl;
    std::cout << "hello git " << std::endl;
    return 0;
}

写到一半(上面给出完整代码,实际上已经可以开始提交了。但在实际中肯定会碰到写一半的情况),有人说master代码有问题,需要修复一下(通常是有bug,现在假设我们想将add改成addFunc)。这时候怎么办。以前的做法是备份文件夹??现在肯定不需要。我们有分支!我们只要切回master分支,修改并提交后,再切回这个分支就好了。

说干就干。

做法一

git checkout master

git pull

报错

error: Your local changes to the following files would be overwritten by merge:
	util.cpp
	util.h
Please, commit your changes or stash them before you can merge.
Aborting

意思是,有些文件master的更新修改了util.h, util.cpp, 现在本地同样修改了这个两个文件,需要先把这个修改提交了,否则,无法拉取最新代码。(因为拉取新代码,可能是你本地未提交代码丢失)

git stash

正确做法

比较好的做法是将在原div分支上先提交代码,再切换分支。有时候,修改代码比较乱,或者涉及到一些本地环境变量,不想提交。那么,stash (贮存)就派上用场了。贮存的意思就是先把本地的修改存起来,还原一个干净的环境。

git stash

Saved working directory and index state WIP on div: 87eaf1e minus function
HEAD is now at 87eaf1e minus function

git checkout master

git pull (切分支最好从最新的master切出来)

git checkout -b ‘hotfix’

修改代码。。

int addFunc(int a, int b);
int minusFunc(int a, int b);
int multiFunc(int a, int b);
#include "util.h"

int addFunc(int a, int b) {
   return a + b;
}

int minusFunc(int a, int b) {
    return a - b;
}

int multiFunc(int a, int b) {
    return a * b;
}
#include <iostream>
#include "util.h"
using namespace std;

int main() {
    int a = 3, b = 5;
    std::cout << a << " + " << b << " = " << addFunc(a, b) << std::endl;
    std::cout << a << " - " << b << " = " << minusFunc(a, b) << std::endl;
    std::cout << a << " * " << b << " = " << multiFunc(a, b) << std::endl;
    std::cout << "hello git " << std::endl;
    return 0;
}

git add .

git commit -m ‘change add to addFunc’

git push origin hotfix

创建merge request,提交合并请求

现在可以切回div分支继续开发了。

git checkout div

恢复开发环境 git stash pop

这样一切都跟原来一样。git stash不仅仅用于这种合并的时候,任何时候想要还原干净环境都可以这样做。

继续编写代码,测试通过之后commit

git add .

git commit -m ‘div function’

合并代码

这时候似乎已经完成开发了。直接到gitlab提交合并请求?

虽然这也可以。但是,理我们从master开分支已经较长时间了,master很有可能已经改变了。我们现在最新的代码是否能够直接合并到master并不确定。所以,我们可以尝试将master合并到本分支,看我们的代码是否有冲突。

更新master代码: git fetch origin master:master

git merge master

ops! 冲突了。。

Auto-merging util.h
CONFLICT (content): Merge conflict in util.h
Auto-merging util.cpp
CONFLICT (content): Merge conflict in util.cpp
Auto-merging main.cpp
CONFLICT (content): Merge conflict in main.cpp
Automatic merge failed; fix conflicts and then commit the result.

接下来我们学习如何合并冲突。

用文本编辑器打开,util.h

int addFunc(int a, int b);
int minusFunc(int a, int b);
<<<<<<< HEAD
int divFunc(int a, int b); //当前分支的代码
=======
int multiFunc(int a, int b); //master分支的代码
>>>>>>> master

上面中文注释部分是新加的。这个冲突的意思是,你当前分支希望这一行是divFunc, master分支希望是multiFunc。git不知道到底听哪个好了。所以列出来由我们决定。

从我们业务逻辑出发,这两个都要啊。那就改下这个代码..

int addFunc(int a, int b);
int minusFunc(int a, int b);
int divFunc(int a, int b);
int multiFunc(int a, int b);

这样就算util.h的冲突解决好了。但是如何让git知道你已经解决好冲突了呢。答案是暂存。 git add util.h即可。

同样的方式解决main.cpp的冲突。

我们再来看一下util.cpp

#include "util.h"

int addFunc(int a, int b) {
   return a + b;
}

int minusFunc(int a, int b) {
    return a - b;
}

<<<<<<< HEAD
int divFunc(int a, int b) {
    return a / b;
=======
int multiFunc(int a, int b) {
    return a * b;
>>>>>>> master
}

mergetool

在这个例子中,手动修改一下很容易。如果更复杂的合并,则可能需要用到mergetool了。我们尝试在这里使用一下mergetool。在ubuntu下我们使用meld

sudo apt-get install meld    //安装
git config --global merge.tool meld  //设置为默认mergetool

git mergetool

弹出一个界面

左边表示你当前分支(div)的更改。右边表示被合并分支master的更改。中间表示你希望的合并结果。

你可以点击左边绿色的箭头,告诉mergetool说,这个函数是需要的。点击以后的结果是:

现在的情况表明,要么选中间的,要么选右边的。没办法兼容的样子。。

没关系,我们还有一招,中间那个文件是可以直接修改的。我们手动把右边的第三个函数copy到中间来,

这样看起来应该没错了(中间的合并结果比右边多了一个divFunc,比左边多一个multiFunc)。[ps: 注意上面代码没合并有问题,后面会有解决方案].

点击右边addFunc那个箭头。因为我们希望add替换成addFunc的。

按ctrl s保存后,关掉这个图形界面即可。如果有多个文件需要合并,会跳出新的合并页面。

make一下,看看是否编译通过,效果是否正确。正确之后就可以提交了。

git commit

跳出一个合并信息页面。一般不需要更改,直接保存退出就好了。

这样就完成了将master合并到div了

现在可以将div提交合并了。

git push origin div

提交代码之后,回到主分支继续工作

参与github项目

下载

假设一个场景,我们发现https://github.com/msracver/Deformable-ConvNets这个项目跟我们相关性很大。我们想把它下下来看看。

一种方法是直接下载源码zip,这个非常不建议的。因为人家项目还会继续更新,而下载源码zip则已经丢失git信息

正确的做法是先fork这个项目。

点击fork这个按钮。会生成在我们自己名下的一个仓库。比如: https://github.com/vsooda/Deformable-ConvNets

找个目录clone这个地址就好了。 git clone https://github.com/vsooda/Deformable-ConvNets

修改

下载下来,跑一跑发现这个项目真的挺牛逼的。但是有一些bug或者我们需要自己定制。

现在以修bug为例。

开个分支 git checkout -b fix_test

在README.md加上一行: just for test (只是测试,在实际中肯定要提交有效代码)

git add README.md

git commit -m ‘fix test’

git push -u origin fix_test

这样在我们github个人仓库下就可以看到我们自己的修改了。

同步upstream

在我们做这个更改的时候,原仓库可能又有一些更改了。我们需要及时更新我们仓库信息。

这就涉及到多个远端问题了。我们增加一个远端,命名为upstream (只是一个名字,其他名字也可以)

git remote add upstream https://github.com/msracver/Deformable-ConvNets.git

显示一下当前远端有哪些:

sooda@thinkpad:~/deep/Deformable-ConvNets$ git remote -v
origin	https://github.com/vsooda/Deformable-ConvNets.git (fetch)
origin	https://github.com/vsooda/Deformable-ConvNets.git (push)
upstream	https://github.com/msracver/Deformable-ConvNets.git (fetch)
upstream	https://github.com/msracver/Deformable-ConvNets.git (push)

抓取远端: git fetch upstream

切到主分支: git checkout master

合并原仓库更新到master: git merge upstream/master

切到fix_test分支: git checkout fix_test

合并本地master: git merge master

推送到github: git push

pull request

现在这个修改只是我们自己能看到。如果我们这个修改确实对整个项目有帮助,我们希望贡献出去,要怎么做呢。

答案是: pull request

在自己的仓库下,选中fix_test分支后,点击new pull request

进入一个新界面:

写一写这个pull request做了什么。点击create pull request提交。接下来就是等待作者的审核了。记得及时回复别人的问题。如果对方有什么修改意见,你直接在fix_test分支继续修改,直接推送上去就好了。不需要重新创建pull request。

更规范一些

参考这个文章

git rebase的使用 git rebase -i HEAD~n //合并多个提交 git rebase origin/master

总结

为什么要开分支

现在我们来考虑一下为什么要开分支。先考虑如果不开分支会怎么样:

  • 在开发一个功能的时候,我们代码甚至编译不通过。如果需要修复一个线上版本的bug, 我们没有办法快速响应。这时候可能需要复制文件夹。
  • 在开发一个功能的时候,如果最终这个功能被认定为不可用,那么这个代码放哪里呢?开个文件夹备份一下?
  • 在开发一个功能的时候,我们突然发现一个bug。想知道以前的版本有没有。如果不开分支,又要文件夹备份?

开分支可以认为就是文件夹备份。但是比文件夹备份牛逼的地方在于,他还能合并!我们用文件夹来做只能是替换文件。而无法合并不同修改。

使用原则

  • 一个分支只干一件事情。

    ​如果多个功能都在一个分支开发。可能相互代码之间会有影响,如果出问题不好查找。如果其中一个功能用不了,整个分支无法合并到主分支。这时候就很麻烦了。

  • 鼓励多提交

    在git中,因为有commid id的存在,只要你提交过代码,就不用担心代码会丢失。可以在开发一个小功能之后就提交代码,小步快跑。最后提交的时候可以合并多个小提交。

    一次git提交相当于一个承诺:这次提交我完成了xxx功能,这次提交应该可以正常执行。有了这个保证之后,将来代码有问题,很容易就可以从提交历史回溯,看看哪次提交造成的。

  • 写好提交信息

    版本控制的目的就是为了方便查看历史。哪天发现你的代码出问题了,一看提交历史发现是某某某改的。提交信息为空,不写理由。这时候你去问他为什么这么改,他估计也忘记了。所以写好提交信息很重要。

  • 临时更改不要提交

    对于本地的一些环境变量,文件名路径名这种不要提交。

  • 大的二进制文件不要提交

    git无法区分二进制文件中的区别。只能全部替换。二进制文件会使得git急剧变大

作业

  1. 重现以上实战步骤
  2. 在https://github.com/vsooda/git_tutorial.git 上任意开发一个小函数,提交pull request,最后成功被合并。

高级话题

这部分涉及的东西很多,未完待续。

修改提交

参考这里

git rm main.cpp

git commit –amend

保存即可。

如果想在一个提交增加某些文件,同样可以进行该操作。

注意,上面的rm操作是真的会删除文件的。但是即使文件被删除了也没有关系,只要提交过了,本地会有记录的,通过git reflog可以查看之前的id,甚至把它checkout出来

搜索

来自这里

Git 日志搜索

或许你不想知道某一项在 哪里 ,而是想知道是什么 时候 存在或者引入的。 git log 命令有许多强大的工具可以通过提交信息甚至是 diff 的内容来找到某个特定的提交。

例如,如果我们想找到 ZLIB_BUF_MAX 常量是什么时候引入的,我们可以使用 -S 选项来显示新增和删除该字符串的提交。

$ git log -SZLIB_BUF_MAX --oneline
e01503b zlib: allow feeding more than 4GB in one go
ef49a7a zlib: zlib can only process 4GB at a time

如果我们查看这些提交的 diff,我们可以看到在 ef49a7a 这个提交引入了常量,并且在 e01503b 这个提交中被修改了。

如果你希望得到更精确的结果,你可以使用 -G 选项来使用正则表达式搜索。

行日志搜索

行日志搜索是另一个相当高级并且有用的日志搜索功能。 这是一个最近新增的不太知名的功能,但却是十分有用。 在 git log 后加上 -L 选项即可调用,它可以展示代码中一行或者一个函数的历史。

例如,假设我们想查看 zlib.c 文件中git_deflate_bound 函数的每一次变更,我们可以执行 git log -L :git_deflate_bound:zlib.c。 Git 会尝试找出这个函数的范围,然后查找历史记录,并且显示从函数创建之后一系列变更对应的补丁。


上一篇 mxnet graph解析

下一篇 ncnn jni

Content