1. .gitignore
常见项目添加
1.1 .gitignore
模板
.gitignore
针对每个语言都有对应的模板,在GitHub创建项目时就可以选择(你可以在GitHub提供的.gitignore模板大全中找到它)。如Python语言的.gitignore
模板如下:
# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/
1.2 添加更多的.gitignore
项目
但是这些往往是不够的的。如我们在Mac系统下用VSCode开发,那么常常还需要添加以下项目:
# IDE - VSCode .vscode/ # OS generated files .DS_Store
其中.vscode/
表示忽略.vscode
这个包含项目配置文件的隐藏目录(注意是包括目录一起忽略,这个和Linux下诸如cp test/ .
这类命令的语义有区别,参加我的博客《Linux:文件解压、复制和移动的若干坑》),.DS_Store
表示忽略掉Mac操作系统下存储目录自定义属性的隐藏文件。
此外,我们再以机器学习相关的项目为例子,数据(放在data
目录下)和模型(放在model
目录下)通常异常巨大,我们并不想将它们放到项目文件夹下,因此我们可能倾向于添加如下的项目:
# data files data/* # model files model/*
data/*
和model/*
语义上表示忽视data
目录下所有文件与model
目录下所有文件及子目录(不包括data
和model
目录本身)。但是我们会发现,实际上空的data
和model
目录并没有成功git add
到项目中:
(base) orion-orion@MacBook-Pro Learn-Git % git add data (base) orion-orion@MacBook-Pro Learn-Git % git add model (base) orion-orion@MacBook-Pro Learn-Git % git status On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits) nothing to commit, working tree clean
这是因为空目录不会称为Git版本控制系统跟踪(track)。但是如果我们想保存data
和model
的目录架构呢?很简单,我们只需要在data
和model
目录下添加.gitkeep
目录即可,然后将在.gitignore
文件中对.gitkeep
进行反选(即不忽视):
# data files data/* !data/.gitkeep # model files model/* !model/.gitkeep
可以看到由于隐藏文件的存在,现在空目录能够正常git add
了:
(base) orion-orion@MacBook-Pro Learn-Git % git add data (base) orion-orion@MacBook-Pro Learn-Git % git add model (base) orion-orion@MacBook-Pro Learn-Git % git status On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits) Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: data/.gitkeep new file: model/.gitkeep
但是需要注意,如果这样写就没用:
# data files data/ !data/.gitkeep
因为data/
表示将data目录本身也忽略了,Git根本就不会去查看该目录,以致.gitkeep
文件也就不起作用了。
额外提一下,如果我们仅仅希望忽略掉data
目录下的.csv
文件,可以这样写:
# data files data/*.csv
2. 移除已暂存(staged)的文件
2.1 关于跟踪与暂存
在Git中,一个文件可能在这三种区域中:工作目录(Working Directory),暂存区(Staging Area,也称索引index),Git仓库(可视为一棵提交树committed tree)。三者关系如下图所示:
当我们将文件添加到项目目录中时,我们其实是在将其添加到工作目录中。
一旦一个目录或文件被git add
了一次,那么它就会被跟踪(track)并加入暂存区。此后再对其进行修改,Git会提醒你Changes not staged for commit
与modified: README.md
,需要再次运行git add
将其暂存(staged):
(base) orion-orion@MacBook-Pro Learn-Git % echo "new version" > README.md (base) orion-orion@MacBook-Pro Learn-Git % git status On branch main Your branch is ahead of 'origin/main' by 2 commits. (use "git push" to publish your local commits) 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: README.md no changes added to commit (use "git add" and/or "git commit -a")
而文件的所谓的未跟踪(untracked)、未修改(unmodified)、已修改(modified)、已暂存(staged)四种状态的关系如下所示:
2.2 清除已暂存的文件
现在假设我们搞忘了编写.gitignore
,然后已经用了git add -A
或git add .
命令目录下所有文件及子目录都暂存了(在Git 2.0中git add -A
或git add .
命令等效)。而其中有很大的日志文件或一些诸如*.a
的编译文件,我们如何将这些文件从暂存区域移除以取消跟踪呢?可以用git rm --cached
命令完成此项工作,如:
git rm --cached README.md
注意要带上选项--cached
,而不仅仅是git rm
,git rm
除了从暂存区域移除外,还会将磁盘上的文件也一起删了。关于参数选项可以参见我的博客《Linux:可执行程序的Shell传参格式规范 》。
使用该命令效果如下:
(base) orion-orion@MacBook-Pro Learn-Git % git rm --cached README.md rm 'README.md' (base) orion-orion@MacBook-Pro Learn-Git % git status On branch main Your branch is ahead of 'origin/main' by 2 commits. (use "git push" to publish your local commits) Changes to be committed: (use "git restore --staged <file>..." to unstage) deleted: README.md
注意到Changes to be committed:
与deleted: README.md
,这说明当我们使用git rm --cached
并commit后, 相关的文件还会被从committed tree中移除。如果我们只想移除出暂存区,可以使用下列命令:
git reset HEAD README.md
该命令等同 git reset --mixed HEAD README.md
(默认参数为--mixed
,还有个参数为--hard
,我们放在3.3
节讲)。使用后效果如下:
(base) orion-orion@MacBook-Pro Learn-Git % git reset HEAD *.md Unstaged changes after reset: M README.md (base) orion-orion@MacBook-Pro Learn-Git % git status On branch main Your branch is ahead of 'origin/main' by 2 commits. (use "git push" to publish your local commits) 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: README.md no changes added to commit (use "git add" and/or "git commit -a")
注意到Changes not staged for commit:
与 modified: README.md
。说明该命令只是将README.md
移除暂存区,但是上次对README.md
的commit还在(即撤销最近的一次commit之后的变化)。
如果要递归地将当前目录下的所有文件及子目录移除出暂存区(与commit tree),可以这样写:
git rm -r --cached .
注意这个命令非常危险和暴力,一般还是建议指定具体的目录或文件名。
3. 追加与撤销git commit
操作
3.1 commit历史查看
用git log
命令可以看到项目的git commit
历史:
(base) orion-orion@MacBook-Pro Learn-Git % git log commit 37a35d36eaf8b56c9e7b719c3c7576f3251cee36 (HEAD -> main) Author: orion-orion <orion-orion@foxmail.com> Date: Mon May 23 14:15:21 2022 +0800 modify .gitignore commit ab7bf6e2c400c8d775cc3bc56928c7748c63c8f8 Author: orion-orion <orion-orion@foxmail.com> Date: Mon May 23 10:08:08 2022 +0800 add .gitignore commit 146c68e12fd2aebed8b38dd5cf95621f800fe4aa (origin/main, origin/HEAD) Author: 猎户座 <46917784+orion-orion@users.noreply.github.com> Date: Sun May 22 09:48:22 2022 +0800 Initial commit
默认不用任何参数的话,git log
会按提交时间列出所有的更新,最近的更新排在最上面。 正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址(如果电子邮件名为<46917784+orion-orion@users.noreply.github.com>
,说明你在GitHub中将邮件名设置为私有的了,需要去修改一下)、提交时间以及提交说明。
3.2 追加commit操作
现在我们又对.gitignore
进行了修改。但是我们不想又commit一次,而想将其合并在最后一次的modify .gitignore
里,使commit记录更为精简。我们可以用以下命令:
(base) orion-orion@MacBook-Pro Learn-Git % git add .gitignore (base) orion-orion@MacBook-Pro Learn-Git % git commit --amend
并在commit信息的编辑界面写入modify .gitignore
:
modify .gitignore # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Mon May 23 14:15:21 2022 +0800 # # On branch main # Your branch is ahead of 'origin/main' by 2 commits. # (use "git push" to publish your local commits) # # Changes to be committed: # modified: .gitignore # new file: data/.gitkeep # new file: model/.gitkeep # # Changes not staged for commit: # modified: README.md # :wq!
可以看到总的commit记录没变,所显示的最后一次commit记录的时间也没变,但新的修改已经追加进去了(SHA-1 校验和发生了变化):
(base) orion-orion@MacBook-Pro Learn-Git % git log commit a0dfeff409494165bdff60c27b24fad2bc0ed0ad (HEAD -> main) Author: orion-orion <orion-orion@foxmail.com> Date: Mon May 23 14:15:21 2022 +0800 modify .gitignore commit ab7bf6e2c400c8d775cc3bc56928c7748c63c8f8 Author: orion-orion <orion-orion@foxmail.com> Date: Mon May 23 10:08:08 2022 +0800 add .gitignore commit 146c68e12fd2aebed8b38dd5cf95621f800fe4aa (origin/main, origin/HEAD) Author: 猎户座 <46917784+orion-orion@users.noreply.github.com> Date: Sun May 22 09:48:22 2022 +0800 Initial commit
3.3 撤销git commit
操作
现在我们想撤销git commit
的操作。我们回到git reset命令
。不过现在我们需要使用git reset --hard
方法:
(base) orion-orion@MacBook-Pro Learn-Git % git reset --hard HEAD^1 HEAD is now at ab7bf6e add .gitignore (base) orion-orion@MacBook-Pro Learn-Git % git log commit ab7bf6e2c400c8d775cc3bc56928c7748c63c8f8 (HEAD -> main) Author: orion-orion <orion-orion@foxmail.com> Date: Mon May 23 10:08:08 2022 +0800 add .gitignore commit 146c68e12fd2aebed8b38dd5cf95621f800fe4aa (origin/main, origin/HEAD) Author: 猎户座 <46917784+orion-orion@users.noreply.github.com> Date: Sun May 22 09:48:22 2022 +0800 Initial commit
命令中的HEAD^1
意思为将commit记录回退到上上次提交后的状态,HEAD^2
以此类推。
不过大家必须注意,--hard
标记是reset
命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的reset
调用都可以轻松撤消,但是--hard
选项不能,因为它强制覆盖了工作目录中的文件。
参考
- [1] 《Pro Git 中文版》在线阅读
- [2] Stack Overflow: How can I Remove .DS_Store files from a Git repository?
- [3] Stack Overflow: How can I add a blank directory to a Git repository?
- [4] Local Coder: Difference between .gitignore rules with and without trailing slash like /dir and /dir/
- [5] Stack Overflow: Difference between "git add -A" and "git add ."
- [6] 知乎:为什么要先 git add 才能 git commit ?
- [7] 知乎:Git commits历史是如何做到如此清爽的?
- [8] Stack Overflow: "git rm --cached x" vs "git reset head -- x"?