Git简单使用

Git简单使用

git初始化操作

pwd:显示当前目录

git init:把这个目录变成git可以管理的仓库

git clone: 从现有 Git 仓库中拷贝项目(类似 svn checkout)。

克隆仓库的命令格式为:

1
git clone <repo>

如果我们需要克隆到指定的目录,可以使用以下命令格式:

1
git clone <repo> <directory>

参数说明:

  • repo:Git 仓库。
  • directory:本地目录。

git config --list:显示当前的git配置信息

编辑 git 配置文件:

1
git config -e    # 针对当前仓库

或者:

1
git config -e --global   # 针对系统上所有仓库

设置提交代码时的用户信息:

1
2
git config --global user.name "runoob"
git config --global user.email test@runoob.com

工作区、暂存区和版本库

  • 工作区(Working Directory):指的是在电脑里能看到的目录,即本地工作目录;
  • 暂存区: 英文叫 stage 或 index。一般存放在 .git 目录下的 index 文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。;

  • 版本库(Repository):.git目录, Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD

下面这个图展示了工作区、版本库中的暂存区和版本库之间的关系:

  • 图中左侧为工作区,右侧为版本库。在版本库中标记为 “index” 的区域是暂存区(stage/index),标记为 “master” 的是 master 分支所代表的目录树。
  • 图中我们可以看出此时 “HEAD” 实际是指向 master 分支的一个”游标”。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。
  • 图中的 objects 标识的区域为 Git 的对象库,实际位于 “.git/objects” 目录下,里面包含了创建的各种对象及内容。
  • 当对工作区修改(或新增)的文件执行 git add 命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。
  • 当执行提交操作(git commit)时,暂存区的目录树写到版本库(对象库)中,master 分支会做相应的更新。即 master 指向的目录树就是提交时暂存区的目录树。
  • 当执行 git reset HEAD 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。
  • 当执行 git rm –cached \<file> 命令时,会直接从暂存区删除文件,工作区则不做出改变。
  • 当执行 git checkout . 或者 git checkout – \<file> 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区中的改动。
  • 当执行 git checkout HEAD . 或者 git checkout HEAD \<file> 命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

后面我会对这里给出的重要命令分别利用具体例子进行解释,更容易理解一些。

基本操作

Git 常用的是以下 6 个命令:git clonegit pushgit addgit commitgit checkoutgit pull

说明:

  • workspace:工作区
  • staging area:暂存区/缓存区
  • local repository:版本库或本地仓库
  • remote repository:远程仓库

提交与修改命令:介于工作区和版本库之间

查看提交日志的命令,先放前面,没什么需要特别说明的

git log 查看历史提交记录
git blame 以列表形式查看指定文件的历史修改记录

git status:查看状态,会提示当前哪些文件在工作区被修改尚未提交到暂存区;哪些文件已经被提交到暂存区,但尚未提交到当前分支;有几个提交已经提交到了当前分支,但尚未推送到远程仓库。

例如在本地新建了一个test.txt文件

在git命令行中被叫做\,而且提示你使用git add命令添加;不过\<Untracked>只适用于新增的文件,如果是对已经提交过的文件做了改动,git的提示叫做:”Changes not staged for commit”。

git add:把需要提交的文件添加到暂存区

这里我们把test.txt添加到暂存区

在git命令行中的提示为”changes to be commited”

这里可以git add 指定文件,也可以git add .添加全部文件

git commit:把暂存区的所有内容提交到当前分支,也可以理解为提交到本地版本库

接下来我们把test.txt提交到当前分支

使用-m参数,添加代码提交说明是一个好习惯

git diff:查看工作区文件和暂存区里面的区别

注意这个命令是对比具体某一个文件的内容的差别,所以后面一般要带上具体的文件名称,该命令有两个主要的应用场景:

  1. 工作区和暂存区文件的区别

    例如我们在上面的test.txt文件中再增加一行

  2. 暂存区中尚未commit的和已经commit的文件的区别

    接下来我们将工作区中增加了一行的test.txt添加到暂存区中

    现在我们最新改动的test.txt添加到了暂存区,但是尚未提交到当前分支,刚才我们已经提交了一个版本的test.txt到当前分支,现在我们可以看看这两个文件的区别(注意使用git diff test.txt是没有用的)

    显示暂存区和上一次提交(commit)的差异:

    1
    2
    3
    $ git diff --cached [file]

    $ git diff --staged [file]

一般情况下,git add和git commit可能就够处理日常的代码提交了,但是考虑以下几种情况,可能还需要撤销一些改动和提交。

