Hello OS!
Thinking 0.1 Git 的使用 1#
操作流程#
# init
mkdir ~/learnGit && cd ~/learnGit
git init .
# 1
touch README.txt
git status > Untracked.txt
# 2
echo "hello world!" > README.txt
git add .
git status > Staged.txt
# 3
git commit -m "23371263"
cat Untracked.txt
cat Stage.txt
# 4
echo "hello git!" > README.txt
git status > Modified.txt
cat Modified.txt
流程分析#
Git 将文件分为四种状态:untracked
,unmodified
,modified
,staged
。#1
的文件处于 untracked
的状态。#2
的文件在 add
之后变成了 staged
的状态,并在 #3
commit
之后变成了 unmodified
的状态。#4
的文件在修改之后,变成了 modified
的状态。
这是一种有限状态机的设计,用图示可以表示为如下:
VS Code 的自动暂存#
平时我们在使用 VS Code 的时候,在一个 Git 仓库中,VS Code 会自动把新建的文件(包括修改的内容)提交到暂存区,自动变成 staged
状态。我们输入提交消息并 commit
之后,文件就变成了 unmodified
的状态。也就是说,VS Code 将这四种状态化简为了两种状态,而自动帮我们处理了其他两种状态与这两种状态之间的转换:
我觉得这非常方便,从多种状态转换的路径中抽离出最常用的两条路径供用户手动处理,而自动处理其他部分的内容,是一种非常自然的设计。
IDEA 的文件状态转换#
反观 IntelliJ IDEA,没有像 VS Code 这样流畅的设计,它的文件状态的转换逻辑和 Git 的设计相同,没有进行简化。所以我们在 IDEA 里新建一个文件,默认是 untracked
状态的。需要在版本管理页面手动为文件打勾,表示将其暂存,才能提交这些文件。多一种操作当然多一些自由度,但是也增加了操作的复杂性。
这样的文件状态转换的方式,与 .gitignore
文件相结合时,也会遇到一些问题。.gitignore
忽略的是 untracked
状态的文件。如果你打开了 IDEA 的自动暂存功能,它就真的把新建的文件提交到暂存区,然后不管不顾了——也就是说,如果你在将某一文件(或者文件类型)加入.gitignore
之前就创建了这一文件(或者这一类型的文件),那么这个文件仍然会被 Git 追踪。VS Code 对待的方式则不同——如果你在创建之后把文件加入了 .gitignore
,那么即使它被暂存了,它依然会被 VS Code 认为是需要忽略的文件,于是它会被清出暂存区,并设置成被忽略的状态。只有被提交过的文件,VS Code 才不会去将它忽略。
总的来说,虽然 IDEA 的逻辑设计与 Git 如出一辙,但我并不认为这是一个好的设计——也许对于 Git 这样的命令行工具来说这种完备性是需要的,但作为一个现代的、智能的 IDE,VS Code(姑且认为它是个 IDE)这种更简洁的方式更加符合直觉。
Thinking 0.2 箭头与命令#
- “add the file”:
git add
- “stage the file”:
git add
- “commit”:
git commit
git add
命令对应的操作是两种:一种是将 untracked
的文件放入暂存区,一种是将 modified
的文件放入暂存区。其实本质上,都是将文件的变化放入暂存区中以记录。
Thinking 0.3 Git 的一些场景#
Git 如何恢复文件#
- 以往我的理解是从 Git 的三个区域入手的,但现在我发现也许从文件的四个状态去理解恢复机制会更好。
untracked
的文件被误删,因为它和 Git 还没产生关系,就没法通过 Git 恢复了。unmodified
的文件被误删,此时它的状态就变成了modified
。我们可以通过git restore <file>
来将其恢复到unmodified
的状态。modified
的文件被误删,状态还是modified
。我们可以把删除也理解为一种修改,而这次的修改(即删除)与上次的修改合并了。所以无论如何,我们也只能通过git restore <file>
把它恢复到unmodified
的状态。- 另一种理解是,我们上一次的修改尚未被 Git 记录,所以没法通过 Git 恢复上一次修改的部分。
staged
的文件被误删。此时文件的状态变为modified
。此时我们也可以通过git restore <file>
的方法将其恢复到staged
的状态。- 注意这里是恢复到
staged
状态而不是更早的unmodified
的状态。如果我们想要(将一个staged
状态下的文件)恢复到更早的unmodified
的状态,我们首先需要通过git restore --staged <file>
来将staged
状态的文件恢复到modified
的状态,再通过git restore <file>
来把它恢复到先前的unmodified
的状态。
- 注意这里是恢复到
- 如果
staged
的文件被误删,此时又git commit
提交了暂存区。已经staged
的“文件”变成了unmodified
的状态,但由于删除的操作并未暂存,所以该文件仍处于modified
的状态。我们还是可以通过git restore <file>
来把它恢复到unmodified
的状态。 - 可以看到,
git restore <file>
可以将一个处于modified
的状态的文件恢复到其上一个状态(unmodified
或者staged
),而git restore --staged <file>
可以将一个处于staged
的状态的文件恢复到其上一个状态(modified
或者untracked
)。所谓的误删操作,在 Git 看来就是将文件从其当前状态变为了modified
状态而已。
- 如果我们删除了一个文件,并已经通过
git commit
提交了(这还能算是误删吗?),我们也可以恢复其历史版本(如果删除之前我们提交过的话)。可以使用git log
查看提交的历史记录,确定某一提交有我们所需的文件后,记住其哈希值的前几位,使用git restore --source=<hashcode> <file>
来恢复该文件。恢复后,该文件会处于modified
的状态。
事实上在 Git 中我们很难彻底删除一个文件,即便是我们毁灭了某几条提交记录,兴许还有办法把这几条记录给找回。这种高级操作大概也叫 Git 魔法(
从 Git 的三个区域入手大概也能理清楚 git restore
的逻辑,但我想应该比上面这种方法要复杂。
Git 如何删除文件#
这个问题从 Git 的三个区域分别讲更好。Git 复杂的原因之一就是有很多个角度去看待它的操作,而且不同操作的最佳视角还不一样。
- 从工作区删除一个文件。直接使用系统的
rm
就好。 - 从暂存区删除一个文件。我们可以使用
git rm <file>
命令。这个命令就是 Git 版rm
,它会同时将工作区和暂存区的这个文件全部删除——前提是这两份版本相同。也就是说,如果你暂存了这个文件之后又修改了它,使用这个命令时 Git 就会发出警告并终止操作。要想强制执行,加-f
即可。- 如果想保留工作区的文件,而删除暂存区的文件,可以使用
git rm --cached <file>
。也就是说,git add <file>
的反义词其实是git rm --cached <file>
——非常糟糕,对吧?- 如果真要这么说,
git rm --cached <file>
和git restore --staged <file>
是同义词。更糟糕了。
- 如果真要这么说,
- 如果想保留工作区的文件,而删除暂存区的文件,可以使用
- 从版本库删除一个文件。这件事情就难办了, 但是使用 Git 的新手又经常遇到这样的问题——经常不检查即将
commit
的代码,而把自己的个人信息或者隐私给提交了。我之前就遇到过这样的问题。下面是我的解决方法,当然可能清除得不彻底。- 使用 pip 安装 git-filter-repo
- 确定泄露信息的格式,比如学号"2337xxxx",则可以使用
git filter-repo --replace-text <(echo "2337xxxx==>")
将所有历史提交中的"2337xxxx"替换为空串。如果要删除某个文件,可以使用git filter-repo --path file --invert-paths
上述的方法也许不够灵活,下面再介绍一种方法: - 如果能够确定是最近几次提交中泄露的信息,可以使用
git rebase -i HEAD~<x>
来修改最近 x 次提交。 - 终端会打开文本编辑器,显示这几次提交的 hash 信息和 commit 消息,将需要修改的提交前的
pick
改为edit
,然后保存退出。 - 此时工作区会恢复到第一条转为
edit
的提交被提交之前的状态,可以在此时清除信息。 - 使用
git commit --amend
修改提交消息。 - 使用
git rebase --continue
继续修改下一条提交。 -这些操作之后,由于你修改了历史提交记录,推送到远程仓库时必须使用git push --force
来强制覆盖远程仓库的记录。注意 OS 和 OO 的 gitlab 是不支持强制推送的,只有在你自己的 Github 仓库中你才有权利这么做,而如果有别的协作者,他们需要使用git fetch
和git reset --hard origin/main
来避免与远程仓库的冲突。
Thinking 0.4 Git 的使用 2#
操作流程#
cd ~/learnGit
#1
touch README.txt
echo "Testing 1" > README.txt
git add .
git commit -m "1"
# repeat with commit message "2" and "3"
git log
#2
git reset --hard HEAD^
git log
#3
git reset --hard <HASHCODE_1> # hashcode of commit with message "1"
git log
#4
git reset --hard <HASHCODE_3> # hashcode of commit with message "3"
分析#
使用 git reset --hard
命令可以进行版本回退或者切换到任何一个版本。
- 使用
HEAD^
切换到上一个版本 - 使用
HEAD~<x>
切换到前第 x 个版本 - 使用
hash
值切换到任意版本
这里可以看到,我们在 #2
中回退到上一个版本,此时执行 git log
会找不到提交消息为"3"的提交记录。如果不是事先记录下它的 hash 值,我们可能无法找到这条记录了。
事实上并非找不到。可以使用 git reflog
来查看 HEAD 指针移动的历史记录。刚才我们 commit 了三次,随后 reset 一次,这四次指针移动的情况被 git reflog
完整的记录下来,我们就可以通过前三次记录找到提交消息为"3"的提交的哈希值。
Thinking 0.5 echo 的使用#
操作流程#
echo first
# first
echo second > output.txt
# output.txt: second
echo third > output.txt
# output.txt: third
echo forth >> output.txt
# output.txt:
# third
# forth
分析#
>
用于将命令的输出覆盖到文件中,>>
用于将命令的输出追加到文件中。
Thinking 0.6 文件的操作#
操作流程#
# command
#!/bin/bash
touch test
echo '#!/bin/bash' >> test
echo 'echo Shell Start...' >> test
echo 'echo set a = 1' >> test
echo 'a=1' >> test
echo 'echo set b = 2' >> test
echo 'b=2' >> test
echo 'echo set c = a+b' >> test
echo 'c=$[$a+$b]' >> test
echo 'echo c = $c' >> test
echo 'echo save c to ./file1' >> test
echo 'echo $c>file1' >> test
echo 'echo save b to ./file2' >> test
echo 'echo $b>file2' >> test
echo 'echo save a to ./file3' >> test
echo 'echo $c>file3' >> test
echo 'echo save file1 file2 file3 to file4' >> test
echo 'cat file1>file4' >> test
echo 'cat file2>>file4' >> test
echo 'cat file3>>file4' >> test
echo 'echo save file4 to ./result' >> test
echo 'cat file4>>result' >> test
bash command > test
bash test > result
分析#
- 单引号用来引用完全的“字面值”的字符串。所有在单引号中的内容都会被原样保留,不会进行替换或者转义。
- 双引号用来引用字符串,但允许变量和命令的替换或转义。
- 反引号用于命令替换,即先执行反引号包含的命令,再将该命令的输出替换到当前位置。等价于
$()
。
上机#
题量很大,勉强做完。知道思路,不懂实现。bash不会,完完蛋蛋。