Git——多人协作

会一些基础的操作了,现在通过一个不太复杂的例子来看看如何进行多人协作。

一般情况下,多人协作需要至少一台机当做服务器,无论是在本机、局域网或互联网上都可以。Git 服务器一般是由管理员搭建的。我们现在将会在局域网内的一台机器上搭建一个 Git 仓库。

设置一个 Git 仓库

非管理员可以跳过本节,无需设置服务器,直接进行多人协作。

先登陆上要设置仓库的机器,然后添加一个 git 用户,供其他用户访问:

1
$ sudo adduser git

为了不用每次都输入密码,以及更高的安全性,可以将要访问的用户的 ssh pub key 放到 /home/git/.ssh/authorized_keys,一个人一行。

设置 ssh key

想要查看现有的 key,在本地机器(不是服务器)执行 ls -al ~/.ssh。然后将 authorized_keys 文件或者 .pub 结尾的文件中的内容添加进服务器的 /home/git/.ssh/authorized_keys 中。

如果还没有 pub key,可以新创建一个:

1
2
3
4
5
6
// 替换邮箱
$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
// 然后输入要保存的文件,一般是直接回车就好
Enter file in which to save the key:
// 这里是设置一个密码,为了以后不用每次都输入密码,所以留空,直接回车
Enter passphrase

创建好之后,就是把 pub key 推送到服务器了。在本地执行:

1
2
// 替换服务器地址
$ ssh git@xxx.xxx 'mkdir .ssh && chmod 700 .ssh && cat >> ~/.ssh/authorized_keys' < ~/.ssh/id_rsa.pub

或者将这个 id_rsa.pub 文件发给管理员添加。

禁止 git 用户登录

为了安全性,可以限制 git 用户登录 shell。在服务器上,编辑 /etc/passwd,找到:

1
git:x:1001:1001:,,,:/home/git:/bin/bash

改为:

1
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

在禁用 git 用户登录后,上面的 key 将无法推送,可以在搜集完 key 后再禁用。

创建仓库

在服务器上,进入放仓库的目录:/home/git,然后初始化一个仓库:

1
2
3
4
// 自行更换仓库名
$ sudo git init --bare name.git
// 将仓库的所有者改为 git
$ sudo chown -R git:git name.git

到这里,一个简单的 Git 仓库就已经设置完成了。

多人协作

接下来就是激动人心的多人协作了。

现在可以将仓库克隆下来,开始多人协作:

1
2
// 自行更改服务器地址以及仓库名
$ git clone git@192.168.2.103:test.git

这样就克隆了一个空的仓库,然后可以在里面开始工作了。

或者,如果你在本地已有了一个仓库,想要链接这个远程仓库,可以设置:

1
2
3
// 自行更改服务器地址以及仓库名
$ git remote add origin git@192.168.2.103:test.git
$ git push -u origin master

git remote add 命令可以将服务器地址添加到当前的仓库里,便于将现在的仓库推送到服务器,origin 表示远程服务器。

-u origin master 是指定操作时默认的远程分支。

这样本地仓库就链接上远程仓库,并且将所有版本记录都上传上去了。

多人协作实例

现在将通过一个不太难的例子来演示多人协作。

有两个人,小明和小红。一起共同开发一个小程序,服务器已经设置好了。

克隆仓库

小明将这个仓库克隆下来:

1
$ git clone git@192.168.2.103:random.git

将会提示这是一个空仓库,所以小明创建了一个 random.c 文件并开始了编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

int random_int(int max)
{
return rand() % max;
}

int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "使用: %s <数字>\n", argv[0]);
return EXIT_FAILURE;
}

int max = atoi(argv[1]);

int result = random_int(max);
printf("%d\n", result);

return EXIT_SUCCESS;
}

编译一下看看能不能正常运行:

1
2
3
4
5
$ gcc -std=c99 random.c
$ ./a.out
使用: ./a.out <数字>
$ ./a.out 10
3

提交更改

