Git 基本操作

以下是 Git 的基本操作。如果你是个人开发者,并且不打算使用云平台,那么这一节可以满足你的绝大部分需求。

git init

首先我们需要新建一个 Git 仓库来管理一个目录里的文件,运行:

$ cd ~/test
$ git init
Initialized empty Git repository in 某某/test/.git/

仓库就是 .git 这个隐藏目录,用来备份文件和存放其他信息。

当然也可以:

$ mkdir test_repo
$ git init test_repo
Initialized empty Git repository in 某某/test_repo/.git/

Git 基本概念

仓库管理的目录,不包括其中的 .git,被 Git 称作工作树(working tree)。Git 内部把目录叫做树。

当你想让 Git 备份工作树里某些文件的时候,你可以把这些文件暂存(stage)。Git 暂存区有三个英文名:staging area、cache 以及 index,这些名字都是一个意思。注意,Git 不会暂存空目录。

Git 可以跟踪(track)暂存区里的文件,为你显示这个文件工作树和暂存区版本之间的区别,或者在你需要时从暂存区恢复工作树文件。如果一个文件只在工作树里,不在暂存区里,它就是 Git 未跟踪的(untracked);反之,它就是跟踪的(tracked)。

由于暂存区里的文件容易被修改,而且不能保存一个文件的多个版本(合并时除外),所以 Git 提供了一个叫做提交(commit)的操作来永久备份暂存区里的文件。你可以多次提交来保存项目的每个版本。

通常情况下,一个有经验的开发者会频繁地暂存工作树里的修改,改错了就从暂存区恢复工作树里的文件。提交也应该尽可能频繁,以便需要时从历史提交恢复文件。但每个提交都是署名且不可修改的,所以交之前要慎重。一个比较合适的提交策略是每实现一个功能或者修复一个问题就提交一次。

git status

Git 管理的目录里运行:

$ git status
On branch master

No commits yet

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

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

分支(branch)master 我们可以先忽略。可以看到所有文件都是未跟踪的。想要暂存它们,Git 告诉我们,运行:

$ git add empty_file files hello_file
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:    empty_file
	new file:    files
	new file:    hello_file

「Changes to be committed」就是暂存区和当前提交之间的区别。由于我们还没有任何提交,所有暂存区里的文件都显示为「new file」。

修改一下工作树:

$ echo oops > empty_file
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:    empty_file
	new file:    files
	new file:    hello_file

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:   empty_file

「Changes not staged for commit」就是工作树里跟踪的文件与它们暂存区版本之间的区别。

你可以根据提示,使用 git restore 文件名 或者 git restore --staged 文件名 恢复文件。

.gitignore

有些时候你希望 Git 忽略一些文件,比如编译结果、某些缓存以及操作系统生成的元数据。Git 会在每次你运行 git status 时显示它们未跟踪。如果想让 Git 不显示这些文件,你需要把它们加到 .gitignore 中:

$ echo to_be_ignored >> .gitignore
$ touch to_be_ignored
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:    empty_file
	new file:    files
	new file:    hello_file

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

然后再暂存 .gitignore 就可以了。

除了在当前目录中放 .gitignore,你也可以在子目录里放它,这样只会忽略子目录里的文件。另外,你还可以编辑 ~/.gitignore;这里的修改会应用于所有 Git 管理的目录。

macOS 会自动在每个目录里生成 .DS_Store 文件;Windows 也会在某些目录里生成 Desktop.ini 文件。这些文件不应该被 Git 管理,所以建议你运行:

$ echo .DS_Store >> ~/.gitignore

或者:

$ echo Desktop.ini >> ~/.gitignore

git commit

提交现在的暂存区,运行:

$ git commit -m "Initial commit."
[master (root-commit) dfb9362] Initial commit.
 3 files changed, x insertions(+)
 create mode 100644 empty_file
 create mode 100644 files
 create mode 100644 hello_file

选项 -m 后面的是这个提交的 message,让别人快速了解你都做了哪些修改。如果你没有加 -m,你会进入之前配置好的编辑器;写好 message 后保存并退出,Git 就会提交。这里的 dfb9362 是这个提交的哈希值。你的哈希值可能和我的不太一样,因为提交时间、提交者的名字和邮箱都会用于哈希值计算。

这时再运行:

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

git log

再做一个提交:

$ echo just nothing >> empty_file
$ git add empty_file
$ git commit -m "Second commit."
[master 0b8cf86] Second commit.
 1 file changed, 2 insertions(+)

运行:

$ git log
commit 0b8cf86c68a62eabf74f65424bfc0be496a1d10 (HEAD -> master)
Author: 你的名字 <你的邮箱>
Date:   ??? ??? ?? ??:??:?? ???? +????

    Second commit.

commit dfb9362516fc00bd9ea063559c89ee8dc97a5c36
Author: 你的名字 <你的邮箱>
Date:   ??? ??? ?? ??:??:?? ???? +????

    Initial commit.

Git 打印出了你的提交记录。HEAD 就是「当前」的提交。每次提交后 HEAD 都会指向新的提交。

git tag

当你的项目取得阶段性进展时,你可以给目前的版本编一个号。运行:

$ git tag v1.0
$ git log
commit 0b8cf86c68a62eabf74f65424bfc0be496a1d10 (HEAD -> master, tag: v1.0)
Author: 你的名字 <你的邮箱>
Date:   ??? ??? ?? ??:??:?? ???? +????

    Second commit.

commit dfb9362516fc00bd9ea063559c89ee8dc97a5c36
Author: 你的名字 <你的邮箱>
Date:   ??? ??? ?? ??:??:?? ???? +????

    Initial commit.

引用提交

当你引用一个提交时,告诉 Git 这个提交哈希值的前几位。比如:

$ git log dfb9362516fc00
commit dfb9362516fc00bd9ea063559c89ee8dc97a5c36
Author: 你的名字 <你的邮箱>
Date:   ??? ??? ?? ??:??:?? ???? +????

    Initial commit.

就会把 dfb9362516fc00 及其之前所有的提交列出来。

除此之外,你还可以:

$ git log HEAD~
commit dfb9362516fc00bd9ea063559c89ee8dc97a5c36
Author: 你的名字 <你的邮箱>
Date:   ??? ??? ?? ??:??:?? ???? +????

    Initial commit.

~ 代表 HEAD 的上一个提交。试一试:

git diff

如果我们再做一些修改:

$ touch new_file
$ echo new_file >> files
$ git status
On branch master
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:   files

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

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

如果想要看到某文件现在的版本和暂存的版本之间的区别,运行:

$ git diff files
diff --git a/files b/files
index f2d4e4a..f31f5fe 100644
--- a/files
+++ b/files
@@ -2,3 +2,4 @@ empty_file
 files
 hello_file
 test2
+new_file

加号 + 后面的是增加的行,如果碰到减号 - 后面就是删掉的行。

如果要查看暂存版本和提交版本之间的区别,运行:

$ git add files new_file
$ git diff --cached
diff --git a/files b/files
index f2d4e4a..f31f5fe 100644
--- a/files
+++ b/files
@@ -2,3 +2,4 @@ empty_file
 files
 hello_file
 test2
+new_file
diff --git a/new_file b/new_file
new file mode 100644
index 0000000..e69de29

根据 git diff --help 或者 man git diff,试一试:

git restore

如果我们想从之前的某个提交中恢复一个文件应该怎么办呢?运行:

$ git diff HEAD~
diff --git a/empty_file b/empty_file
index b8008a4..60b9387 100644
--- a/empty_file
+++ b/empty_file
@@ -0,0 +1,2 @@
+nothing
+just nothing
$ git restore -s HEAD~ empty_file
Updated 1 path from b99d3bb
$ git status
On branch master
Changes not staged for commit:
  (use "git restore --staged <file>..." to unstage)
	modified:   empty_file

$ git diff
diff --git a/empty_file b/empty_file
index b49604a..e69de29 100644
--- a/empty_file
+++ b/empty_file
@@ -1,2 +0,0 @@
-nothing
-just nothing

git restore -s 某提交 某文件名 会从该提交中恢复工作树文件。

git restore 的选项 --staged--worktree 以及 --source=-s 是缩写)在 man git restoregit restore --help 里介绍得非常清楚。以下是一些常见用法:

部分暂存和恢复

如果你只想暂存一个工作树文件的一部分怎么办?试试运行 git add -p

$ ls --help > ls_help.txt
$ git add ls_help.txt
$ echo We are editing >> ls_help.txt
$ git add -p ls_help.txt
diff --git a/ls_help.txt b/ls_help.txt
index 3a169bb..efd450e 100644
--- a/ls_help.txt
+++ b/ls_help.txt
@@ -136,3 +136,4 @@ Exit status:
 GNU coreutils online help: 
 Full documentation 
 or available locally via: info '(coreutils) ls invocation'
+We are editing
(1/1) Stage this hunk [y,n,q,a,d,e,?]? 

Git 会问你要不要暂存这一块(hunk)修改。输入问号 ? 获取帮助:

(1/1) Stage this hunk [y,n,q,a,d,e,?]? ?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
e - manually edit the current hunk
? - print help
@@ -136,3 +136,4 @@ Exit status:
 GNU coreutils online help: 
 Full documentation 
 or available locally via: info '(coreutils) ls invocation'
+We are editing
(1/1) Stage this hunk [y,n,q,a,d,e,?]? 

有时你的可用命令还会更多。

想要获得精细的控制,输入字母 e,你就会在你选择的编辑器里看到:

# Manual hunk edit mode -- see bottom for a quick guide.
@@ -136,3 +136,4 @@ Exit status:
 GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
 Full documentation <https://www.gnu.org/software/coreutils/ls>
 or available locally via: info '(coreutils) ls invocation'
+We are editing
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# If the patch applies cleanly, the edited hunk will immediately be marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.

直接把有加号 + 的行删掉就可以不暂存它们;如果遇到减号 -,把这些行之前的减号改成空格就可以不在暂存区里删掉它们。

同理,你也可以使用 git restore -p 来恢复文件的某部分。在恢复时,你需要把加号 + 改成空格或者删掉有减号 - 的行来取消恢复某一行。

如果你要暂存未跟踪的文件,你会发现直接 git add -p 不起作用。你需要先运行 git add -N 文件名,在暂存区新建一个空文件,然后再 git add -p