Skip to content

Latest commit

 

History

History
658 lines (467 loc) · 31.7 KB

05.rst

File metadata and controls

658 lines (467 loc) · 31.7 KB
Authors: Kenneth Lee
Version: 0.1
Date: 2022-08-30
Status: Draft

Makefile和git

:index:`Makefile`

Makefile

这一章,我故意放到最后面。希望你敲键盘敲烦了再说。

首先我们得说,作为程序员,把键盘练熟,这是必须的,这个不能跳过去,靠拷贝粘贴, 会让你失去对“重复”的厌恶,老把自己当机器来用。你是人,是教机器干活的人,不是 自己去当机器,所以,如果你发现有重复的事情,就应该让它变成一个逻辑,写在文本 中,而不需要每次都重新执行。

为此,你个人也得敲键盘敲得飞快,千万不要当那种只用两个手指的程序员。由于你天天 要写程序,练习量是不用担心的,你只要保证你的姿势是5个手指正确按键的,过一段时间 就不会有任何问题了。倒不用专门去练习。

好了,我们现在开始解释一下Makefile和git是什么,以便你建立“工程”的概念。

我们来举个例子,你有一个程序,分成三个cc文件,比如是main.cc,foo.cc,bar.cc。你 全部链接在一起,可以这样::

g++ main.cc foo.cc bar.cc -o myapp

Note

至于为什么一定要写成多个文件,不能写成一个。这个把程序写复杂一点就会知道,好 比你现在调用的cout,如果它的代码也写到你的.c里面,你很不好管理吧?所以你的程 序写大了,那些公共的基础功能,你也不会想都放在一个文件里面的,这很难管理。

这种事情,对于程序员来说,不可能每次都要敲一次,最简单的做法,是写成一个脚本::

vim compile.sh

#!/bin/sh
g++ main.cc foo.cc bar.cc -o myapp

这样,你每次运行这个compile.sh就可以了。不用敲那么多字了。这就是我前面说的,只 要重复的东西,就要变成“程序”(脚本也是程序)。

你看,现在我们手上有这些文件:

  • compile.sh
  • main.cc
  • foo.cc
  • bar.cc
  • myapp

如果要把这些文件都保留起来,拷贝给老师,或者自己保存起来,我们可以放到同一个目 录中,然后压缩成一个rar文件,或者一个zip文件,拷贝到U盘上保存起来,这样就可以了。

这个包含所有这些文件的目录或者压缩文件,就叫一个工程。它包含了我们人头脑创造的 所有成果。

但等等,这个说法不太准确,myapp不是我们人脑的创造,它是编译器的创造。我们还是把 myapp删除,前面四个才是这个工程的一部分。

我们再考虑一下,如果我们后面修改了一下foo.cc,运行compile.sh,好像也有点浪费, 因为我们只修改了了一个文件,但编译器却编译了3个文件。这很多余。

现在我们可以解释一个新的工具了,这个工具叫make,它是另一种脚本,这个脚本专门解 决编译的问题。

我们把那个compile.sh换成一个Makefile。它是这样写的::

myapp: main.o foo.o bar.o
  g++ main.o foo.o bar.o -o myapp

main.o: main.cc
  g++ -c main.cc -o main.o

foo.o: foo.cc
  g++ -c foo.cc -o foo.o

bar.o: bar.cc
  g++ -c bar.cc -o bar.o

这样写完以后,你再执行make这个命令,就会你修改了哪个cc文件,就只编译那个文件了。

Note

在第二章我们介绍过gcc/g++,它其实不是编译器,而是一个集成编译器和链接器的一 个“外壳”。所以,你直接做g++ main.cpp foo.cpp bar.cpp它会自动做一个个程序的编 译,然后再和你需要用到的库链接在一起。但gcc/g++并没有make这种自动新旧的能力, 所以在上面的编译中,我们把编译的过程独立出来,用g++ -c命令,这样得到的就是一 个编译的中间文件(.o),到最后我们采用不带-c参数的g++把所有.o和库(比如你用 到的cout)这些东西链接在一起。