很好,现在将 random.c 添加到仓库:

1
$ git add random.c

然后看看操作的状态是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git status 
位于分支 master

尚无提交

要提交的变更:
(使用 "git rm --cached <文件>..." 以取消暂存)

新文件: random.c

未跟踪的文件:
(使用 "git add <文件>..." 以包含要提交的内容)

a.out

有个 a.out 文件,这是一个可执行文件。作为编译生成的文件是不应该存储在版本库中的,所以小明创建一个名为 .gitignore 的文件:

1
2
# 可执行文件
*.out

再将 .gitignore 添加进版本控制里面:

1
$ git add .gitignore

再次查看状态,已经没有 a.out 文件了:

1
2
3
4
5
6
7
8
9
10
$ git status 
位于分支 master

尚无提交

要提交的变更:
(使用 "git rm --cached <文件>..." 以取消暂存)

新文件: .gitignore
新文件: random.c

接下来就是提交更改了:

1
git commit -m "最初版"

上传更改

将更改提交到仓库后,小明打算上传到服务器上供小红继续开发:

1
git push

检查历史

现在轮到小红克隆这个版本库了:

1
2
$ git clone git@192.168.2.103:random.git
$ cd random

小红想要先看看提交日志,所以她使用了 log 命令:

1
2
3
4
5
6
$  git log 
commit 3f59af20a7621f9d670da046da6f805464f6a379 (HEAD -> master, origin/master, origin/HEAD)
Author: 小明 <小明@gmail.com>
Date: Fri Mar 30 10:53:59 2018 +0800

最初版

经过一番检查和调试,小红发现这个随机数生成器从不初始化,每次生成的都是一样的数字。小红进行了一点修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int random_int(int max)
{
return rand() % max;
}

int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "使用: %s <数字>\n", argv[0]);
return EXIT_FAILURE;
}

int max = atoi(argv[1]);

srand(time(NULL));
int result = random_int(max);
printf("%d\n", result);

return EXIT_SUCCESS;
}

进过一番调试,每次生成的都不一样了,可以进行提交了。看看需要什么操作:

1
2
3
4
5
6
7
8
9
10
11
$ git status 
位于分支 master
您的分支与上游分支 'origin/master' 一致。

尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git checkout -- <文件>..." 丢弃工作区的改动)

修改: random.c

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"

git 已经知道 random.c 被修改了。小红想要通过 diff 命令查看更详细的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ git diff
diff --git a/random.c b/random.c
index b831e09..636b28c 100644
--- a/random.c
+++ b/random.c
@@ -1,5 +1,6 @@
#include <stdio.h>
#include <stdlib.h>
+#include <time.h>

int random_int(int max)
{
@@ -16,6 +17,7 @@ int main(int argc, char *argv[])

int max = atoi(argv[1]);

+ srand(time(NULL));
int result = random_int(max);
printf("%d\n", result);

现在可以添加、提交并且推送到服务器了:

1
2
3
git add random.c
git commit -m "初始化随机数"
git push

重命名和移动文件

小明打算重构一下,所以决定将所有源码文件移动到 src/ 目录下:

1
2
3
4
5
6
7
8
9
10
11
$ mkdir src
$ git mv random.c src/
$ git status
位于分支 master
您的分支与上游分支 'origin/master' 一致。

要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)

重命名: random.c -> src/random.c
$ git commit -m "重构文件"

然后小明决定为项目创建一个 MakefileREADME 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat Makefile
all:: random

CC=gcc
CFLAGS=-std=c99 -Wall -Wextra

random:
$(CC) $(CFLAGS) src/random.c

clean:
$(RM) *.o random

.PHONY: all clean

$ cat README
随机数生成
==========

在给定的最大数内生成随机整数。

然后将这两个文件添加并提交:

1
2
$ git add Makefile README
$ git commit -m "添加 Makefile 和 README 文件"

然后小明决定将 random.c 文件重命名为 rand.c

1
git mv src/random.c src/rand.c

