Git and Github (Part I)

My Question in The First Place


为什么每次在 git push 之前必须先 git pull 一下?

git status
git add .
git commit -m "commit"
git pull origin main
git push origin main

这是最早困惑我的问题。但回过头来想一想并不难理解为什么要这么做。因为 git pull 操作相当于 git fetchgit merge 两步操作的合并。如下:

git pull origin main
# is the same as:
git fetch origin main
git merge

因为 Git 是一个版本控制系统,所以在 git push 之前需要先拉取远程更新并解决冲突。什么意思呢?就是说在你 git pull 一个新版本时,需要先检查当前版本中的文件是否有修改,确定需要保留什么、更新什么,解决冲突后再提交修改,最后通过 git push 上传到远程仓库。

从云端拉取当前版本信息的操作就是 git fetch 这时候会生成一个 .lock 文件,以免别人对版本做出修改(也就是 Git 操作的原子性)。之后你用 git merge 修改需要修改的,保留需要保留的。然后生成一个新版本并上传云端,即 git push

Git: A Distributed Version Control System


Version Control System

Git 是一种开源的分布式版本控制系统。虽然说我们有必要学习一下什么是版本控制系统,但是版本控制在我们生活中太常见了,到处都是。通过版本控制,你就能很方便地回溯到以前的某个版本。

版本控制系统分为两种,集中式版本控制系统和分布式版本控制系统。集中式版本控制系统的所有版本历史都存储在一个中央服务器上。而分布式版本控制系统中,每个开发者在其存储库中都包含项目的完整版本历史。Git 就是一个开源免费的分布式版本控制系统。

Git 的特点就是并不依赖一个中央服务器,而且有一个远程仓库用于共享代码。

Creating A Version

要使用版本控制系统,你需要不停的创建新的版本。下面,我们来了解一下如何创建一个版本。

git init

要创建版本,你需要先初始化一个 Git repo 。当你运行 git init 后,你的当前目录就会生成一个隐藏的 .git 文件夹。这个文件夹存储着所有版本控制所需要的元数据(如提交历史、分支、标签和配置等),通过这些元数据,Git 就能跟踪当前 repo 都做了哪些改动,实现版本控制。

初始化 Git repo 后,当前目录就会称为工作区(working directory),你可以在当前的工作区中修改文件。然后通过 git addgit commit 对修改的文件进行提交,纳入版本控制。

如果重复运行 git init 就会彻底地重置 repo 。

在最开始的时候我创建了一个 LearnGit 的文件夹,并创建了一个 file1.cpp 文件。

du@DVM:~/LearnGit$ ls
file1.cpp

使用 git init ,一个 repo 就初始化好了。

du@DVM:~/LearnGit$ git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint: 
hint:   git config --global init.defaultBranch <name>
hint: 
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint: 
hint:   git branch -m <name>
Initialized empty Git repository in /home/du/LearnGit/.git/

加入 -A 参数查看当前文件夹下的内容,我们确实得到了一个 .git 文件,现在 Git 会跟踪你对对当前 repo 做出的所有修改。

du@DVM:~/LearnGit$ ls -A
file1.cpp  .git

Identity Configuration

在版本控制系统中,每一次提交都会记录作者的信息,而且在团队开发中,明确每个人的更改记录能够有助于问题出现时能够快速解决造成问题的人。所以在提交版本前,你需要先配置你的邮箱和姓名。

你可以加上 --global 配置全局的个人信息。(不需要初始化 repo)

git config --global user.name "Your Name"
git config --global user.email "email@example.com"

git config --global --list

也可以仅仅配置当前 repo 的个人信息,这样在该 repo 中的提交会使用此专用信息。(需要先初始化 repo )

git config --local user.name "Your Name"
git config --local user.email "email@example.com"
git config --local --list 

git status

使用 git status 可以显示自上次 commit 后当前 repo 中所有的修改。它提示我们这里的 file1.cpp 文件还尚未暂存(untracked files)。

du@DVM:~/LearnGit$ git status
On branch master

No commits yet

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

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

git add <file|folder>

git add 的作用是将哪些文件暂存到暂存区。我们使用 git add file1.cpp 将文件 file1.cpp 暂存到暂存区里面。我们再创建一个 file2.cpp,查看一下 status :

# git add . 就是把当前文件夹中的所有文件都加入到暂存区里面,这个时候工作区只有 file1.cpp。相当于 git add file1.cpp
du@DVM:~/LearnGit$ git add .
du@DVM:~/LearnGit$ git status
On branch master

No commits yet

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

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

我们可以见得 file1.cpp 已经放到暂存区了,而 file2.cpp 仍然在工作区。我们提到了工作区、暂存区,这两个区到底是什么?

git add

git reset

git commit

git checkout

Working Directory

Staging Area

Repository

Git 工作流牵扯到四个区域:工作区、暂存区、版本库和远程仓库。我们先解释一下前三者。我们编辑文件的地方就是工作区,所有的改动最早就发生在这里,但 Git 不会跟踪这些修改。你也在上面看到了 Untreacked files ,这里反应的就是工作区,相当于草稿纸。

文件的工作完成后,我们将其加入到暂存区,暂存区是一个检查点,用于选择性地提交文件,相当于待提交清单。到了后面,一旦暂存区提交到版本库中,这些变动就会永久地保存为一个版本快照,即一个全新的版本。版本库就相当于一个存档册。