认真看看这个文件,它其实是另一种形式的脚本。一般脚本是一个线程,顺序一个命令一 个命令执行下来的。而Makefile是一个“有条件的脚本”。

它描述的意思是:先比较一下myapp和main.o foo.o bar.o,如果后面三个文件比myapp新, 就执行下面的命令。这叫myapp“依赖”main.o, foo.o, 和bar.o。然后我们又知道main.o依 赖main.c,所以如果main.c比main.o新,就说明在编译main.o之后,你又修改了main.cc, 所以要重新运行g++ -c main.cc -o main.o这个命令,重新生成main.o,这样,main.o就 是最新的,main.o变新了以后,它肯定比myapp新,所以g++ main.o foo.o bar.o -o myapp会被重新运行,重新生成这个myapp文件。

这样,如果你仅仅修改了main.cc这个文件,就只需要执行两个命令::

g++ -c main.cc -o main.o
g++ main.o foo.o bar.o -o myapp

这就是Makefile的作用,如果你写了几百个文件,这样能让你编译的时间短很多,因为你 不用每次都编译所有的文件。

make程序解释这个Makefile的时候,先把所有的文件的比较关系(严格的说法叫“依赖”) 理出来,然后一层层解释下去,按上面的比较策略找出谁被修改了,然后决定重新编译哪 部分程序。很多图形工具(IDE工具)能帮你自动生成这个Makefile,但你还是需要知道原 理,而且,以后你要写复杂的程序,自动生成的Makefile是不够用的,很多时候你还需要 自己写。现在还有很多语法更灵活的完成这种make功能的工具(比如CMakefile,meson等 ),但原理都是类似的,你学会了基本的Makefile,那些工具就是个类比的问题。Makefile 是解决这个问题的基本算法逻辑。

这其实也是把大程序分成很多文件的一个理由:如果你所有函数都写在一个文件里面,修 改了任何一个,编译器都要编译所有的代码,但分开了,你只是编译修改过的那个文件。

写在不同的文件中,编译的时候只编译这个文件,但编译器怎么知道你要用其他文件呢? 我举个例子,你的main.cc里面定义了一个变量,在bar.cc里面要用,怎么办呢?:

//main.cc                 |        //bar.cc
int a;                    |        int b = a + 1;
...                       |        ...

你的a的定义写在main.cc中,但bar.cc里面要用它,那么你前面做g++ -c bar.cc -o bar.o怎么知道这个a是什么类型的,内存在哪里呢?

C/C++解决这个问题的方法是“声明“,就是你要用其他文件的数据或者函数,你自己说它是 什么类型的。比如前面的程序,我们可以这样写::

//main.cc                 |        //bar.cc
int a;                    |        extern int a;
...                       |        int b = a + 1;
                          |        ...

先在bar.cc知道a是什么类型的了。但它还是不知道a的地址在什么地方,这个不要紧,我 们前面提过了,链接器负责把多个单独的文件连在一起,连在一起的时候,就知道了,到 时链接器负责把这里的汇编修改一下就可以了。

程序这样写就可以了。但其实很不方便。比如说,如果foo.cc也要用这个a怎么办呢?你又 要写一次,而且如果main.cc是你同学写的,bar.cc是你写的,她那边把int a修改成了 unsigned int a。你这里声明成了int。这个链接器是不知道的,因为链接的时候都是内存 地址,你说你是当int来解释,它就是int,结果你不是int,那解释就错了。

所以,最好这个extern的声明,都让写main的人写,因为她才知道怎么写才是对的。这就 是“头文件”的作用, 你另外写个文件,里面放这句extern int a,谁要这个声明,就放一 份这个文件在自己的文件里面就行了。C/C++提供一个语法,让你包含另一个文件进来。像 下面这样::

