Git教程:入门

Posted by lili on

本文介绍Git的入门知识。

目录

入门

命令行用法

运行不带任何参数的git命令后会出现如下信息(LANG为zh_CN.UTF-8的Ubuntu系统):

$ git
usage: git [--version] [--help] [-C <path>] [-c name=value]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]

这些是各种场合常见的 Git 命令:

开始一个工作区(参见:git help tutorial)
   clone      克隆一个仓库到一个新目录
   init       创建一个空的 Git 仓库或重新初始化一个已存在的仓库

在当前变更上工作(参见:git help everyday)
   add        添加文件内容至索引
   mv         移动或重命名一个文件、目录或符号链接
   reset      重置当前 HEAD 到指定状态
   rm         从工作区和索引中删除文件

检查历史和状态(参见:git help revisions)
   bisect     通过二分查找定位引入 bug 的提交
   grep       输出和模式匹配的行
   log        显示提交日志
   show       显示各种类型的对象
   status     显示工作区状态

扩展、标记和调校您的历史记录
   branch     列出、创建或删除分支
   checkout   切换分支或恢复工作区文件
   commit     记录变更到仓库
   diff       显示提交之间、提交和工作区之间等的差异
   merge      合并两个或更多开发历史
   rebase     本地提交转移至更新后的上游分支中
   tag        创建、列出、删除或校验一个 GPG 签名的标签对象

协同(参见:git help workflows)
   fetch      从另外一个仓库下载对象和引用
   pull       获取并整合另外的仓库或一个本地分支
   push       更新远程引用和相关的对象

命令 'git help -a' 和 'git help -g' 显示可用的子命令和一些概念帮助。
查看 'git help <命令>' 或 'git help <概念>' 以获取给定子命令或概念的
帮助。

git命令单独作用不大,它有很多子命令(sub command),比如我们常用的”git add”、”git commit”、”git pull”等等。上面的帮助列举了常见的一下子命令,如果想查看所有的子命令,可以运行”git help --all”。命令的选项(option)有些是直接影响git命令,比如:

$ git --version
git version 2.7.4

而有些影响子命令,比如”git commit --amend”,选项amend会影响commit这个子命令。也可能存在既影响git的选项也存在影响子命令的选项,比如”git --git-dir=project.git repack -d”。在早期的git版本里,各个子命令都是单独的可执行程序,比如git-commit的作用就类似于git commit子命令,但是后来大家习惯于使用一个带大量子命令的单独的git命令。有的选项是长的形式(多于一个字母)而有些是短形式,比如:

$ git commit -m "Fixed a typo."
$ git commit --message="Fixed a typo."

-m和--message的效果是一样的,都是给commit子命令增加注释。短形式的用法是减号加一个字母,然后跟选项的值,字母和值直接可以有空格,也可以没有,如果值不包含空格,可以不加引号,比如:

$git commit -mfix_a_typo
$git commit -m fix_a_typo
$git commit -m "fix_a_typo"

是等价的。

而长形式是两个减号加一个单词,然后后面加等号再加选项的值。对于常见的命令,我们通常会记得短形式的选项,但是不常见的命令我们可以使用长形式,因为当我们输入--的时候键入tab键时shell会帮我们补全,另外长命令的等号(=)也可以换成空格,因此:

$git commit --message=add_file
$git commit --message "add_file"
$git commit --message add_file

