合并冲特与解决

这是 Git 学习中第一次要你“当裁判”——两个分支改了同一个地方,Git 无法自动决定用哪个,你必须手动裁决。我们会先故意制造一个冲突,再一步步解决它。继续在 ~/my-first-repo 操作。先确认一下当前历史形态(你的哈希值会不同):

cd ~/my-first-repo
git log --oneline --graph --all

确保你在 master 分支,并且工作区是干净的:

git switch master
git status

(应该显示 nothing to commit, working tree clean

冲突是如何发生的

冲突并非 Git 出错,而是两个分支修改了同一个文件的同一区域,Git 无法判断该保留哪个版本,于是把决定权交给你。

假设你和队友都发现 hello.txt 的第三行 “learning diff” 不够好,你们各自在分支里做了不同修改,现在要合并。

第一步:创建两个分支,各自修改同一行

  1. 创建并切换到 branch-A,修改第三行:

git switch -c branch-A

简单起见,我们重写整个文件,确保第三行不同:

echo -e "Hello, Git!\nGit is fun!\nA's change: diff is powerful" > hello.txt

-e\n 转义,但 Ubuntu 默认的 echo 可能不支持,可以改用 printf 或直接 cat <<EOF。这里写一个稳妥的方式用 heredoc):

cat > hello.txt << 'EOF'
Hello, Git!
Git is fun!
A's change: diff is powerful
Author: GitLearner
Sign: Best regards
EOF

提交:

git add hello.txt
git commit -m "branch-A: 修改第三行"
  1. 切回 master,创建 branch-B,同样修改第三行,但内容不同:

git switch master
git switch -c branch-B

修改文件:

cat > hello.txt << 'EOF'
Hello, Git!
Git is fun!
B's change: diff is easy
Author: GitLearner
Sign: Best regards
EOF

提交:

git add hello.txt
git commit -m "branch-B: 修改第三行"

现在两个分支在 “第三行” 产生了分歧。图形化看看:

git log --oneline --graph --all

会看到从 master 分出两个叉。

合并冲突

我们尝试把 branch-B 合并到 branch-A。冲突通常发生在你要合并时,所以先切换到 接收合并的分支(这里选 branch-A):

git switch branch-A
git merge branch-B

终端会立刻显示:

Auto-merging hello.txt
CONFLICT (content): Merge conflict in hello.txt
Automatic merge failed; fix conflicts and then commit the result.

这就是冲突通知。意思是:hello.txt 文件里,两个分支对同一部分有不同修改,Git 无能为力,你来解决。

查看冲突状态

运行 git status,你会看到:

On branch branch-A
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   hello.txt

“both modified” 告诉你哪些文件冲突了。此时 Git 处于 合并中间状态,hello.txt 里被写入了冲突标记。

手动解决冲突

打开 hello.txt 看看里面变成什么样了:

cat hello.txt

内容类似:

Hello, Git!
Git is fun!
<<<<<<< HEAD
A's change: diff is powerful
=======
B's change: diff is easy
>>>>>>> branch-B
Author: GitLearner
Sign: Best regards

解释:

  • <<<<<<< HEAD======= 之间:你当前所在分支(这里 branch-A)的内容。

  • =======>>>>>>> branch-B 之间:被合并进来的分支(branch-B)的内容。

这两个版本同时存在,你必须决定:保留 A 的、保留 B 的、或者组合成一个新版本。现在你当裁判:假设我们决定综合一下,改成 "Both: diff is powerful and easy"。用编辑器修改文件。我们直接用命令行方式重写文件:

cat > hello.txt << 'EOF'
Hello, Git!
Git is fun!
Both: diff is powerful and easy
Author: GitLearner
Sign: Best regards
EOF

注意:必须把 <<<<<<<=======>>>>>>> 这些标记全部删掉,并留下最终想要的内容。

标记为已解决、完成合并提交

冲突解决完后,你需要告诉 Git:“这个文件我处理好了”。将修改后的文件加入暂存区:

git add hello.txt

再查看状态:

git status

输出会变成:

On branch branch-A
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

