Git版本控制管理——文件管理和索引

之前提到,Git的操作结束后需要提交才会真正起作用。这是因为Git在工作目录和版本库之间加了一层索引,用来暂存(stage),收集或修改。这也是其它文章称工作目录和版本库中间的一层为暂存区的原因。

当使用Git进行代码管理时,工作目录进行编辑,索引中积累更改,然后将索引中的修改一次性提交到版本库。

索引

Git中的索引不包含任何文件内容,它只追踪想要提交的内容。即当执行git commit命令时,Git会检查索引而不是工作目录来找到提交的内容。

在任何时候都可以通过git status命令查看索引的状态,该命令会列出哪些文件是暂存的。

当然也可以通过底层命令如git ls-files来查看Git的状态。

同样在索引中,也可以使用git diff命令来对比差异:

  • git diff:显示仍留在工作区域且未暂存的变更
  • git diff --cached:显示已经暂存并有助于提交的变更

Git中的文件分类

Git中将所有文件分为三类:

  • 已追踪的
  • 被忽略的
  • 未追踪的

已追踪的(Tracked)

表示已经在版本库中的文件,或者是已暂存到索引中的文件。

之前也提到过新添加的文件需要首先执行git add命令,该命令就是将文件添加为已追踪的文件。

被忽略的(Ignored)

被忽略的文件必须在版本库中被明确声明为不可见或被忽略,即使该文件可能会在工作目录中出现。

比如实际开发中的库文件,这些文件可能只是为了验证是否能够通过编译,便不需要在版本库中追踪,同时也是为了节约空间。Git会维护一个默认忽略文件列表,也可以配置版本库来识别其它文件。

未追踪的(Untracked)

未追踪的文件是指不在上述两种文件范围中的文件。

比如在初始化一个空版本库时:

$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

上述内容表示该版本库没有任何提交。

然后新建一个文件:

$ touch memo.txt
$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        memo.txt

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

上述内容表示当前工作目录存在未追踪的文件memo.txt。

而如果对该文件执行git add命令:

$ git add memo.txt
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   memo.txt

上述内容表示meme.txt已经追踪到了,此时可以提交了。

最后提交该文件:

$ git commit -m "commit memo.txt"
[master (root-commit) 7356634] commit memo.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 memo.txt

$ git status
On branch master
nothing to commit, working tree clean

此时整个工作目录就变成干净的了,这个过程也对应一个文件从新建到提交到版本库的基本过程。

而如果工作目录中的文件memo.o不想被追踪,则为了让Git忽略该文件,可以将该文件添加到.gitignore文件中:

$ touch memo.o
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        memo.o

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

$ echo memo.o > .gitignore
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gitignore

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

上面的代码中新建了memo.o文件,此时该文件是未追踪的,之后又新建了.gitignore文件,并将memo.o写入了该文件,此时memo.o文件就不再是未追踪的了,而变成了被忽略的文件。但.gitignore文件此时变成了未追踪的,这是因为该文件和版本库中的任何普通文件都是同样管理的,除非把该文件添加到索引中,否则就要接受Git的管理。

git add

之前提到过git add可以暂存一个文件,该命令会将未追踪的文件转换为已追踪的。而如果git add作用于一个目录名,则会递归暂存该目录下的文件和子目录。

比如上一段显示.gitignore文件是未追踪的,暂存该文件:

$ git add .gitignore
warning: LF will be replaced by CRLF in .gitignore.
The file will have its original line endings in your working directory

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .gitignore

而在Git的对象方面,在发出git add命令时每个文件的全部内容都会被复制到对象库中,并且按照文件的散列值进行索引。

可以使用git ls-files命令查看隐藏在对象模型下的内容,并找到暂存文件的散列值:

$ git ls-files --stage
100644 9c6636ec568867d742b61b7eb2074e81650429ff 0       .gitignore
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       memo.txt

上面的命令显示当前索引中存在两个文件,分别是memo.txt和.gitignore。

