Git命令中的restore是什么?

git-restore的作用,简单来说就是用于恢复缓存区(别名叫做索引区或index)和工作区(working directory)的资源文件,使他们回滚到上一次提交到(add操作)缓存区或上一次commit的状态。在使用命令时可以指定不同参数,达到不同效果。

使用概要

下面是使用概要,可以在看完使用方法后再回来看会更加清晰,可以先跳过:

git restore [<options>] [--source=<tree>] [--staged] [--worktree] --pathspec-from-file=<file> [--pathspec-file-nul]
复制代码

可选参数

使用简写时注意大小写。

--worktree(-W)和--staged(-S)

通过使用--worktree和--staged选项可以指定恢复的资源文件来自指定的工作树(working tree)或缓存区。如果都不指定,那么默认是当前的工作树一般是HEAD指向的commit。

--staged

假设项目里有个文件hello.txt,并且写入内容"第一次修改",然后使用git add hello.txt将文件tracked(添加到git的缓存区中)。

hello.txt文件

第一次修改
复制代码

此时hello.txt的状态是已经被添加到缓存区中,可以使用git status命令查看文件状态。

上面图中的提示代码也明确说明了可以使用git restore --staged <文件路径> 来进行文件逆向操作,将文件从缓存区中移除也就是Untracked状态。所以我们来试试,输入:

git restore --staged hello.txt // 回滚文件状态
git status ./hello.txt // 查看文件状态
复制代码

注意看文件的状态变为了Untracked files,说明文件已经被成功restore了。

--worktree

假设项目里已经有一个已经被tracked和commit过一次的文件hello.txt,且内容为"第一次修改"。此时文件的状态是在本地仓库中。

然后在文件中增加一行"第二次修改",此时文件状态变为了存在工作区。

使用git status命令查看:

因为我们的文件现在是在工作区中,所以它有两个提示,第一个提示是使用git add <文件路径> 将文件提交到缓存区中,第二个提示是使用git restore <文件路径> 将对文件的操作从工作区中抛弃。我们来试一下第二个提示命令:

git restore hello.txt
// 等同于
git restore --worktree hello.txt
// 等同于
git restore -W hello.txt
复制代码

文件成功回滚到了修改内容之前的commit状态,"第二次修改文案"消失了!

除了使用restore恢复文件内容外,还可以使用它直接恢复被误删除的文件。此时我们将hello.txt文件直接删除,然后使用git status命令查看内容:

同样有两个提示,同样提示可以使用git restore <文件路径> 来抛弃此次在工作区的更改,我们使用git restore命令来恢复文件:

git restore hello.txt
// 等同于
git restore --worktree hello.txt
// 等同于
git restore -W hello.txt
复制代码

输入命令后,被删除的文件被成功的恢复了!

如果同时使用--worktree和--staged,那么将会把缓存区和工作区一起进行恢复。

--source=<tree>(-s <tree>)

该选参的作用是将要恢复(resotre)的工作区从指定的工作树(working tree)中恢复。这个参数--source的值可以是某个commit、branch或者tag。值得注意的是:如果--source没有被指定,但是同时--staged被指定了,文件会从HEAD指向的commit中进行恢复,否则一旦--source被指定,会默认从缓存区(index)进行恢复。

举个例子:

项目中有两个commit。第一个commit增加了a.js,第二个commit增加了b.js并且删除掉了a.js。

此时项目中应该只有b.js,但是现在发现又需要a.js,所以需要在第一个commit中将文件进行恢复,所以可以使用restore命令,接下来实现一下:

// 这里使用第一个commit的hash
git restore --source=63e02b3c5bb9605f1e6c8dd6bc51b466a2699d74 a.js
// 相当于
git restore --source=63e02b3c5bb9605f1e6c8dd6bc51b466a2699d74 --worktree a.js
复制代码

在执行完命令后,我们发现a.js被成功的恢复到了项目中,并且状态是Untracked file。因为--source被指定了所以应该从缓存区中读取a.js,并且--staged和--worktree都未被指定,所以会写入到当前的工作区上,相当新创建一个a.js文件,所以文件状态是Untracked file。

接着使用--staged选参试试看:

// 这里使用第一个commit的hash
git restore --source=63e02b3c5bb9605f1e6c8dd6bc51b466a2699d74 --staged a.js
复制代码

奇怪的是,在项目中并没有发现有a.js,难道是恢复失败了吗?

使用git status a.js查看文件的状态,我们发现a.js文件先是被添加然后又被删除了。猜测是因为只在缓存区中恢复了文件,并没有在工作区中恢复文件才会导致文件状态是被删除。