文件改错了,想撤回恢复原状,这个时候分为三种情况

  1. 你修改了本地文件,未添加到暂存区,想将本地的文件恢复原状

    执行 git checkout . 或者 git checkout – \<file> 命令时,会用暂存区全部或指定的文件替换工作区的文件

    注意:这个操作很危险,会清除工作区中未添加到暂存区中的改动,确保你本地的改动不想要了或者已经备份了本地的改动,再执行该命令

    另外还有一点,如果是本地新增的一个文件,暂存区中并没有,那么git checkout是不会对此文件有任何效果的,如果你不想要这个文件了,手动删除即可。

    “–”很重要,不加的话就变成了切换分支的命令。

    考虑一种情况:工作区的修改在之前已经添加到暂存区,现在又做了修改,checkout命令只能回到上一次添加到暂存区后的状态,并不能撤回上一次添加到暂存区的操作(想要撤回那就是下面这种情况);

  2. 你修改的文件被添加到了暂存区,但尚未提交到当前分支

    我们继续上面的例子,增加了一行数据的test.txt被添加到了暂存区,尚未提交到当前分支,可以通过执行git reset HEAD来丢弃暂存区的修改

    但是其修改的原理是:使用当前分支指向的目录树刷新暂存区的目录树,执行完该命令后,我们第一次commit到当前分支的test.txt状态和内容不会改变,工作区的内容同样不会改变。

    如果需要提交本地的修改,那就再次add、commit;如果工作区的代码你也不想要了,那就回到了第1点。

  3. 你修改的文件已经提交到了当前分支,但尚未push到远程分支:

    其实通过上面的学习,我们可以绘制出这样一个流程图

    上面第2种情况中提到如果代码添加到了暂存区尚未提交到分支,可以通过reset来恢复暂存区,那么当代码已经提交到了分支之后,无论怎么reset,都是没有用的,那么究竟应该怎么做?

    符合这一条件的就是第一次commit到当前分支的test.txt,我们以这个文件为例来实现。

    不过这里同样需要使用reset命令,利用其 –soft 参数,可以回退到某个版本:

    第一步:执行git log 定位commit xxxadsfdgfngffdhfj2435465fdfdfwe (commitid),一般选择本次提交前的上一个版本即可

    第二步:git reset –soft commitid 回退到这一版本

    这个时候之前提交到分支的test.txt文件被从当前分支中移除,目前处于暂存区,想要继续撤销的话就可以使用git reset HEAD,也就是回到上面的第2点。

问题1:那么有没有丢弃本次提交到分支的内容并且同时清空暂存区的方法呢?

有,那就是git reset HEAD^,此时工作区的改动不会受到影响

假设我们有test1.txt和test2.txt已经提交到了当前分支

1
2
3
git reset HEAD^            # 回退所有内容到上一个版本  
git reset HEAD^ hello.php # 回退hello.php的版本到上一个版本
git reset 052e # 回退到指定版本

可以看到,此命令相当先reset到上一个版本,然后git reset HEAD。

问题2:那么有没有丢弃本次提交到分支的内容并且同时清空暂存区,并且恢复工作区到与暂存区一致的方法呢?

有,那就是git reset --hard HEAD^,这里HEAD^表示上一个版本,如果命令中使用HEAD是没有用的,因为改动已经提交,当前的改动就是最新版本。

假设我们有test1.txt和test2.txt已经提交到了当前分支,并且又分别在这两个文件末尾增加了两行改动,现在执行该命令

执行完我们可以发现,工作区的这两个文件直接被删掉了,而不是丢弃最新的一次修改,所以你应该能够明白,”–hard”参数不要轻易使用,除非在你确定工作区的所有改动都不想要了或者已经备份的情况下再使用。

可以看到,此命令相当于先reset到上一个版本,然后git reset HEAD,最后又执行了git checkout .,区别点在于,如果提交的工作区文件是较上个版本新增加的文件,git reset --hard HEAD^会把工作区新增的文件也给删掉,而如果先执行git reset HEAD^再执行git checkout .则不会删掉新增的文件;如果改动的是上一个版本中就已经存在的文件,那么两种方式就没有区别,最终都会将相比上个版本的所有改动丢弃。

如果工作新增了文件,但从未添加到过暂存区,则无论是checkout还是reset –hard都不会对其造成影响。

HEAD 说明:

  • HEAD 表示当前版本
  • HEAD^ 上一个版本
  • HEAD^^ 上上一个版本
  • HEAD^^^ 上上上一个版本
  • 以此类推…

可以使用 ~数字表示

  • HEAD~0 表示当前版本
  • HEAD~1 上一个版本
  • HEAD^2 上上一个版本
  • HEAD^3 上上上一个版本
  • 以此类推…

问题3:当改动尚未提交到当前分支,已经添加到了暂存区,有没有同时清除暂存区和工作区改动的方法呢?

同样也是有的,就是git checkout HEAD .git checkout HEAD file,同样是利用checkout,但是多加了一个HEAD, 它就会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。