而之前也提到过,当使用git add命令时,索引会发生更新。这也是实际使用中建议的使用方法,在提交之前,应执行git add命令用最新版本的文件更新索引。否则将会得到两个不同版本的文件:

  • 一个在对象库中被捕获并被索引引用的
  • 一个在工作目录

这里使用git hash-object命令来看一下文件修改前后的变化。

NAME
git-hash-object - Compute object ID and optionally creates a blob from a file

SYNOPSIS
git hash-object [-t <type>] [-w] [--path=<file>|--no-filters] [--stdin [--literally]] [--] <file>…​
git hash-object [-t <type>] [-w] --stdin-paths [--no-filters]
DESCRIPTION
Computes the object ID value for an object with specified type with the contents of the named file (which can be outside of the work tree), and optionally writes the resulting object into the object database. Reports its object ID to its standard output. When <type> is not specified, it defaults to "blob".

该命令可以用来计算一个对象的ID。

首先再看一下当前暂存文件的散列值:

$ git ls-files --stage
100644 9c6636ec568867d742b61b7eb2074e81650429ff 0       .gitignore
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       memo.txt

如果修改memo.txt文件的话:

$ echo abc > memo.txt
$ git hash-object memo.txt
8baef1b4abc478178b004d62031cf7fe6db6f903

此时memo.txt的散列值发生了变化,然后使用git add暂存该修改:

$ git add memo.txt
$ git ls-files --stage
100644 9c6636ec568867d742b61b7eb2074e81650429ff 0       .gitignore
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0       memo.txt

从上面也可以看出,memo.txt对应的散列值发生了更新,变得和之前计算得到的散列值一致了,这也表示暂存了更新后的文件。

同时Git的提交使用的索引中的文件版本,也就是执行过git add的文件版本,这也是为什么建议在修改后要执行git add的原因。

git commit

git add之后,文件就成为了已追踪的,此时如果修改结束,就可以进行提交了。

git commit --all

git commit的-a或--all选项会将之前自动暂存的,所有未暂存的和未追踪的文件变换,也包括删除已追踪的文件。

为了显示git commit的--all会有什么作用,执行下面的代码:

git init
echo abc > file1
echo abcd > file2
git add file1 file2
git commit -m "commit file1 file2"
echo abcde > file1
git add file1
echo abcdef > file2
touch file3
echo abcdefg > file3
mkdir subdir
echo abcdefgh > subdir/file4

上面的代码初始化了空版本库,然后新建了file1,file2,并完成了提交,然后修改了file1,file2,并对file1进行暂存,完成索引更新,还创建了file3,最后创建了subdir子目录,并在该目录下新建了file4。

此时的file1应是暂存的,file2是未暂存的,file3,file4则是未追踪的,使用git status查看版本库状态:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   file1

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   file2

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file3
        subdir/

然后执行git commit --all命令:

$ git commit --all -m "commit --all"
warning: LF will be replaced by CRLF in file2.
The file will have its original line endings in your working directory
[master 3d8c97b] commit --all
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file3
        subdir/

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

此时,Git会遍历整个版本库,暂存所有已知的和修改的文件,然后提交。而file3是未追踪的,subdir是新子目录,而该目录下没有任何文件名或路径是已追踪的,因此file3和subdir子目录仍然不会提交。

而如果在提交时未设置或未正确设置提交日志,都会导致提交失败:

$ git commit -m
error: switch `m' requires a value

$ git commit -m ""
Aborting commit due to empty commit message.

这是因为Git不会处理空提交,即提交日志为空的提交。

git rm

既然可以使用git add将文件进行暂存,并在后续进行提交,就也应该可以删除文件。

Git提供了git rm命令,但该命令会同时从版本库和工作目录中同时删除文件。Git也可以从索引或者同时从索引和工作目录中删除一个文件。但Git不会只从工作目录中删除一个文件,这种操作可以通过系统命令rm来执行。

从工作目录或索引中删除一个文件,并不会删除该文件在版本库中的历史记录。文件的任何版本,只要是提交到版本库的历史记录的一部分,就会留在对象库中并保存到历史记录。

git init
echo abc > file1
echo abcd > file2
echo abcde > file3
git add file2 file3
git commit file3 -m "commit file3"

执行上面的代码后的状态为:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   file2

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file1

此时file1只存在于工作目录,file2位于暂存区,file3位于版本库:

$ git ls-files --stage
100644 acbe86c7c89586e0912a0a851bacf309c595c308 0       file2
100644 00dedf6bd5f3e493ce8b03c889912f47b01297d4 0       file3

而想要删除file1,就是从工作目录删除:

$ rm file1

而此时版本库状态为:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   file2

从上面看,file1确实是删除了。

而想要从暂存区中删除file2,即将该文件由已暂存转化为未暂存的:

$ git rm --cached file2

而此时版本库状态为:

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file2

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

从上面内容来看,file2确实从暂存区中删除了,变为了未追踪的文件,但此时该文件还存在于工作目录中。

而想要删除提交文件file3:

$ git rm file3
$ git commit file3 -m "commit rm file3"

此时执行git rm,然后提交该请求即可,此时的版本库状态为:

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file2

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

此时会在索引和工作目录中均删除该文件。

不过Git在删除一个文件之前,首先会检查以确保工作目录下该文件的版本和当前分支中的最新版本(HEAD)是匹配的,以防止修改操作丢失。

而若在执行git rm file3之后,提交该请求之前,想要撤销该操作,就需要执行:

$ git checkout HEAD -- file3

该命令的含义为:

git-checkout(1) Manual Page
NAME
git-checkout - Switch branches or restore working tree files

SYNOPSIS
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>] [--] <pathspec>…​
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
git checkout (-p|--patch) [<tree-ish>] [--] [<pathspec>…​]
DESCRIPTION
Updates files in the working tree to match the version in the index or the specified tree. If no pathspec was given, git checkout will also update HEAD to set the specified branch as the current branch.

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>.

If <branch> is not found but there does exist a tracking branch in exactly one remote (call it <remote>) with a matching name and --no-guess is not specified, treat as equivalent to

$ git checkout -b <branch> --track <remote>/<branch>
You could omit <branch>, in which case the command degenerates to "check out the current branch", which is a glorified no-op with rather expensive side-effects to show only the tracking information, if exists, for the current branch.

git checkout -b|-B <new_branch> [<start point>]
Specifying -b causes a new branch to be created as if git-branch(1) were called and then checked out. In this case you can use the --track or --no-track options, which will be passed to git branch. As a convenience, --track without -b implies branch creation; see the description of --track below.

If -B is given, <new_branch> is created if it doesn’t exist; otherwise, it is reset. This is the transactional equivalent of

$ git branch -f <branch> [<start point>]
$ git checkout <branch>
that is to say, the branch is not reset/created unless "git checkout" is successful.

git checkout --detach [<branch>]
git checkout [--detach] <commit>
Prepare to work on top of <commit>, by detaching HEAD at it (see "DETACHED HEAD" section), and updating the index and the files in the working tree. Local modifications to the files in the working tree are kept, so that the resulting working tree will be the state recorded in the commit plus the local modifications.

When the <commit> argument is a branch name, the --detach option can be used to detach HEAD at the tip of the branch (git checkout <branch> would check out that branch without detaching HEAD).

Omitting <branch> detaches HEAD at the tip of the current branch.

git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>…​
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
Overwrite the contents of the files that match the pathspec. When the <tree-ish> (most often a commit) is not given, overwrite working tree with the contents in the index. When the <tree-ish> is given, overwrite both the index and the working tree with the contents at the <tree-ish>.

The index may contain unmerged entries because of a previous failed merge. By default, if you try to check out such an entry from the index, the checkout operation will fail and nothing will be checked out. Using -f will ignore these unmerged entries. The contents from a specific side of the merge can be checked out of the index by using --ours or --theirs. With -m, changes made to the working tree file can be discarded to re-create the original conflicted merge result.

git checkout (-p|--patch) [<tree-ish>] [--] [<pathspec>…​]
This is similar to the previous mode, but lets you use the interactive interface to show the "diff" output and choose which hunks to use in the result. See below for the description of --patch option.

也就是说,上述命令会使用HEAD中的file3来覆写file3,也就达到了撤销操作的目的。

也可以再看一下版本库状态:

$ git status
On branch master
nothing to commit, working tree clean

此时连带着之前git rm的请求都会删除掉。

git mv

之前提到过重命名的操作,一般说来,Git中的重命名会有两种方法:

mv file newfile
git rm file
git add newfile

或者是

git mv file newfile

虽然两种重命名的逻辑不同,但Git都会在索引中删除file的路径名,并添加newfile的路径名,而file的原始内容,则仍保存在对象库中,然后会将之与newfile重新关联。

$ git init
$ echo abc > file
$ git add file
$ git commit file -m "commit file"
$ git mv file newfile

执行上面的代码后,版本库状态为:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        renamed:    file -> newfile

如果提交该请求:

$ git commit newfile -m "commit newfile"
warning: LF will be replaced by CRLF in newfile.
The file will have its original line endings in your working directory
[master 05585b2] commit newfile
 1 file changed, 1 insertion(+)
 create mode 100644 newfile

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        deleted:    file

$ git commit file -m "commit file"
[master 9f1d1da] commit file
 1 file changed, 1 deletion(-)
 delete mode 100644 file

$ git status
On branch master
nothing to commit, working tree clean

从上面看,如果只提交file或newfile的话会在暂存区留下另外一个待提交的请求,因此重命名需要不指明文件直接使用git commit提交。

而如果想要查看重命名后的文件修改历史:

$ git log newfile
commit 05585b27da5cd420a2c3c5815753d62f838068c8
Author: wood_glb <wood_glb@git.com>
Date:   Sat Jun 18 20:46:33 2022 +0800

    commit newfile

可以看到,这里只有重命名后的newfile的修改历史,而没有之前的file历史。

其实Git是保存全部的历史记录的,但是显示会受到命令中指定的文件名的显示,而--follow选项会让Git在日志中回溯并找到与内容相关联的整个历史记录:

$ git log --follow newfile
commit 05585b27da5cd420a2c3c5815753d62f838068c8
Author: wood_glb <wood_glb@git.com>
Date:   Sat Jun 18 20:46:33 2022 +0800

    commit newfile

commit bcbc546be89ae18bc85a4acc5cfb766c09d98d96
Author: wood_glb <wood_glb@git.com>
Date:   Sat Jun 18 20:45:48 2022 +0800

    commit file

.gitignore文件

之前提到.gitignore文件可以忽略某些文件。其实.gitignore文件可以存在于:

  • 要忽略文件的同一目录,此时该文件只影响该目录及其所有子目录
  • 版本库顶层目录中

即可以在项目的任一级子目录中构建.gitignore文件选择忽略的文件,这样可以避免在版本库顶层目录中设置过程的路径名。

同时.gitignore文件的格式为:

  • 空行会被忽略,而以#开头的行可以用于注释
  • 一个简单的文件名匹配任何目录中的同名文件
  • 目录名由末尾的反斜线/标记,这能匹配同名的目录和子目录,但不匹配文件或符号链接
  • 包含shell通配符,如*,可扩展为shell通配模式,但一个*只能匹配一个文件或目录名
  • 起始的!会对该行其余部分的模式进行取反,并且被之前模式排除但被取反规则匹配的文件是要包含的,取反模式会覆盖低优先级的规则。

既然每个目录都可以存在.gitignore文件,那么不同目录的.gitignore文件的匹配优先级为:

  • 在命令行上指定的模式
  • 从相同目录的.gitignore文件中读取的模式
  • 上层目录中的模式,优先级递减,即当前目录优先级>上一级目录优先级>上上一级目录优先级
  • 来自.git/info/exclude文件指定的模式
  • 来自配置变量core.excludedfile指定的文件中的模式

因为在版本库中.gitignore文件被视为普通文件,因此在复制过程中该文件会被复制,并使用于本地版本库的所有副本。

而如果某种忽略模式特定于本地的版本库,并且不应适用于其它人复制的版本库,那么该模式应放到.git/info/exclude文件中,因为该文件在复制时不会随之复制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值