所以应该恢复文件同时加上--staged和--worktree在缓存区和工作区中同时恢复文件:

// 这里使用第一个commit的hash
git restore --source=63e02b3c5bb9605f1e6c8dd6bc51b466a2699d74 --staged --worktree a.js
复制代码

此时的a.js文件的状态就是已经被写入缓存区和工作区。

--patch(-p)

使用该参数后会以一种交互式的方式来选择如何处理每一个文件的hunk(修改),在restore的源和进行restore的位置。

假设项目根目录中有一个a.js文件,我们来随便写点东西:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(this.name + ':say hi');
  }
  sayGoodBye() {
    console.log(this.name + ':say goodbye');
  }
}

const somebody = new Person('tom');
复制代码

根目录还有一个b.js文件:

console.log('b.js');
复制代码

将这两个文件commit到本地仓库中。

随后我们对工作区做两个改动,首先将a.js进行内容修改:

class Person {
  constructor(name, age) { // 新增代码
    this.name = name;
    this.age = age; // 新增代码
  }
  sayHi() {
    console.log(this.name + ':say hi');
  }
  sayGoodBye() {
    console.log(this.name + ':say goodbye');
  }
}

const somebody = new Person('tom', 28); // 新增代码
复制代码

然后直接将b.js文件删除。

这时候,如果我们想恢复文件可以使用--patch参数,来恢复想要恢复的内容:

git restore --patch
复制代码

我们看到控制台里面出现了可以交互的界面,并且询问我们是否要放弃本次修改(hunk)在当前工作树中。本次修改被分为了两个hunk,也就是分为了两段代码来让用户确认修改。这个命令和git diff所展示内容似乎是一毛一样的。

git中的每一次修改都会被认为是一个hunk,根据特定的方式来进行分割。

我们来试试使用git diff:

git diff如果不加任何参数会将所有文件的变动一次性全部输出,而使用git restore --patch会将有变动的文件一个一个询问来让我们处理是否要回滚,也就是第一张图蓝字的部分。

我们看到处理文件的选项有[y,n,q,a,d,j,J,g,/,s,e,?],这几个英文字母又代表什么意思呢?这时候就可以使用?符号在控制台打印出帮助说明,我们先来看看对应的英文说明:

y - discard this hunk from worktree

n - do not discard this hunk from worktree

q - quit; do not discard this hunk or any of the remaining ones

a - discard this hunk and all later hunks in the file

d - do not discard this hunk or any of the later hunks in the file

g - select a hunk to go to

/ - search for a hunk matching the given regex

j - leave this hunk undecided, see next undecided hunk

J - leave this hunk undecided, see next hunk

s - split the current hunk into smaller hunks

e - manually edit the current hunk

? - print help

这里一个一个选项来试并查看结果:

1.(y)

输入y将会抛弃本次hunk在工作树中的修改,接着会询问第二个hunk的处理方式:

2.(n)

n的作用就是不抛弃本次修改。

我们选择n并回车,修改的代码将会被保留下来,操作完毕后对a.js文件改动就结束了:

const Person {
  constructor(name) { // age参数被删除了
    this.name = name;
    // this.age = age 被删除了
  }
  sayHi() {
    console.log(this.name + ':say hi');
  }
  sayGoodBye() {
    console.log(this.name + ':say goodbye');
  }
}

const somebody = new Person('tom', 28); // 传入28的参数被保留了
复制代码

3.(q)

如果想退出本次restore,不对本次的hunks做任何的restore,这时候可以直接输入q。

再次回到对a.js做第一次修改的时候:

class Person {
  constructor(name, age) { // 新增代码
    this.name = name;
    this.age = age; // 新增代码
  }
  sayHi() {
    console.log(this.name + ':say hi');
  }
  sayGoodBye() {
    console.log(this.name + ':say goodbye');
  }
}

const somebody = new Person('tom', 28); // 新增代码
复制代码

然后使用git restore --patch命令,接着输入q放弃本次的所有restore操作(包括放弃后面删除b.js的restore):

4.(a)

使用a就相当于直接使用git restore不带任何参数,抛弃本次单个文件内所有的hunk做的代码修改。

一旦我们输入a并回车后,文件就会变成上一次commit的状态:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(this.name + ':say hi');
  }
  sayGoodBye() {
    console.log(this.name + ':say goodbye');
  }
}

const somebody = new Person('tom');
复制代码

这样就处理完了第一个a.js文件,接着会继续询问是否对b.js进行restore,根据自己的需要选择对应的参数就可以。