假设我们有个README文件(上个版本中已经存在),现在我们在工作区进行了修改,并添加到了暂存区。

执行完之后,我们发现暂存区被清空了,而且工作区的改动也被丢弃了。

现在我们再假设工作区新建了一个test.txt文件(上个版本中并不包含),现在我们将其添加到暂存区。

可以发现这种情况下,使用checkout是无法起作用的,新增加的test.txt依然在暂存区中,这个时候git reset --hard HEAD就派上用场了。

执行完可以发现暂存区和工作区中的test.txt都被清理掉了。

通过以上的学习可以发现,checkout无法处理新增的文件,reset –hard可以处理新增的文件。

文件删除

在工作中我们也会遇到需要删除线上的部分文件的场景,那么我们首先需要在工作区和暂存区中删除,然后commit给分支,最后再push到远程分支。

这个时候就需要使用git rm \<file>命令

假设我们工作区有一个README文件,并且在上一个版本中就已经有该文件,我们执行git rm README.md之后,会发现工作区中没有该文件了,查看状态会发现暂存区中也删除了该文件,待提交到分支(所以如果你想把远程分支上的该文件也删除,还需要commit和push)。

其实上面的操作基本已经可以解决工作中绝大多数的问题了,不过考虑一种情况,添加了较多文件到暂存区准备提交,然后多添加了个别并不打算提交的文件,然后又并不想通过上面那种撤销的操作来解决,那同样可以使用这里提到的文件删除操作。

我们同样也是分为两种情况来讨论

  1. 工作区新建了文件,但未添加到暂存区,不想要这个文件了,那随便删,不想要任何git命令;

  2. 针对上个版本已有的文件进行了修改,并添加到了暂存区,但是现在想从线上删除该文件,理论上这种情况不存在对吧,既然不要了也不会改,既然改了那应该不会想从线上删除该文件,不过即使存在这种情况,可以使用下面第3点一样的方法来解决;

  3. 工作区新建了文件,并添加到了暂存区,想要删除该文件;

    假设新建了test.txt文件,并添加到了暂存区,然后我们将工作区的test.txt删掉,看下会发生什么

    暂存区的test.txt依然存在,正在等待commit,然后还提示工作区删掉了test.txt并未同步到暂存区,可以通过git add/rm test.txt来同步给暂存区。

    我们接下来执行这两个命令的任一个肯定是可以删掉暂存区中的test.txt的,但是实际上如果使用git rm的话,无需先手动删除工作区的test.txt,但是因为test.txt文件是新建的文件(或者其他已有的文件进行了修改,新建也可以理解为修改),需要使用-f参数,也就是force强制删除。

    删除命令:git rm -f test.txt

    那现在还有个问题,这样直接把我们工作区新建的文件并且写的代码都给删掉了,我并不想删啊,我只是这次不想提交,那就使用--cache参数只删除暂存区,仍然保留工作区的改动。

    假设我们在工作区新建了test.txt并添加到了暂存区

    可以看到这时只删除了暂存区的test.txt,工作区的依然还在。

远程操作命令:介于版本库和远程仓库之间

远程操作

git remote:用于在远程仓库的操作

git remote -v:显示所有远程仓库

origin为远程地址的别名,也可以叫远程主机名,命令中使用的[alian]标志表示别名的意思。

显示某个远程仓库的信息,show后面也可以远程仓库的url

这里告诉我们远程有dev和main分支,本地也有dev和main分支。

那么这里为啥我们没有master分支呢,实际上后来github上已经将master分支更名为main分支,本质就是一个名称,没有什么区别,后面对于所有出现的main或master分支是等价的

拉取远程代码

git fetch:用于从远程获取代码库, 该命令执行完后需要执行 git merge 远程分支到你所在的分支。

现在我们测试将远程的main分支拉到本地,拉之前先修改一下远程main分支的README文件内容,方便后续对比变化。

拉取代码的语法规则是:git fetch [远程地址名] [分支名],分支名不写的话默认为master分支(这里和GitHub上你设置的默认分支有一致),远程地址名不写默认应该是origin。

以上信息”f564186..9cc37d5 main-> origin/main” 说明 master 分支已被更新,接下来我们使用merge命令将更新同步到本地,这里本地指的是本地版本库以及工作区。

合并代码

合并代码的命令语法是:git merge 远程地址名/分支名

命令执行结束查看当前分支的本地工作区文件已经被同步修改。

origin master表达的意思是:git服务器(origin代表)上的master分支。
origin/master表示本地分支(本地的远程分支),是从远程拉取代码后,在本地建立的一份拷贝,需要merge后才能更新当前分支的代码版本。

拉取并合并

git pull: 用于从远程获取代码并合并本地的版本,其等价于fetch+merge,使用语法