//main.cc                 |        //bar.cc
int a;                    |        #include "main.hh"
...                       |        int b = a + 1;
                          |        ...

这个main.hh就叫“头文件”,用来放那些extern语句用的。其实编译器不管你叫什么名字, 你叫xxx.hh也行,叫xxx.inc也行,直接叫xxx.c都行,反正只是找到这个文件,里面有什 么就都当作bar.cc的一部分来用就行了。这样我们就又消除了一部分“重复”了。

不过,一般C里面头文件都叫.h,C++有叫.hh的,也有叫.hpp的,我们一般还是按规矩写的 好。

#include这个语法,也有几种形式,上面那种写法是C和C++都支持的。还有一种写法是这 样的::

#include <iostream.hh>

这表示这个文件不是从当前目录找,从编译器自己默认的目录找(一般用来找系统自己的 库的头文件),具体怎么找的,你自己看手册。或者用编译器的-v参数编译程序,它会告 诉你怎么找的。还可以用参数(-I)强行指定编译器怎么找。

C++还有一个写法,可以省略扩展名,比如这样::

#include <iostream>

这样,无论那个文件是.hpp还是.hh,编译器都能找到。

头文件会给Makefile制造很多麻烦。我们假定我们有一个main.hh,然后foo.cc和bar.cc都 要用它。假定我修改了main.hh,按上面的Makefile的规则,foo.o是不会重新编译的,因 为foo.o还是比foo.cc新啊。所以,正确的写法得是这样::

myapp: main.o foo.o bar.o
  g++ main.o foo.o bar.o -o myapp

main.o: main.cc main.hh
  g++ -c main.cc -o main.o

foo.o: foo.cc main.hh
  g++ -c foo.cc -o foo.o

bar.o: bar.cc main.hh
  g++ -c bar.cc -o bar.o

这样修改了main.hh才会重新编译那些包含了main.hh的文件。但这样写确实很麻烦,因为 如果main.hh中又包含了另一个.hh呢?你怎么找得齐所有头文件呢?

gcc/g++可以帮你自动生成这个依赖关系,但那个就复杂了,我们重点学原理,所以我们不 深究下去,这个事情我们以后再说。现在这个阶段,如果出现这种情况,你把这些.o啦, myapp啦,都删掉,然后重新make,就没有问题了。

为此,我们再学习一下Phony依赖。make命令运行的时候,用Makefile的第一个依赖作为目 标依赖。也就是说,你运行make,他就看myapp有多少依赖,保证myapp是最新的就行。如 果你不想编译myapp,只想要foo.o,那么你可以运行make foo.o,这样,需要生成的目标 就变成foo.o,依赖就按它来算了。

但假设,我们需要做一个动作,这个动作不是为了生成某个个文件,我们只是想运行一个 或者几个命令,这种情况怎么办呢?那么我们可以创建一个Phony依赖(Phony是假的意 思),比如我们可以这样写::

myapp: main.o foo.o bar.o
  g++ main.o foo.o bar.o -o myapp

main.o: main.cc main.hh
  g++ -c main.cc -o main.o

foo.o: foo.cc main.hh
  g++ -c foo.cc -o foo.o

bar.o: bar.cc main.hh
  g++ -c bar.cc -o bar.o

.PHONY: clean

clean:
      rm -rf *.o
      rm -rf myapp

这个clean就是phony依赖,并不存在clean这个文件,只是你运行make clean的时候,它不 管三七二十一,直接运行后面那几个删除命令而已。用这种方法,你不需要写很多个脚本, 所有这些工程有关的脚本,都写在Makefile里面,要生成哪个目标,就make那个目标就可 以了。

Makefile的基本知识基本上就这些,我们这里只讲原理,深入的,等你有兴趣了,就去看 Makefile的手册,比如这个: GNU Makefile Manual

现在,简单几个文件,写成这样就可以了。或者我们可以多了解一个用来消除重复的语法:宏。

我们说过,软件很大程度上要做的工作是消除重复,把重复的事情交给计算机,自己做不 重复的事情。