5.(d)

使用d将会接受对本文件的所有hunk修改,然后询问对下一个文件的restore:

在输入d参数后,a.js文件保留了对该文件的所有hunk,然后继续询问我们对b.js的hunk。

6.(g)

使用g命令就能不按顺序,指定想要更改哪个hunk:

在输入g后,出现了2个选项并且询问要去哪个hunk,输入2并回车,直接跳转到了第二个hunk中:

7.(/)

使用/将会以正则表达式匹配hunk修改的内容,比如如果想直接跳转到第二个hunk里面,我们可以直接输入28来匹配:

我们看到此时已经跳转到了第二个hunk,并询问我们处理的方式。

8.(J)

使用J可以暂时跳过当前hunk,去处理下一个hunk。

如上图,在处理第一个hunk时,我们输入了J来暂时跳过这个hunk的restore转而进入第二个hunk的处理。

9.(j)

j的作用和J很相似,只不过j是跳转到下一个之前跳过的未处理的hunk。在上面J的例子中,使用了J跳过了第一个hunk,此时再使用K跳过第二个hunk,返回上一个hunk,也可以使用k跳过第二个hunk,返回上一个未处理的hunk。接着可以再输入j查看结果:

如上图,成功跳转到了下一个未处理的hunk。

10.(s)

使用s可以将hunk拆分成更细粒度的多个hunks。

如上图,将第一个hunk拆分成更细粒度的两个hunks。

11.(e)

使用e选项可以手动的修改当前的hunk:

如上图,可以用类似vim编辑器去手动的修改hunk。

--ours、--theirs

当文件存在冲突(使用merge或者rebase等操作会产生冲突)处于unmerged paths,可以使用--ours或者--theirs来将文件restore成其中的一个版本。

假如有一个项目,这个项目有一个master分支和feature分支,在master分支中有一个名称为a的commit记录,这个commit中添加了一个文件weeks.txt并写入内容Monday。在feature分支中,也有一个weeks.txt文件并且写入内容为Tuesday。接着将feature分支合并到master上,此时weeks.txt文件产生了冲突:

这时如果使用git status查看weeks.txt文件:

此时weeks.txt文件的状态就是unmerged paths也就是处于冲突状态。可以使用git restore来选择需要保留的文件版本。比如如果想保留feature中的weeks.txt版本,就可以使用--thiers代表要保留merge的版本。

git restore weeks.txt --theris
复制代码

执行命令后weeks.txt文件内容变为Tuesday

如果此时想再切回master版本的代码,可以再使用--ours参数切回master:

git restore weeks.txt --ours
复制代码

这个命令跟git checkout weeks.txt --theris或--ours功能一样。

--merge(-m)

该参数可以将已经处理完成的有冲突的文件,重新还原成未处理的产生冲突的状态。

还是以上述的weeks.txt文件为例,此时如果我们已经处理完冲突,就可以使用git add weeks.txt将文件标记为已经处理完冲突。

但是现在如果想重新回到weeks.txt冲突产生的时候该怎么办呢?使用--merge就能做到:

git restore weeks.txt --merge
复制代码

使用该命令就可以回滚weeks.txt文件,重新回到master merge feature的状态。

--conflict=<style>

这个参数和--merge的作用十分相似,只不过它能让冲突以不同方式展现。<style>的默认值是"merge",可配置的值有"diff3"和"zdiff3"。

默认值merge的效果和使用--merge效果一样,直接来看diff3的效果:

在master分支有一个weeks.txt文件内容为Monday。然后使用git switch -c创建一个新的分支feature,然后修改weeks.txt内容为:

Tuesday
Common Change
复制代码

使用commit提交到本地仓库中并命名为b。这样在feature分支的commit路径为a->b。重新切回master分支修改weeks.txt内容为:

Wednesday
Common Change
复制代码

然后提交本地仓库并命名为c,在master分支的commit路径就为a->c。接着将feature分至合并到master中:

这样"Tuesday"和"Wednesday"就产生了冲突,可以使用上述提到git restore或git checkout解决冲突。随后如果想回滚weeks.txt文件重新回到合并的状态,可以使用git restore --conflict=merge或git restore --merge。

但是使用这两个命令都不能知道修改master和feature之前的,也就是它们的共同祖先-名称为"a"的提交记录的文件内容,这时候就可以使用--conflict=diff3。

git restore weeks.txt --conflict=diff3
复制代码

使用命令后:

如上图,在文件中出现三条对比路线。ours、共同的祖先||||||| base和theirs。这样就可以清楚对比本地改了哪些内容和即将合并的分支有哪些区别。

