GIT科普系列2:git代码检出与日常维护

背景:

由于公司内部目前采用git来进行代码管理,因此近期会逐步更新部分关于git的介绍。一来是为了给大家提供一个集中学习和参考的地方;二来是希望通过具体的示例来给出git的操作指南。虽然之前介绍过很多关于git的使用但不够系统,不够集中。这次希望通过几篇连续的博文给出一个简单的git实用指南。
既然提到了开发代码的版本管理,那么首要的就是从目标仓库中获取开发代码,即SVN中常用的检出(checkout)。本系列就从git的代码检出(git checkout?)开始!

git的几个关键概念

相较于SVN等其他版本管理系统,GIT的组织结构略显复杂,但正是因为这样,才使得GIT这一分布式的版本管理系统更能适应多变的开发环境和协同需求。在SVN中只有本地副本(working copy)和版本库(repository)两大部分,在GIT中结构复杂一些,包括:

  • working tree:也叫working directory.与SVN的本地副本有些类似,但也有不同。官方手册对working tree的介绍为:

    The working directory is a single checkout of one version of the project. These files are pulled out of the compressed database in the Git directory and placed on disk for you to use or modify.

  • index:也叫staging area.可以简单的理解为缓冲区。官方手册对index的介绍为:

    The staging area is a file, generally contained in your Git directory, that stores information about what will go into your next commit. It’s sometimes referred to as the “index”, but it’s also common to refer to it as the staging area.

  • repository分为local repository(本地的版本库)和remote repository(远程仓库)。由于git采用无中心分布式设计,因此本质上来说local repository与remote repository的地位是相同的,内容也几乎相同(远程仓库可能为了协同开发会绑定相关集成脚本),唯一的不同就是remote repository可以被更多的开发者访问,而local repository仅仅是个人工作环境的维护。官方手册对于repository(即.git目录)的介绍为:

    The Git directory is where Git stores the metadata and object database for your project. This is the most important part of Git, and it is what is copied when you clone a repository from another computer.

为了说清楚上述几个概念,这里节选了几幅比较形象的示意图:

这里写图片描述

摘自1.3 Getting Started - Git Basics

这里写图片描述

摘自Difference between HEAD / Working Tree / Index in Git

这里写图片描述

摘自Git Beginner’s Guide for Dummies

如上述图片所示,正式因为在本地存在着完完整整的仓库目录(.git目录),在工作副本和仓库之间添加了Staging Area缓冲区,才使得git能够实现本地和远程双重管理。

git的日常操作流workflow

官方文档给出了git版本管理系统下的文件的各种状态,包括untracked、unmodified、modified、staged。如下图所示:

这里写图片描述

git不单单是一个版本管理系统,其实可以看成是一个mini的文件系统,用于管理文件和日常的所有操作。git中的所有操作会使得git文件系统内部的各文件状态在上述四种之间来回切换。如下面两张截图所给出的git的各种日常操作指令。
这里写图片描述

摘自My Git Workflow

这里写图片描述

摘自Git Data Transport Commands

【备注】:这里要注意,不要将git所管理的文件的状态(untracked、unmodified、modified、staged)与git自身的阶段(working tree、index、local repository、remote repository)相混淆。
(1)working tree中的文件可以包含多种状态,例如从版本库上一次提交snapshot检出的文件处于tracked和unmodified状态、本地对tracked文件的任何修改会使文件处于modified和staged(git add操作即可)状态,本地新增文件或其他操作会使得文件处于untracked状态;
(2)index中的文件只能处于modified和staged状态(同时处于);
(3)repository(local or remote)中文件的状态只能处于unmodified的跟踪状态。——当然这句话还有待考究,这里严格意义来说是从repository中检出的文件只能处于unmodified的tracked状态,因为你只能从别的仓库中将unmodified的tracked状态的文件拉倒本地。但原始的仓库中其实有可能存在着其他状态的文件,但是对外来讲是隐藏的、透明的。

git的代码“检出”

其他版本库管理系统(例如SVN)一般都会使用checkout来直接检出服务端的版本库(即开发代码)到本地工作目录,而git checkout却不能实现这一功能,原因是:

If you’re familiar with other VCS systems such as Subversion, you’ll notice that the command is “clone” and not “checkout”. This is an important distinction – instead of getting just a working copy, Git receives a full copy of nearly(服务器会存在部分hooks或者其他文件,详情参考Git Internals) all data that the server has. Every version of every file for the history of the project is pulled down by default when you run git clone.

上文提到了git采用分布式无中心化设计,因此检出代码的同时需要将完整的仓库下载到本地。所以简单的checkout是不能完成这项任务的,在git中应该使用git clone将远程仓库检出到本地。

1. git clone的整体流程如下:

Created with Raphaël 2.1.0 local local remote remote git clone init .git dir pull all in .git add remote urls checkout latest working copy just do anything by yourself