git pull 远程主机名 远程分支名:本地分支名

将远程主机 origin 的 main分支拉取过来,与本地的 dev分支合并。

git pull origin main:dev

如果远程分支是与当前分支合并,则冒号后面的部分可以省略。

git pull origin main

上面命令表示,取回 origin/main分支,再与本地的main分支(假设当前分支是main)合并。

当然也有简写的版本(一般不是很建议过多去记省略的版本,容易出错)

从origin的默认分支(一般是master,可以在GitHub上改)拉取过来,然后与当前分支合并

git pull origingit pull

推送代码

git push:用于将本地的分支版本上传到远程并合并,命令语法:

git push 远程主机名 本地分支名:远程分支名

如果本地分支名与远程分支名相同,则可以省略冒号:

git push 远程主机名 本地分支名

注意这里与pull的区别在于,pull是从远程到本地,远程分支名在前,本地分支名在后;而push是从本地到远程,所以本地分支名在前。

以下命令将本地的 master 分支推送到 origin 主机的 master 分支。

git push origin master

等价于

git push origin master:master

简化版本:git pushgit push origin

将当前分支的内容推送到远程与当前分支同名的远程分支名,例如当前分支为dev,则是从本地dev分支推送到远程dev分支。

分支管理

几乎每一种版本控制系统都以某种形式支持分支,一个分支代表一条独立的开发线。

使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作。

Git 分支实际上是指向更改快照的指针。

查看分支

1
git branch

新建分支

1
git branch 分支名称

切换分支

1
git checkout 分支名称

上面也有用过checkout,用来清空工作区的改动,注意使用的区别

合并分支

1
git merge 分支名

删除分支

1
git branch -d 分支名

这个命令是把指定的分支合并到当前分支,上面我们把origin/master分支合并到当前分支也是用的merge,且用法一致。

下面我们使用一些例子来理解分支

已知我们现在已经有两个分支main和dev,现在我们切换到dev分支,然后新建一个文件test1.txt并提交。

然后我们切换回main分支会发现工作区又回到了原来的样子,刚才在dev分支新建的文件不见了,切换回dev分支的话,test1.txt又会出现。

使用分支将工作切分开来,从而让我们能够在不同开发环境中做事,并来回切换。

如果在dev分支新建了文件没有提交,无论添加没添加到过暂存区,那么切换分支后是可以看得到的。

我们在dev分支新建了test1文件,将他合并到main分支

git merge dev

这样main分支也就有了test1.txt文件,假设dev分支开发已经结束,可以将其删除。

这里提示说dev没有完全merge,确认删除使用”-D”参数。

Diverge 分支分化问题

Diverge的主要原因是因为没有同步服务器端最新的代码就commit,设想服务器端已经有新的提交(其他同事更新了代码),但是你并没有同步,然后直接commit代码,这个时候你的分支代码与服务器端分支代码是不同的,当你push时肯定会出问题。
正确的流程应该是:先将本地代码同步为最新,再add,commmit和push。

如果出现Diverge问题,解决办法:
方案一:

  1. 先备份本地修改的未提交的代码;
  2. git fetch origin;
  3. git reset –hard origin/master

这种解决办法的原理是,先将本地的代码同步为服务器端的最新代码(fetch),这时候origin/master分支就是从远程拉过来的本地分支,然后使用reset命令和--hard参数覆盖本地版本库和工作区。

处理完之后可以再将修改的文件添加进来,重新提交,可以看出方案一的做法比较保守,也比较安全,适合新手。

方案二:

  1. git fetch origin
  2. git merge origin/master

第2步很有可能出现冲突,若出现冲突就解决冲突。

冲突解决

这里举个简单的例子解决冲突,假设我们的main分支test文件的内容如下

1
2
3
4
first line
second line
third line
end line

dev分支的test文件内容如下

1
2
3
4
first line
third line
second line
end line

将两个分支所做的改动都提交后,将dev分支的内容merge到main分支

提示告诉我们发生冲突了,无法完成自动merge,需要手动处理之后再提交,我们打开test文件看一下

1
2
3
4
5
6
7
8
9
first line
<<<<<<< HEAD
second line
third line
=======
third line
second line
>>>>>>> dev
end line

这里告诉我们”<<<”和”===”之间包括的内容与”===”和”>>>”之间包括的内容冲突,需要你确定下到底怎么保留,考虑到实际工作场景可能两边的代码都是需要的,所以我们这样处理

1
2
3
4
5
6
first line
second line
third line
third line
second line
end line

处理完之后,我们需要git add和git commit和push。

这个时候main分支冲突问题解决了,但是要注意dev分支和main分支还是有区别的,如果这一阶段的开发完成了可以删除dev分支;或者将main分支合并到dev分支以同步修改(如果你需要的话)。