git reset <file|folder>

相当于 git add 的逆向操作,将文件从暂存区移回工作区。

git commit -m "Messages"

修改完成后,一天的工作可能到此结束。我们需要提交暂存区中的文件。我们使用 git commit -m "message" 来带有附加信息地提交已暂存文件。至此,一个版本诞生了!

du@DVM:~/LearnGit$ git commit -m "Version1"
[master (root-commit) 1b6d4d0] Version1
 1 file changed, 6 insertions(+)
 create mode 100644 file1.cpp
du@DVM:~/LearnGit$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file2.cpp

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

你可以用 git log 查看已提交的版本信息:

du@DVM:~/LearnGit$ git log
commit 1b6d4d07b9d608fb7082ce804a8b3b4cbad6d56d (HEAD -> master)
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:06:09 2025 +0800

    Version1

如果提交后发现文件漏了或者是信息填写错误,我们可以使用 --amend 进行补救。假如文件 file2.cpp 本应一并提交的,但忘记添加到暂存区里面了,我们就可以:

du@DVM:~/LearnGit$ git add file2.cpp
du@DVM:~/LearnGit$ git commit --amend --no-edit # No change to commit messages

如果你发现原先提交的提交信息写错了,我们仍然使用 --amend 。如:git commit --amend -m "correct messages"。这里我不做修改。

现在,我们有一个版本了,我们再来多创建几个版本。

Two More

在第二个版本中,我们对两个文件都进行了修改。加入暂存区、提交。第二个版本也创建好了。

第三个版本中,我们添加一个 file3.cpp 文件,然后对所有的文件都进行修改。加入暂存区、提交。第三个版本也创建好了。

du@DVM:~/LearnGit$ git log
commit 8971aa2d544055f35807f30e7ca1916717d25fd4 (HEAD -> master)
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:31:18 2025 +0800

    Version3

commit 795279eb96afbd381e598a5b4e880b6341af9d50
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:28:47 2025 +0800

    Version2

commit d4ae88f7c9b6ac9c0e789c3a3a7812ed12e4c7f6
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:06:09 2025 +0800

    Version1

如果这个时候,我们发现版本三有大问题,需要回溯到版本二重新修改提交。既然我们在用的是版本控制系统,回溯版本肯定没有任何问题。

git checkout -- <Commit Hash>

在我们使用 git log 时,每个 commit 都会对应一个提交哈希。要回溯版本,你只需要复制哈希值并使用 git checkout -- <Hash> 回溯到旧版本。

du@DVM:~/LearnGit$ git log --all
commit 8971aa2d544055f35807f30e7ca1916717d25fd4 (master)
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:31:18 2025 +0800

    Version3

commit 795279eb96afbd381e598a5b4e880b6341af9d50 (HEAD)
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:28:47 2025 +0800

    Version2

commit d4ae88f7c9b6ac9c0e789c3a3a7812ed12e4c7f6
Author: YourName <email@example.com>
Date:   Sat Mar 29 22:06:09 2025 +0800

    Version1

这里,你会发现 HEAD 移动到 Version2 commit 后面了,这个 HEAD 是干嘛的?HEAD 是一个指向某个提交版本或指向分支的指针(如果你只有一个分支,一般就用main/master来指代最新的版本,你由此会看到 HEAD -> master)。

在 Version2 ,我们添加 file4.cpp 、保存到暂存区然后提交(附加 "new Version3")。这样,我们就有一个分支了(branch)。

du@DVM:~/LearnGit$ git log --all --graph
* commit 197d5496fb2680446e9a034c759a8ddc32bddb8f (HEAD)
| Author: YourName <email@example.com>
| Date:   Sat Mar 29 22:40:45 2025 +0800
| 
|     new Version3
|   
| * commit 8971aa2d544055f35807f30e7ca1916717d25fd4 (master)
|/  Author: YourName <email@example.com>
|   Date:   Sat Mar 29 22:31:18 2025 +0800
|   
|       Version3
| 
* commit 795279eb96afbd381e598a5b4e880b6341af9d50
| Author: YourName <email@example.com>
| Date:   Sat Mar 29 22:28:47 2025 +0800
| 
|     Version2
| 
* commit d4ae88f7c9b6ac9c0e789c3a3a7812ed12e4c7f6
  Author: YourName <email@example.com>
  Date:   Sat Mar 29 22:06:09 2025 +0800
  
      Version1

在回退到旧版本后,用 git log 并不会显示旧版本之后版本的信息。你需要使用 git log --all 来显示所有版本的信息。

Visualizing Git with VS Code

你可以使用 VS Code 或其他的 IDE 更高效地使用 Git。

Restore A Version (Update A Old Version)

Git Ignore

.gitignore 是除了 .git 文件外另一个重要的配置文件。.git 用于告知 Git 跟踪哪些文件或目录,而 .gitignore 就是告知 Git 哪些文件或目录应当被忽略。它的核心作用就是让 Git 明确排除那些不需要被跟踪的文件(比如临时文件、日志、编译产物等),避免垃圾污染仓库。

Remove .git Repo

rm -rf .git 永久移除当前 repo 下的 .got