上面这个例子里面就有很多重复的东西,比如这个g++ -c之类的,这些重复的字,每次都 要该的,我们都可以写成一个统一的名字,这样修改起来就简单一些,比如上面的例子, 我们可以写成这样::

LINKER=g++
COMPILER=g++ -c
ALL_O_FILES=main.o foo.o bar.o
APP=myapp

$(APP): $(ALL_O_FILES)
  $(LINKER) $(ALL_O_FILES) -o $(APP)

main.o: main.cc main.hh
  $(COMPILER) main.cc -o main.o

foo.o: foo.cc main.hh
  $(COMPILER) foo.cc -o foo.o

bar.o: bar.cc main.hh
  $(COMPILER) -c bar.cc -o bar.o

.PHONY: clean

clean:
      rm -rf $(ALL_O_FILES)
      rm -rf $(APP)

这个Makefile就容易修改多了,如果你要把你的程序从myapp修改成selina_s_best_work, 你修改一下APP的定义就可以了。这种用一个名字替换另一个名字的方法就叫“宏”,经过 这段时间的学习,你应该也注意到了,C/C++也支持“宏”。这种替换,主要有两个作用:

  1. 像前面说的,消除重复
  2. 它相当于做了一个注释,比如g++ -c你不容易记住这个参数是什么意思吧(特别是以后 有很多参数的时候)?但如果它被定义成了COMPILER这个名字,你就很容易知道它什么 意思了。

实际上,Makefile有很多默认的宏,比如,每个依赖的目标和依赖对象都可以用宏表示, 比如对于main.o: main.cc main.hh这个依赖:

  1. $@表示目标,@就是一个目标的形状,表示这里的main.o
  2. $<表示第一个输入,<是一个输入的形状,表示这里的main.cc
  3. $^表示全部输入,^是一个全部的形状,表示这里的main.cc main.hh

这样,前面的Makefile就可以写得更简单,比如这样::

LINKER=g++
COMPILER=g++ -c
ALL_O_FILES=main.o foo.o bar.o
APP=myapp

$(APP): $(ALL_O_FILES)
  $(LINKER) $(ALL_O_FILES) -o $(APP)

main.o: main.cc main.hh
  $(COMPILER) $< -o $@

foo.o: foo.cc main.hh
  $(COMPILER) $< -o $@

bar.o: bar.cc main.hh
  $(COMPILER) $< -o $@

.PHONY: clean

clean:
      rm -rf $(ALL_O_FILES)
      rm -rf $(APP)

这个其实还是有重复,make有其他语法让你消除它们的:比如下面是一个更深层次的重复 消除::

LINKER=g++
COMPILER=g++ -c
ALL_O_FILES=main.o foo.o bar.o
APP=myapp

$(APP): $(ALL_O_FILES)
  $(LINKER) $(ALL_O_FILES) -o $(APP)

main.o: main.cc main.hh
foo.o: foo.cc main.hh
bar.o: bar.cc main.hh

%.o: %.cc
  $(COMPILER) $< -o $@

.PHONY: clean

clean:
      rm -rf $(ALL_O_FILES)
      rm -rf $(APP)

我这里只是说原理,就到此为止吧。编程基本上我们都是先学基本原来,然后看实际的代 码,看到一个新的语法糖,就去了解它背后的原理,慢慢慢慢经验多了,我们就“学会”这 门语言了。这和我们学英语,学法语的原理是一样的。

:index:`git`

git

最后我们学习关于“工程”的最后一个辅助工具,git。

git是一种管理一组文件的修改的工具。前面我们已经有了一组文件:

  1. main.cc
  2. main.hh
  3. foo.cc
  4. bar.cc
  5. Makefile
  6. 其他脚本

反正你写的任何创造,它们都是文本文件,里面都包含了你的创造,你的智慧。你会担心 丢了,会担心改错了。