意思是所有冲突都解决了,但合并过程还没完成,你还需要一个提交来结束合并。现在提交:

git commit -m "合并 branch-B:综合第三行修改"

这个提交会自动完成合并流程。完成后,git log --oneline --graph 会看到一个新的合并提交,将两个分支重新连在一起。

实用技巧

  1. 中止合并——git merge --abort

如果你在解决冲突的过程中慌了,比如发现搞错了分支,想回到冲突前干净的状态,用:

git merge --abort

这会让分支回到执行 git merge 之前的状态,所有冲突标记消失,工作区干净。非常安全。我们不妨现在就试试:先重置一下场景(重新制造冲突),再中止。(如果你已经解决了上一个冲突并提交了,那可以先撤销最后一次合并提交来回退,但不推荐初学者这样做。我们换个思路,用 branch-C 再做一次演示。)不过现在你在 branch-A 上已经完成了合并,直接创建一个新分支演示即可。创建 branch-Cmaster 出发(确保没有冲突历史):

git switch master
git switch -c branch-C
# 修改同一行造成冲突(简单复制之前操作)
cat > hello.txt << 'EOF'
Hello, Git!
Git is fun!
C's proposal: diff is magical
Author: GitLearner
Sign: Best regards
EOF
git add hello.txt
git commit -m "branch-C: 修改第三行"

再创建 branch-D

git switch master
git switch -c branch-D
cat > hello.txt << 'EOF'
Hello, Git!
Git is fun!
D's proposal: diff is straightforward
Author: GitLearner
Sign: Best regards
EOF
git add hello.txt
git commit -m "branch-D: 修改第三行"

切回 branch-C 并合并 branch-D,触发冲突:

git switch branch-C
git merge branch-D   # 冲突

现在查看状态,然后尝试中止:

git merge --abort

状态回到合并前,hello.txt 恢复为 C 的版本,冲突消失。非常干净。

  1. 使用可视化合并工具

在 Ubuntu 终端里,如果安装了 meldvimdiff,你可以用 git mergetool 来解决冲突,但对初学者,手动编辑已经足够,而且能真正理解冲突结构。这里不深入。

远程仓库与 GitHub 协作

这一章你会打破“本地孤岛”,把代码上传到 GitHub,和别人共享、协作。我们会在 WSL2 的 Ubuntu 里配置 SSH 密钥,然后把之前那个 my-first-repo 推送到 GitHub。

准备工作:注册 GitHub 账号

如果你还没有 GitHub 账号,先去 GitHub 注册一个(免费),记住你的用户名和注册邮箱。

注意:这个邮箱最好和你第一章 git config 里设置的邮箱一致,这样 GitHub 才能把你的提交和你的账号关联起来。

配置 SSH 密钥

Git 和 GitHub 之间可以通过 HTTPS 或 SSH 通信,SSH 更安全且不用每次输密码。我们来配 SSH。

检查是否已有密钥,在终端输入:

ls -al ~/.ssh

如果看到 id_ed25519id_ed25519.pubid_rsaid_rsa.pub,说明已经有密钥,可以跳过下面的生成部分;如果没有,就先生成。

生成新的 SSH 密钥(推荐 Ed25519)

ssh-keygen -t ed25519 -C "你的邮箱@example.com"
  • -t ed25519 指定加密类型,更快更安全。

  • -C 后面是注释,一般填你的邮箱。

运行后会提示你选择存储位置,直接回车使用默认路径 ~/.ssh/id_ed25519
接着提示输入 passphrase(密码短语),可以回车留空,也可以设一个额外的保护密码(你自己决定,留空更方便)。

启动 ssh-agent 并添加密钥

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

这一步让代理管理你的密钥,免去重复输入(如果设了 passphrase 只输一次)。

复制公钥

cat ~/.ssh/id_ed25519.pub

把屏幕输出的那一长串内容(以 ssh-ed25519 开头,以邮箱结尾)完整复制下来。

添加到 GitHub

  1. 浏览器登录 GitHub,点击右上角头像 → Settings。

  2. 左侧选择 SSH and GPG keys。

  3. 点击绿色的 New SSH key。

  4. Title 随便写,比如 “WSL2 Ubuntu”。

  5. Key 里粘贴刚才复制的公钥。

  6. 点击 Add SSH key。