值得注意的是此时的共同修改内容"Common Change"在ours和theirs中都存在了,它是一个无需手动解决的冲突,没必要在两个内容块中都展示出来,所以可以使用第二个参数--zdiff3把Common Change的内容提取出来:

如上图,此时的Common Change被提取到了冲突代码的外面。

--ignore-unmerged

当使用git restore去还原存在冲突且文件是unmerged的状态,那么这个操作不会被中断。

假设项目中有两个分支,一个分支为master,在该分支中有一个weeks.txt文件,文件内容为"Monday Common Change"。

另一个分支为feature,在该分支有一个weeks.txt文件,文件内容为"Tuesday Common Change"。除了该文件外还有一个fruits.txt内容为"Apple":

这时候将feature分支合并到master中。

如上图,fruits.txt文件被add到了缓存区,weeks.txt文件产生了冲突需要处理。

如果这时候直接使用git restore .命令恢复所有文件是会失败的,并且restore操作会被终止。

这时候就可以加上--ignore-unmerged来忽略有冲突且没有处理的文件(冲突内容会被保留,文件会被标记为已解决):

git restore --ignore-umerged --staged .
复制代码

如上图,当前restore操作成功的进行了还原,并且weeks.txt的冲突还是存在,文件也被标记为已解决。

--overlay、--no-overlay

在使用overlay模式时,如果在还原目标中不存在该文件,那么在当前的还原位置也不会直接删除该文件。默认使用的是--no-overlay,也就是和上面的条件相反会删除不存在的文件。

假设项目中有一个master分支,分支有两个文件,第一个是weeks.tx内容是"Monday",第二个文件是fruits.txt内容是"Apple"。

项目中还有另一个feature分支,分支有一个文件weeks.txt内容是"Tuesday"。

现在如果想让master分支还原feature分支的所有文件,可以使用git restore命令来还原:

git restore . --source feature
复制代码

还原结果:

如上图,weeks.txt文件被成功还原成了feature分支中的内容,但是fruits.txt文件却被删除了。如果想同时保留fruits.txt文件不被删除就可以使用--overlay:

git restore . --source feature --overlay
复制代码

还原结果:

这时候fruits.txt文件被成功的保留了下来,并且weeks.txt被还原成了feature中的文件内容。

<pathspec>

用于限制git restore的作用范围,具体可以参考这篇文章

--ignore-skip-worktree-bits

稀疏检出模式中,默认只会更新和<pathspec>匹配的条目以及$GIT_DIR/info/sparse-checkout中声明的条目。这个选项将会忽略稀疏模式,无条件的恢复和<pathspec>匹配的条目。

--recurse-submodules、--no-recurse-submodules

如果使用**--recurse-submodules**选项,在还原工作区内容的同时还会同步还原submodules中的内容。如果没有指定该参数或者使用--no-recurse-submodules,submodules将不会进行还原,就像是使用git-checkout命令一样,HEAD指针将会和submodules进行分离。

假设项目中有该如下结构:

如上图,项目中有一个submodule,在submodule里面有一个README.md文件且没有任何内容。接下来随便写点内容做一些更改:

此时在README.md文件中增加了"project init",如果这时候使用restore命令进行还原:

git restore .
复制代码

项目中的submodules是不会有任何变化的:

在执行命令后,README.md文件还是和修改后的样子一样。如果想要进行还原submodule则可以使用--recurse-submodules选项:

git restore . --recurse-modules
复制代码

执行命令后:

README.md文件成功变成了上一次commit的状态。

--pathspec-from-file=<file>

在上面指定restore的<pathspec>都是来自命令行参数,然而也可以指定它来自某一个文件。

假设项目中有如下结构:

a.txt、b.txt和c.txt的内容都是空的,然后在pathspec.txt中写入要还原的:

a.txt
b.txt
复制代码

如上所示,目标是还原a.txt和b.txt,而c.txt不进行还原。多个使用换行符进行分隔,每一行都是一个独立的。

然后在a.txt、b.txt以及c.txt分别写入各自的文件名称字符串:

最后尝试使用resotre命令进行还原:

git restore --pathspec-from-file=pathspec.txt
复制代码

执行命令后:

如图所示,a.txt和b.txt被成功还原了,但是c.txt还是保持修改后的样子,这说明--pathspect-from-file选项生效了。

--pathspec-file-nul

只有和--pathspec-from-file才有意义,指定使用NUL字符作为分隔符而不是换行符。

猜你喜欢

转载自juejin.im/post/7193648925000073274