所以你要备份,比如你花了一天,写了一个计算3次多项式的函数,里面还有几个子函数, 写在几个文件中。第二天,你打算把它修改一下,变成支持n次多项式的函数,你想好一个 算法,然后你就开始改改这个文件,改改那个文件,改了一整天,发现改错了,但当初那 个计算3次多项式的程序也不能用了。现在你手上什么都没有,这是不是很痛苦的一件事?

程序员没日没夜工作,就是为了得到一堆文本文件,这些文本文件不但需要写,还需要经 过很长时间的“调试”,这里改几句,那里改几句,得到一个没有错误的组合。一旦改错了, 就什么都归零了。这完全无法接受。

所以,过去很多程序员在修改一个调试好,可以正常工作的程序前,都会全部文件都拷贝 到另一个地方,如果今天修改错了,那至少还可以把今天的工作放弃掉,留着昨天的结果。

这种一个能工作的代码文件的组合,称为一个“版本”,把它拷贝一份,就叫拷贝一个“版本” 出来。但这种原始的方法很低效,因为你每天写程序,写上一个月,你的磁盘上就有30个 版本了,到时你都不记得哪个版本能用,每个版本都是干什么的。

这样,你又需要写一个文本文件,用来说明,你这是什么版本,版本的用途。为此,就有 人写了专门的工具来管理这些版本。这种工具就叫“版本管理工具”。

git就是其中一个最出色的版本管理工具,它是Linus专门给Linux Kernel写的版本管理工 具,但现在它几乎成了所有开源的,不开源的软件的首选版本管理工具了。甚至你可以认 为它直接改变了人们管理版本的方式,成为软件开发管理版本的一种“事实上的标准”。

git的用法很简单,比如你有一个目录,你需要用git来管理这个目录里面的文件的版本, 你只需要到这个目录里面运行::

git init

它就会在里面创建一些文件用来放你目录中的文件的版本的信息。这些文件也不会影响你, 因为它们全部都在.git目录下面,如果你不需要git帮你管理了,你删掉这个目录,所有这 些信息就都没有了。

之后如果你增加或者修改了文件,你只需要这样::

git add xxxx.cc xxxx.hh
git commit

这样就可以了,其中add表示告诉git,你增加或者修改了xxxx.cc,xxxx.hh,commit表示 告诉git现在增加的这些,就是我新的版本了,你给我创建一个新的版本。

git会让你输入这个版本的说明,这样以后你就可以查你每个修改具体修改了什么,以及具 体是怎么修改过来的。

比如,下面是我在Linux Kernel下运行::

git log

的结果::

commit 39c3c396f8131f3db454c80e0fcfcdc54ed9ec01 (HEAD -> mainline_master, mainline/master)
Merge: 5de64d44968e 1f7ea54727ca
Author: Linus Torvalds <[email protected]>
Date:   Tue Jul 26 19:38:46 2022 -0700

    Merge tag 'mm-hotfixes-stable-2022-07-26' of git://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm

    Pull misc fixes from Andrew Morton:
     "Thirteen hotfixes.

      Eight are cc:stable and the remainder are for post-5.18 issues or are
      too minor to warrant backporting"
      ...

commit 1f7ea54727caaa6701a15af0cbeddfdb015b2869
Author: Gao Xiang <[email protected]>
Date:   Tue Jul 19 23:42:46 2022 +0800

    mailmap: update Gao Xiang's email addresses

    I've been in Alibaba Cloud for more than one year, mainly to address
    cloud-native challenges (such as high-performance container images) for
    open source communities.

...

这里的每个commit,就是一个版本(git里面叫revision),用一个随机生成的代码表示, 里面说了谁是修改的人,什么时候修改了,为什么要修改。

这样我们就可以记住所有的修改,也可以用那个commit的代码查这个commit具体修改了什 么,比如我们有一个commit叫commit cdb281e63874086a650552d36c504ea717a0e0cb,我们 可以用show命令::

git show cdb281e6387408