同时,Makefile 也需要更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ git diff
diff --git a/Makefile b/Makefile
index 41c6682..597d818 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,12 @@
-all:: random
+all:: rand

CC=gcc
CFLAGS=-std=c99 -Wall -Wextra

-random:
- $(CC) $(CFLAGS) src/random.c
+rand:
+ $(CC) $(CFLAGS) src/rand.c

clean:
- $(RM) *.o random
+ $(RM) *.o rand

.PHONY: all clean

$ git add Makefile
$ git commit -m "重命名文件"

更新存储库(合并)

提交完以后,小明准备推送这些更改到服务器:

1
2
3
4
5
6
7
8
$ git push
To 192.168.2.103:random.git
! [rejected] master -> master (fetch first)
error: 无法推送一些引用到 'git@192.168.2.103:random.git'
提示:更新被拒绝,因为远程仓库包含您本地尚不存在的提交。这通常是因为另外
提示:一个仓库已向该引用进行了推送。再次推送前,您可能需要先整合远程变更
提示:(如 'git pull ...')。
提示:详见 'git push --help' 中的 'Note about fast-forwards' 小节。

推送失败,这是因为小红已经先推送了一个版本。小明需要先将小红推送的内容拉取下来(需要添加提交信息):

1
2
3
4
5
6
7
8
9
10
11
$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
展开对象中: 100% (3/3), 完成.
来自 192.168.2.103:random
3f59af2..4f42481 master -> origin/master
自动合并 src/rand.c
Merge made by the 'recursive' strategy.
src/rand.c | 2 ++
1 file changed, 2 insertions(+)

pull 命令是获取服务器上最新的版本,并且会自动与本地的版本合并,并且提交合并。

1
2
3
4
5
6
7
8
9
$ git show
commit dd5316ec3e192129a9283eaf38208a15ce0b070f (HEAD -> master)
Merge: 5616d0c 4f42481
Author: 小明 <小明@gmail.com>
Date: Fri Mar 30 12:35:15 2018 +0800

Merge branch 'master' of 192.168.2.103:random

合并小红的版本

show 命令是显示当前的提交。看上去像是合并成功了,并且没有任何问题。

创建标签

小红和小明决定发布这个项目,所以小明创建了一个标签,以便可以更好的访问、引用发布的版本。

1
2
3
4
5
6
7
8
9
10
11
$ git tag -a -m "random v0.1" v0.1
$ git tag --list
v0.1
$ git log -1
Merge: 5616d0c 4f42481
Author: 小明 <小明@gmail.com>
Date: Fri Mar 30 12:35:15 2018 +0800

Merge branch 'master' of 192.168.2.103:random

合并小红的版本

现在,这个标签仅在小明的本地库中,接下来就是把所有更改全都推送到服务器上:

1
2
3
4
5
6
7
8
9
$ git push origin tag v0.1
对象计数中: 15, 完成.
Delta compression using up to 8 threads.
压缩对象中: 100% (12/12), 完成.
写入对象中: 100% (15/15), 1.70 KiB | 579.00 KiB/s, 完成.
Total 15 (delta 1), reused 0 (delta 0)
To 192.168.2.103:random.git
* [new tag] v0.1 -> v0.1
$ git push

小红更新仓库以获取 v0.1 标签,并从最新的版本库继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git pull
remote: 对象计数中: 15, 完成.
remote: 压缩对象中: 100% (12/12), 完成.
remote: Total 15 (delta 1), reused 0 (delta 0)
展开对象中: 100% (15/15), 完成.
来自 192.168.2.103:random
6e6e562..05db770 master -> origin/master
* [新标签] v0.1 -> v0.1
更新 6e6e562..05db770
Fast-forward
Makefile | 12 ++++++++++++
README | 4 ++++
random.c => src/rand.c | 0
3 files changed, 16 insertions(+)
create mode 100644 Makefile
create mode 100644 README
rename random.c => src/rand.c (100%)

解决合并冲突