是等价的,但是”git commit --messageadd_file”是不行的,因为命令会把”messageadd_file”当成一个选项,结果会提示”error: unknown option `messageadd_file’“。

有的选项,比如前面的-m和--message,同时存在长形式和短形式,但是有的选项只有一种形式。

同一个命令根据参数类型的不同可能有不同的行为,但是参数的值有时候很难区分,这个时候git可能要用到”--“分开。比如我们有一个分支也叫main.c(有些奇怪?但是分支名字可以包含点),另外有一个文件也叫main.c,那么:

$ git checkout main.c

到底是checkout出main.c这个分支还是checkout出main.c这个文件呢?为了区分是checkout分支还是checkout一个文件,git命令默认checkout一个分支,那为了checkout一个文件,我们需要在文件路径前增加--。因此:

$ git checkout main.c
$ git checkout -- main.c

第一个命令是checkout main.c这个分支,而第二个命令是checkout main.c这个文件。如果读者还不熟悉git,可能不能理解上面两个命令的含义,我们可以查看帮助”git checkout –help”,我们会发现:

    git checkout <branch>
           To prepare for working on <branch>, switch to it by updating the index and the files in the working
           tree, and by pointing HEAD at the branch. Local modifications to the files in the working tree are
           kept, so that they can be committed to the <branch>.



    git checkout [-p|--patch] [<tree-ish>] [--] <pathspec>...
           When <paths> or --patch are given, git checkout does not switch branches. It updates the named paths
           in the working tree from the index file or from a named <tree-ish> (most often a commit). In this
           case, the -b and --track options are meaningless and giving either of them results in an error. The
           <tree-ish> argument can be used to specify a specific tree-ish (i.e. commit, tag or tree) to update
           the index for the given paths before updating the working tree.

git快速上手

为了熟悉git,我们通过一个简单的示例来介绍怎么用git创建一个repo(repository),并且进行一些修改,熟悉的读者可以跳过本节。我们可以从零开始创建一个repo,也可以从别人那里clone一个repo,这里我们从零开始。

创建一个repo

假设我们在Home下有一个目录用于存放个人主页,比如~/public_html。如果没有的话我们可以现在就创建一个:

$ mkdir ~/public_html
$ cd ~/public_html
$ echo 'My website is alive!' > index.html

然后我们可以用git init初始化(创建)一个repo:

$ git init
初始化空的 Git 仓库于 /home/lili/public_html/.git/

git不会care这个目录是否有内容,它只是会在这个目录下创建一个.git子目录,这个隐藏的子目录包含了git需要管理这个repo的全部数据。不管这个目录是否空,新建的repo总是空的,我们需要用命令把要管理的内容手工加到repo里,用术语描述就是让版本控制软件track这些内容。所以前面我们创建的index.html不会有任何变化,git把非.git目录下的文件/目录叫做工作目录(working directory),它并不是版本控制内容的一部分。当然我们可以用git add等命令把工作目录的内容添加到版本控制系统里,也可以checkout出版本控制系统里的内容,比如取出index.html昨天的内容。这没有什么神奇,它就是版本控制系统的最主要用途!如果读者使用过CVS或者SVN,可能会发现有些区别:git只在repo的顶层目录下有一个.git目录,而CVS和SVN在repo的每个目录(顶层和子目录)下都有隐藏的目录。如果我们想删除版本控制的内容(尽量不要!),使用git会简单得多。

把文件加入repo

我们需要使用git add把一个文件加到repo里:

$ git add index.html

如果有很多文件和子目录要加进去,我们也可以使用”git add .”把所有的文件都加到repo里。但是注意:git add只是把文件”暂存”(stage)到index里,这么做的原因是为了避免我们频繁的把一些临时的更新加到版本的历史里。我们可以运行git status来查看当前的状态:

$ git status 
位于分支 master

初始提交

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

	新文件:   index.html

上面的命令告诉我们index.html已经加到index里,下次提交(commit)的时候就被”真正”加到版本控制系统里,变成”历史”,而历史是不能修改的(也不完全,我们现在看到的”历史”不知被多少朝代和多少人修改过多少遍,所谓”一切历史都是当代史”)。为了把index里暂存的数据加到repo里,我们需要使用git commit这个命令:

$ git commit -m "Initial contents of public_html" \
> --author="lili <fancyerii@gmail.com>"
[master (根提交) 4613c42] Initial contents of public_html
 Author: lili <fancyerii@gmail.com>
 1 file changed, 1 insertion(+)
 create mode 100644 index.html

它有两个必须的选项:-m来填写commit的注释,这样便于我们知道这次commit的内容;--author填写提交者的信息。因为提交者一般不变,每次都填写很烦人,所以git可以让我们使用git config来配置它:

$ git config user.name "lili"
$ git config user.email "fancyerii@gmail.com"

注意,--author其实包含两个信息:user.name和user.email。我们也可以用git config user.name来查看当前的提交者(输入git config 后使用tab可以提示补全):

$ git config user.name 
lili

如果要查看所有的配置信息,可以输入”git config -l”。使用git config配置的信息会保持在.git/config里:

$ cat .git/config 
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[user]
	name = lili
	email = fancyerii@gmail.com

我们会在[user]这个section里看到我们输入的信息,我们也可以手动修改.git/config文件,效果是一样的,但是手动修改容易出错,所以 建议用git config修改。

另外这里的user.name和user.email是没有认证的,因此你填写任何别人的名字和邮箱都是可以的。Git本身只是一个版本控制系统,如果需要认证和授权,需要额外的工具,比如最简单的的文件系统读写权限、ssh权限或者github/gitlab这种更加复杂的系统。习惯了CVS或者SVN的读者需要了解这一点,因为CVS除了版本控制之外,还包括认证和授权的管理,其粒度甚至可以到分支,而Git本身没有任何这些功能,用户只要有repo的文件读写权限,它就可以做任何操作。

另外一个用户的repo很多,每个配置也很烦,那么可以用--global配置全局的信息:

$ git config --global user.name "lili"
$ git config --global user.email "fancyerii@gmail.com"

--global的配置会保持的用户Home下的.gitconfig文件里:

$ cat ~/.gitconfig 
[http]
[user]
	name = lili
	email = fancyerii@gmail.com
...............

如果你是系统管理员(root),你还可以修改/etc/gitconfig。这三个配置文件的优先级从高到底为:.git/config、~/.gitconfig和/etc/gitconfig。因此如果在repo里配置了会优先被使用、repo里没有的再去家目录找,如果都没有则用/etc/gitconfig下的,如果再没有呢?那就真没有了,如果一个命令必须要这个选项,则会提示你手动输入。

另外我们在commit是需要用-m填写注释,这是必须要的选项。如果没有填写,那么git会弹出特定的编辑器(比如VIM)让我们填写。如果我们要填写非常长的注释,那么可以不带-m选项,这样处理换行等特殊字符比较方便。那么git会弹出哪个编辑器呢,这依赖于系统,不过我们可以自己来配置自己喜欢的编辑器:

$ git config --global core.editor

这里顺便提一下,如果想删除某个配置可以在config命令后面增加--unset选项,比如想删除user.name:

git config --unset [--global] user.name

再次提交

接下来我们再次修改index.html,使得它的内容为:

$ cat index.html 
<html>
    <body>
        My web site is alive!
    </body>
</html>

我们修改了后可以直接用commit子命令:

$ git commit index.html
[master 99ce8c5] Convert to HTML
 1 file changed, 5 insertions(+), 1 deletion(-)

上面的命令会弹出VIM(或者其他编辑器),我们可以输入”Convert to HTML”保持,然后就和提交成功。读者可能会奇怪这次没有使用git,如果这个文件还没有加到repo里,使用上面的命令会出错;而现在git知道index.html已经在repo里了,则可以省略掉”git add index.html”。但是如果git commit后不带文件名,它会把所有已经在repo里的修改都提交吗?为了安全,它不会这样做!

查看提交的历史

我们可以用git log来查看提交的历史:

$ git log
commit 99ce8c5dca641f355e596a4961071dd1fbc6c100
Author: lili <fancyerii@gmail.com>
Date:   Wed Feb 5 11:27:55 2020 +0800

    Convert to HTML

commit 4613c42e807ec9877501129a3b29ba2977548bf7
Author: lili <fancyerii@gmail.com>
Date:   Wed Feb 5 11:02:53 2020 +0800

    Initial contents of public_html

如果想查看某一个commit的更详细信息,可以用git show commit-id:

$ git show 99ce8c5dca641f355e596a4961071dd1fbc6c100
commit 99ce8c5dca641f355e596a4961071dd1fbc6c100
Author: lili <fancyerii@gmail.com>
Date:   Wed Feb 5 11:27:55 2020 +0800

    Convert to HTML

diff --git a/index.html b/index.html
index 34217e9..d23f160 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,5 @@
-My website is alive!
+<html>
+    <body>
+        My web site is alive!
+    </body>
+</html>

如果git show不带任何参数,则会打印最后一次commit的信息,因此和前面是等价的。此外我们也可以用git show-branch来线上当前分支的commit信息:

$ git show-branch --more=10
[master] Convert to HTML
[master^] Initial contents of public_html

查看某次commit的diff

$ git diff 4613c42e807ec9877501129a3b29ba2977548bf7 99ce8c5dca641f355e596a4961071dd1fbc6c100
diff --git a/index.html b/index.html
index 34217e9..d23f160 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,5 @@
-My website is alive!
+<html>
+    <body>
+        My web site is alive!
+    </body>
+</html>

git diff commit-id1 commit-id2可以比较两次commit之间的差别,我们可以把每次commit都看成这次commit之后的状态,因此上面的命令就是最后一次commit的修改,也就是把”My website is alive!”那一行删掉(-开头的行),加入后面的html(+开头的那些行)。

删除和重命名文件

删除文件首先要用git rm同时从工作目录和index里删除文件,然后用git commit从repo里删除。如果用操作系统的命令rm删除一个文件的话:

$ rm index.html 
$ git status
位于分支 master
尚未暂存以备提交的变更:
  (使用 "git add/rm <文件>..." 更新要提交的内容)
  (使用 "git checkout -- <文件>..." 丢弃工作区的改动)

	删除:     index.html

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

我们还需要使用git add/rm把它从index里删除(用add删除有点奇怪?):

$ git add index.html 
$ git status
位于分支 master
要提交的变更:
  (使用 "git reset HEAD <文件>..." 以取消暂存)

	删除:     index.html

最后还需要用commit把它从repo里删除:

git commit -m "delete index.html"

要重命名/移动文件有两种方法:第一种先用操作系统的mv命令、然后git rm删除老文件、最后git add新文件;也可以用git mv一步完成前面的操作:

$ mv foo.html bar.html
$ git rm foo.html
rm 'foo.html'
$ git add bar.html

# 等价于:
$ git mv foo.html bar.html

不管哪种方法,都是对index的操作,最后需要commit。

$ git commit -m "Moved foo to bar"
Created commit 8805821: Moved foo to bar
1 files changed, 0 insertions(+), 0 deletions(-)
rename foo.html => bar.html (100%)

复制

我们可以用git clone子命令来从一个repo复制得到一个新的repo:

$ git clone public_html my_website

这两个目录的内容是完全一样的,我们可以用diff来验证:

$ diff -r public_html my_website

上面的输出是空,说明两个目录(递归diff后)完全相同。注意:没有提交的内容,包括untracked和index里的内容是不会被clone的。