(注:commit的那个id(称为hash,用它的生成方法来命名的)不需要写全的,只要能写 到和其他commit代码不一样就行)去显示它的内容::

commit cdb281e63874086a650552d36c504ea717a0e0cb
Author: Qi Zheng <[email protected]>
Date:   Tue Jul 26 14:24:36 2022 +0800

    mm: fix NULL pointer dereference in wp_page_reuse()

    The vmf->page can be NULL when the wp_page_reuse() is invoked by
    wp_pfn_shared(), it will cause the following panic:

    ...

    Fixes: 6c287605fd56 ("mm: remember exclusively mapped anonymous pages with PG_anon_exclusive")
    Signed-off-by: Qi Zheng <[email protected]>
    Reviewed-by: David Hildenbrand <[email protected]>
    Signed-off-by: Linus Torvalds <[email protected]>

diff --git a/mm/memory.c b/mm/memory.c
index 4cf7d4b6c950..9174918ce3f7 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -3043,7 +3043,7 @@ static inline void wp_page_reuse(struct vm_fault *vmf)
        pte_t entry;

        VM_BUG_ON(!(vmf->flags & FAULT_FLAG_WRITE));
-       VM_BUG_ON(PageAnon(page) && !PageAnonExclusive(page));
+       VM_BUG_ON(page && PageAnon(page) && !PageAnonExclusive(page));

        /*
         * Clear the pages cpupid information as the existing
(

这个commit你看见了,它说明了作者,日期这些基本信息,还有作者给你说明的为什么要 进行的修改,你还可以看到它具体修改了哪个(些)文件(这里的mm/memory.c),还有就 是具体修改了哪一行(这里的-号表示删掉的,+号表示增加的),这样你就很容易知道当 时那个修改具体是用来解决什么问题的了。

这种方法特别适合用来多人协同,比如你和其他3个同学一起合作写程序,你负责几个函数, 她们负责另外几个函数。你们可以创建一个一起用的工程,把这个工程放到一台服务器上, 大家修改完了,就写到那个服务器上,这样你们互相修改,就有各自的commit,如果有一 个人修改错了,大家可以把她的commit删除,这样也不影响其他人的工作。这样合作起来 就会很方便。

git把另一个git目录叫remote,比如你原来的git目录叫myapp,你同学的目录叫herapp, 你可以用remote命令把她的目录的位置告诉你的目录,比如这样::

git remote add hereapp_s_remote /path/to/herapp  #让你的目录认识她的目录
git pull                        #把她的修改拉进来
git push                        #把你的修改推给她

当然,她的目录一般不在你的机器上,这是你就可以用一个公共的服务器来完成这样的工 作。所以,要学习这种合作开发,你可以先从gitee.com申请一个免费的帐号,然后创建一 个工程,创建了以后,网站会告诉你用什么add命令去让你认识它,比如我这里这个文档, 就用了gitee的目录,我要认识它,我的写法是这样的::

git remote add origin [email protected]:Kenneth-Lee/MySummary.git   # 把gitee的工程叫做origin,这是个默认的名字
git push                                                         # 把我的目录push给gitee
git pull --rebase                                                # 把gitee的内容拉到我的目录来,
                                                                 # --rebase不是必须的,但用上可能简化日志,这个用一段时间就知道了

这样,你们所有同学都对着这个服务器进行推拉,内容就可以互相拷贝了。而且,这样也 自动完成了备份,不会你机器坏了,代码就丢了。当然,放在网上的信息是不安全的,你 学习的代码这样放可以,不要把密码,个人隐私这些东西放上去。

好了,我的整个教程就写到这个程度了。剩下的东西就等你自己看教程和提问题了。很多 学校上机学习可能用IDE(图形界面),但其实背后都是对上面这些工具的调用,你搞懂这 些工具,学习那些东西就是个眼见功夫,那些就自我发挥就可以了。

附录

一个在gitee上合作开发的例子

我假定你已经在gitee上申请了一个帐号,我假定叫aaa吧,我的帐号是Kenneth-Lee-2012。 现在假定我们合作开发,我在我的机器上创建一个目录,叫cpp_study,我先用这些命令创 建我自己的git仓库::

mkdir cpp_study
cd cpp_struct
vim README   # 写一个项目说明,一般都有这个习惯,反正你需要在里面放什么文件就一个个创建吧
...
git init     # 创建一个git仓库
git add .    # 把当前目录下所有文件都纳入管理
git commit -s -m "init" # 提交修改
git status   # 这可以看到你的库的状态

好了,现在我的机器上有了一个仓库,我希望共享给你。所以我打算在gitee上创建一个和 你共享的仓库。

我打开gitee.com网站,在右上角找到一个加号,选择“新建仓库”就可以创建一个新的仓库 了。创建以后,它会出来一组帮助,告诉你怎么把代码推上去的。(你可以自己试着创建 几个,创建完不需要就删除就可以了)

我这里直接把过程放在这里。继续在你的Windows Ubuntu Shell里面运行这些命令::

git config --global user.name "Kenneth-Lee-2012" # 这是用户名,随便起名字,能分出你自己就行
git config --global user.email "[email protected]" #这是email地址,也是随便的,但最好有一个真的

# 前面两个命令其实是配置git的参数,就是修改你Home目录的.gitconfig文件,如果你配置过了,以后都不需要了。
# 这两个配置的作用是你修改了代码,告诉别人这是谁修改的而已

git remote add origin [email protected]:Kenneth-Lee-2012/cpp_study.git
# 上面这个命令把gitee的目录作为我机器上一个远程仓库,叫origin(这是git的默认远程仓库)

git push -u origin "master"
# 上面这个命令把master分支(什么是分支我们后面说,反正我们现在修改都是master分支)推送到origin的master上

好了,现在我的文件推到gitee上了。轮到你了,你先找一个地方准备放这个目录的,然后 你就可以这样下载这个目录了::

git clone [email protected]:Kenneth-Lee-2012/cpp_study.git

(这个过程需要你的gitee用户名和密码)

这样,你的机器上也有一个cpp_study的仓库了。现在你可以增加你的文件,或者修改我原 来有的文件。修改完以后,你可以用下面命令推到服务器上::

git add .
git commit -s -m "我的XXXX修改"
git push

如果我需要拿到你的修改,我可以执行::

git pull

我就能拿到你的修改了。反过来,如果我修改了东西,你也可以通过git pull取下来。

好了,现在我们讨论一下什么叫版本和分支。

我们有三个版本的仓库:你的,我的,gitee上的。一开始我修改完了,我的版本叫v1,我 push到gitee上,gitee的版本也是v1,你clone一份,你也是v1,现在,我修改了一下,v1 变成v2,我push上去,gitee也是v2,然后你也修改了一下,你的版本和我不一样,你那个 我叫它v3,你也push,服务器就不知道怎么处理了,因为服务器上是v1->v2,你要push一 个v1->v3上来,它是留着我那个呢?还是你那个呢?

这种情况push会失败,这时我们有三种解决方法:

  1. git push -f,强行覆盖我的修改,这样,服务器上就是v1->v3,我的东西没有了。我 们最好不要这样玩,因为一不小心你就把我辛辛苦苦写了几天的程序都删掉了。

  2. git pull --rebase,把服务器的内容拉下来,尝试和你的修改合并在一起,让你再 修改一下,变成这样:v1->v2->v3.1。之后再push就可以了。

  3. 让服务器同时记录v1->v2和v1->v3两个不同的修改序列。这两个不同的修改序列就叫不同的 分支。我们刚才不是说了吗?我们一起工作的那个分支叫master,你可以用如下命令给你的 修改创建一个新的分支,比如叫my_branch::

    git checkout -b my_branch
    git push origin my_branch
    

    但这样我们的工作就各顾各,不会再合并在一起了(当然,这之后我们还是可以用其他 命令合并起来的,这个我们用到的时候再学习)

上面所有操作gitee的命令,每次都有输入密码,如果你不想输入密码,可以考虑使用ssh key。

ssh还记得吗?就是那个Secure Shell,一个加密的通讯方法。它使用的是非对称加密,用 一对加密密码,公钥和私钥。公钥放外面去,私钥放自己计算机上。这样通讯的时候就可以 不用被人偷信息了。gitee支持ssh的通讯方式,用这种方式,你就不需要每次都输入密码了。 每次输入密码也不安全。

为了用ssh,我们先创建一对钥匙::

ssh-keygen

这个命令会随机产生一堆钥匙,它需要真随机数,所以,如果它停住不动,赶紧动动鼠标, 好产生一些真随机数。产生的key在你的Home目录的.ssh下,私钥叫id_rsa,这个文件打死 也不能泄漏到网上(最好完全不要通过网络传输),它是你保密的基础。另一个文件叫 id_rsa.pub,这是公钥,可以放到对方那边给对方用的。

在gitee.com上,点击自己的头像图标,找到“设置菜单”,在左边的侧栏上找到“安全设置 -SSH公钥”,点进入,把你的id_rsa.pub(它就是个文本文件)的内容原封不动全部拷贝到 里面去,保存。这样,你的机器和gitee网站之间就建立一对密钥的通讯了,之后你再运行 git的push/pull命令,就不会再找你要密码了。

公钥和密钥,最好找自己的U盘备份一下。

git管理的文件的几个状态

要学习git,关键是搞清楚它的文件的几个状态。git把你的文件的连续的多个版本保存到 它的数据库里面,文件的内容到了数据库里面了,这个状态就叫“提交”(commit)状态, 而你用git add命令告诉git你修改了某个文件,这个状态叫“暂存”(Staged)状态。你用 git commit提交一个版本,用的就是这个Staged的状态里面修改的内容。而你仅仅修改了 文件,没有告诉git,这个状态叫unstaged状态,表示git不知道你修改了。

Note

Stage是舞台的意思,Staged表示这个文件被推到了前台,而UnStaged表示这个文件还 在“后台”,而commit,表示这一场已经落幕,文件的状态成为了历史。

你把一个文件修改了,然后add它,这时unstaged状态和staged的状态是一致的。但如果add 完了,你又去修改它,这时新的修改是unstaged的状态,原来修改过的东西是staged的状态, 你可以再运行git add加到里面去。之后你再做commit,就一次把所有的修改都commit进去 了。git commit -a可以自动把所有unstaged的修改都先add了再commit,但它不会增加新写 的文件,只增加修改的文件,所以,如果你有新文件,还是要主动add一次的。

当你没有staged和unstaged的内容的之后,你的库就称为Clean的。库和库之间同步,都是用 commit的内容同步的,所以同步之前,最好先保证你的目录是clean的。比如你本地要和 gitee(或者github)同步,你最好就是就是commit完所有的内容,然后才做git pull。 如果你本地不是clean的,git pull下来的内容就需要和你本地的内容合并,这很容易出错。

在clean的情况下,git pull有两种可能性,一种是git pull的版本比你领先,它会自动更 新到服务器的版本。但还有一种很常见的情况:服务器上的版本比你的新,而你的版本也 在本地修改了东西,所以你的版本也有部分内容比服务器新。这样就会冲突。运气好的话, 服务器上的内容和你的内容没有冲突的地方,比如各自改各自的文件,或者即使同一个文 件,大家改的不是同一个函数。这种情况,你运行git pull --rebase,它就能直接合并。 但如果冲突了,你就只能根据打印的信息,把冲突的文件一个个看一遍,修改掉。这时就 有了unstaged的内容了,你按原来的方法,add进去,变成一个commit,这样你本地的 commit就成为服务器版本的版本升级,这时你就可以正常git push到服务器上了。