从上图可以看出git clone也是一个复合高级指令,内部会执行git init(在本地初始化git repository,即创建.git隐藏目录)、git pull(将远程仓库的几乎所有拷贝到本地)、git remote add(将远程仓库的URL连接添加到记录),以及git checkout(将默认的仓库的master分支代码拷贝到本地工作目录working directory和缓冲区index)。
既然提到了其他的指令,这里一并介绍一下git pull、git checkout。

2. git pull的整体流程如下:

Created with Raphaël 2.1.0 local local remote remote git pull sub-command:git fetch sub-command:git merge just do anything by yourself

【备注】:这里要注意git pull与git fetch的区别,由上图可以看出git pull是git fetch和git merge的复合指令,由此可以得出git fetch只负责将文件检出到本地,但并不会与当前工作区的代码进行合并。官方说明如下:

It’s important to note that the git fetch command only downloads the data to your local repository – it doesn’t automatically merge it with any of your work or modify what you’re currently working on.
【zssure】:即git fetch从服务器取回本地没有的文件,但不会自动merge本地working copy。不带参数的git fetch会取回所有的分支。

3. git checkout的整体流程如下:

Created with Raphaël 2.1.0 local local remote remote git checkout checking out files checking out commits checking out all branches do anything by yourself


git的checkout

1. checkout的底层机制

通过上面两个章节“git的日常操作流workflow”“git的代码‘检出’”,我们大致了解了如何从仓库中将git所管理的各种文件拉取到本地,以及本地文件可能所处的各种状态。接下来就说一下日常最容易碰到的问题,诸如简单的git add、git commit和git reset就不赘述了,这几个指令日常使用最频繁就是将本地修改添加到git的仓库中进行管理和保存。这里着重介绍的是checkout,尤其是当需要同时管理多个分支,在多个分支之间切换时;甚至是当需要与同一个源的其他仓库进行合并时(即我们通常所说的多人协同开发)。

git checkout有多重指令格式,例如:

git checkout [-q] [-f] [-m] [<branch>]
git checkout [-q] [-f] [-m] --detach [<branch>]
git checkout [-q] [-f] [-m] [--detach] <commit>
git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>…​
git checkout [-p|--patch] [<tree-ish>] [--] [<paths>…​]

但是最终checkout所做的事情就是将命令行对应的版本库中或者index中的文件拷贝出来,粘贴到working directory(如果参数是版本库也会拷贝到index区域)区域中——这里与SVN等版本管理的checkout有些类似。这里给出一个最简单的本地切换分支的示意图:

这里写图片描述

摘自图解GIT

当git checkout后面跟的是一个本地(或远程)分支、或者HEAD(即默认的当前分支)时,实际的操作如上图所示:
- 1)会将本地的working directory和index的内容设置成与制定的分支的最近一次提交内容相一致,即目标分支中存在的任何文件都会被拷贝到index和working directory中
- 2)当前状态存在而目标分支下没有的文件会从index和directory中移除,
- 3)两个提交都存在的文件保持不变。

由此我们可以看出git checkout在分支切换时(分支切换使用最频繁),用来判别切换前后index和working directory状态的依据是两个分支的最后一次提交,而跟index和working directory的状态没有多大关系。

2. checkout多分支维护

这句话什么意思呢,让我们用示例来看一下:
1)假设本地有两个分支branch1和branch2.此刻两个分支刚merge完成,内容一致。此刻我们在branch分支上。新建git-dirty-test.txt文件,并输入一行string。可以使用git add来添加到index区域。但不commit。

这里写图片描述

在最终执行git commit将本次修改提交存储到branch1之前,我们可以在branch1和branch2之间任意自由切换,且不会丢弃掉当前的修改。无论是否将新增的git-dirty-test.txt文件添加进了index区域。具体执行情况如下:

#无论何时,一进来后先查看当前状态
D:\ZSDevelops\Git\git-dirty-test>git status
On branch official-master
Your branch is up-to-date with 'official/master'.
nothing to commit, working directory clean
#新建一个txt文件,再一次查看状态。此刻并没有执行git add指令
D:\ZSDevelops\Git\git-dirty-test>git status
On branch official-master
Your branch is up-to-date with 'official/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        git-dirty-test.txt

nothing added to commit but untracked files present (use "git add" to track)
#查看本地分支状态
D:\ZSDevelops\Git\git-dirty-test>git branch -v
  master          5fcbcaa Merge branch 'cache-enhancements'
* official-master 5fcbcaa Merge branch 'cache-enhancements'
#切换分支到master分支
D:\ZSDevelops\Git\git-dirty-test>git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
#再一次查看当前状态
D:\ZSDevelops\Git\git-dirty-test>git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        git-dirty-test.txt

nothing added to commit but untracked files present (use "git add" to track)

上文可以看出新增的文件依然在working directory中,分支切换也没有受阻,并且working directory的中间状态untracked也没有被破坏。
其实即使执行了git add指令依然可以自由切换分支,并不会影响working directory和index任何东西。执行过程如下:

D:\ZSDevelops\Git\git-dirty-test>git add git-dirty-test.txt

D:\ZSDevelops\Git\git-dirty-test>git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   git-dirty-test.txt


D:\ZSDevelops\Git\git-dirty-test>git checkout official-master
A       git-dirty-test.txt
Switched to branch 'official-master'
Your branch is up-to-date with 'official/master'.

D:\ZSDevelops\Git\git-dirty-test>git status
On branch official-master
Your branch is up-to-date with 'official/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   git-dirty-test.txt

2)这一次我们将git-dirty-test.txt文件commit到branch1中。然后切换分支到branch看一下。

这里写图片描述

D:\ZSDevelops\Git\git-dirty-test>git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

D:\ZSDevelops\Git\git-dirty-test>git branch -v
* master          9eee183 [ahead 1] git dirty file test
  official-master 5fcbcaa Merge branch 'cache-enhancements'

D:\ZSDevelops\Git\git-dirty-test>git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   git-dirty-test.txt

no changes added to commit (use "git add" and/or "git commit -a")

D:\ZSDevelops\Git\git-dirty-test>git checkout official-master
error: Your local changes to the following files would be overwritten by checkout:
        git-dirty-test.txt
Please commit your changes or stash them before you can switch branches.
Aborting

一旦执行git commit后,此时index、working directory和branch1的最后一次提交的snapshot三者同步了。此时在切换到分支branch2上。可以发现之前可以看到的在index和working directory中存储的git-dirty-test.txt文件消失了。这是因为git-dirty-test.txt文件在commit后已经归属于branch1了,按照之前讲的checkout的执行策略,属于现有分支但不属于目标分支的文件会被移除。
3)最后我们再来做一次操作,首先将分支再一次切回branch1。然后对git-dirty-test.txt文件进行修改,添加新的一行string。然后再次尝试切换分支到branch2.执行过程如下:

#切回之前的分支,这里是master分支
D:\ZSDevelops\Git\git-dirty-test>git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
#查看分支切换状态
D:\ZSDevelops\Git\git-dirty-test>git branch -v
* master          9eee183 [ahead 1] git dirty file test
  official-master 5fcbcaa Merge branch 'cache-enhancements'
#修改文件后,查看状态
D:\ZSDevelops\Git\git-dirty-test>git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   git-dirty-test.txt

no changes added to commit (use "git add" and/or "git commit -a")
#尝试切换到新的分支official-master,出现了错误
D:\ZSDevelops\Git\git-dirty-test>git checkout official-master
error: Your local changes to the following files would be overwritten by checkout:
        git-dirty-test.txt
Please commit your changes or stash them before you can switch branches.
Aborting

上述过程出现了错误,无法执行checkout执行。究其原因是因为此刻checkout原本应该执行与2)中类似的操作,将git-dirty-test.txt文件从index和working directory中移除,但是当前working directory(如果git add了话就是index)的内容还未保存到branch1分支中,还处于临时状态。而此刻如果移除git-dirty-test.txt文件,就会导致当前本地的修改丢失。因此此刻git进行了错误提示。

3. checkout错误的dirty状态

至此我们清楚了checkout是如何完成分支切换的,以及什么情况下才能切换。这也就是官方所说的只有在非“dirty”状态下才能自由执行checkout指令。官方对于dirty的描述如下:

A working tree is said to be “dirty” if it contains modifications which have not been committed to the current branch.
摘自:https://www.kernel.org/pub/software/scm/git/docs/gitglossary.html#def_dirty

这种情况在实际代码开发过程中会经常遇到,尤其是多个分支切换时。例如我们采用“主线开发、分支发版”的方式,假设已经发布了一款产品的分支为branch-publish1,此刻你在master分支上研发新的功能模块,由于还没有完工,所以没有执行git commit。突然发现了重大bug,公司要求你立刻切换到branch-publish1产品分支进行修复。最笨最笨的办法就是你把当前master的项目文件夹备份,重新建一个文件夹检出branch-publish1的代码进行修复后再commit。
但是正确的操作步骤如下:

  1. stash your current change or
  2. reset –hard HEAD (if you do not mind losing those minor changes) or
  3. checkout -f (When switching branches, proceed even if the index or the working tree differs from HEAD. This is used to throw >away local changes. ).摘自Does git “dirty” mean files not staged, or not committed? (glossary conflict)

之所以需要上述操作是因为当前你所操作的working directory和index处于dirty状态。

总结:

此篇博文着重介绍了git最常用的工作流程,对于多分支维护给出了具体的示例。通过本文的介绍已经基本能够应付日常80%的开发维护工作。后续会进一步介绍git的高级知识。






作者:[email protected]
时间:2016-07-30

猜你喜欢

转载自blog.csdn.net/zssureqh/article/details/52074287