小红现在决定将一个伪随机数生成器的初始化提取到一个单独的方法中,这样初始化和生成随机数都被封装起来,让未来的修改更加容易。添加 init_rand()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ git diff
diff --git a/src/rand.c b/src/rand.c
index 636b28c..fec25cb 100644
--- a/src/rand.c
+++ b/src/rand.c
@@ -2,6 +2,11 @@
#include <stdlib.h>
#include <time.h>

+void init_rand(void)
+{
+ srand(time(NULL));
+}

int random_int(int max)
{
return rand() % max;
@@ -17,7 +22,7 @@ int main(int argc, char *argv[])

int max = atoi(argv[1]);

- srand(time(NULL));
+ init_rand();
int result = random_int(max);
printf("%d\n", result);

$ git add src/rand.c
$ git commit -m "提取随机生成器的初始化"

与此同时,小明发现 rand() 函数的文档说此方法是一个弱伪随机生成器。所以他决定添加注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git diff
diff --git a/src/rand.c b/src/rand.c
index 636b28c..c18c11e 100644
--- a/src/rand.c
+++ b/src/rand.c
@@ -2,6 +2,7 @@
#include <stdlib.h>
#include <time.h>

+// 考虑使用更好的随机生成器
int random_int(int max)
{
return rand() % max;

$ git add src/rand.c
$ git commit -m "添加注释"
$ git push

这时候,小红也准备推送了,但是被拒绝:

1
2
3
4
5
6
7
8
$ git push
To 192.168.2.103:random.git
! [rejected] master -> master (fetch first)
error: 无法推送一些引用到 'git@192.168.2.103:random.git'
提示:更新被拒绝,因为远程仓库包含您本地尚不存在的提交。这通常是因为另外
提示:一个仓库已向该引用进行了推送。再次推送前,您可能需要先整合远程变更
提示:(如 'git pull ...')。
提示:详见 'git push --help' 中的 'Note about fast-forwards' 小节。

小明已经推送了一个版本,小红必须先 pull 拉取到最新的版本再合并,才可以继续推送:

1
2
3
4
5
6
7
8
9
10
$ git pull
remote: 对象计数中: 4, 完成.
remote: 压缩对象中: 100% (3/3), 完成.
remote: Total 4 (delta 2), reused 0 (delta 0)
展开对象中: 100% (4/4), 完成.
来自 192.168.2.103:random
05db770..e20781e master -> origin/master
自动合并 src/rand.c
冲突(内容):合并冲突于 src/rand.c
自动合并失败,修正冲突然后提交修正的结果。

这次合并失败了,Git 无法自动合并两人的更改。提示有冲突,所以小红决定在编辑器中打开 src/rand.c 文件来检查:

1
2
3
4
5
6
7
8
9
<<<<<<< HEAD
void init_rand(void)
{
srand(time(NULL));
}

=======
// 考虑使用更好的随机生成器
>>>>>>> e20781e46d20ffcd2b79c5c537823ffc9deaa102

Git 同时将小红的代码(<<<<<<<======= 之间)和小明的代码(=======>>>>>>> 之间)都包含了。只有两个代码块分开时自动合并才会成功,因为没有分开,所以这次合并失败了。小红的 init_rand() 函数可以在小明的注释之前,所以小红这样更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git diff
diff --cc src/rand.c
index fec25cb,c18c11e..0000000
--- a/src/rand.c
+++ b/src/rand.c
@@@ -2,11 -2,7 +2,12 @@@
#include <stdlib.h>
#include <time.h>

+void init_rand(void)
+{
+ srand(time(NULL));
+}
+
+ // 考虑使用更好的随机生成器
int random_int(int max)
{
return rand() % max;

这样应该就解决完问题了,然后再添加并提交:

1
2
3
$ git add src/rand.c
$ git commit -m "合并: init_rand() + 注释"
$ git push

push 已经没有任何问题了。

如果你喜欢我的博客,那就请我吃冰淇淋吧(づ ̄3 ̄)づ╭❤~