验证配置是否成功:

ssh -T git@github.com

如果看到 “You’ve successfully authenticated, but GitHub does not provide shell access.” 就说明 SSH 成功了。

在 GitHub 上创建一个空仓库

  1. GitHub 首页右上角点击 + → New repository。

  2. Repository name:填 my-first-repo(和本地目录同名,便于理解)。

  3. Description 可选。

  4. 不要勾选 “Add a README file”、“Add .gitignore” 或 “Choose a license”。因为我们已有本地仓库,如果勾选会导致历史冲突,初学者暂时避开。

  5. 点击 Create repository。

创建后你会看到一个页面,中间有 “…or push an existing repository from the command line” 一栏,里面有几条命令,正是我们接下来要执行的。

关联远程仓库

回到终端,确保在 my-first-repo 目录且干净:

cd ~/my-first-repo
git status

我们给 GitHub 上的那个远程仓库起个简短的名字,约定俗成叫 origin(可以理解为主要远程仓库的默认称呼)。

在 GitHub 新建仓库的页面,找到 SSH 地址,形如:git@github.com:你的用户名/my-first-repo.git

添加远程:

git remote add origin git@github.com:你的用户名/my-first-repo.git

(把“你的用户名”换成你自己的 GitHub 用户名)

验证是否添加成功:

git remote -v

输出应类似:

origin  git@github.com:你的用户名/my-first-repo.git (fetch)
origin  git@github.com:你的用户名/my-first-repo.git (push)

说明你的本地仓库已经关联了一个远程仓库,名字叫 origin,既可以推送(push)也可以抓取(fetch)。

推送本地内容到 GitHub

把本地的 master 分支推送给远程的 master 分支:

git push -u origin master
  • -u--set-upstream 的缩写,会把本地 master 和远程 origin/master 绑定,以后在这个分支只需要 git push 即可。

  • 因为你是第一次推送,GitHub 那边是空的,所以推送成功会显示类似:

Enumerating objects: 9, done.
...
To github.com:你的用户名/my-first-repo.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

现在去 GitHub 刷新你的仓库页面,hello.txtreadme.md 和历史提交都出现了

从远程获取更新

想象你有一台电脑在办公室,一台在家,或者队友向远程仓库推送了新提交,你需要把这些更新拉取到本地。

模拟远程更新:我们先在 GitHub 上直接编辑文件。

  1. 在 GitHub 仓库页面,点击 readme.md 文件。

  2. 点击右上角的铅笔图标(Edit this file)。

  3. 在文件里添加一行:## 远程协作练习

  4. 页面下方,“Commit changes” 部分,填写提交信息,比如 “通过 GitHub 网页更新 README”。

  5. 选择 “Commit directly to the master branch”。

  6. 点击 Commit changes。

现在远程仓库的 master 比你的本地多了一个提交。

拉取到本地

回到终端,确保你在本地 master 分支:

git switch master
git pull

你会看到类似:

remote: Enumerating objects: ...
...
Updating a1b2c3d..f6g7h8i
Fast-forward
 readme.md | 2 ++
 1 file changed, 2 insertions(+)

git pull 其实是 git fetch(获取远程数据)和 git merge(合并到当前分支)的组合。现在本地 readme.md 文件里已经有你通过网页添加的那句话了。

克隆一个仓库

如果你想在新地方完全复制一个远程仓库(比如你的另一台电脑,或者当你的本地完全乱掉想重新开始),用 git clone

为了演示,我们回到用户主目录,克隆一份我们的远程仓库到一个叫 my-first-repo-clone 的目录:

cd ~
git clone git@github.com:你的用户名/my-first-repo.git my-first-repo-clone

执行后,Git 会把远程仓库的所有历史、分支都下载下来,并自动给它起名为 origin。进入这个克隆目录看看:

cd ~
cd my-first-repo-clone
git log --oneline

你会发现历史记录和原来一模一样。这就是“克隆”。