找回密码
 立即注册
首页 业界区 安全 Git:.gitignore、移除暂存与撤销修改

Git:.gitignore、移除暂存与撤销修改

周冰心 3 天前
1. .gitignore常见项目添加

1.1 .gitignore模板

.gitignore针对每个语言都有对应的模板,在GitHub创建项目时就可以选择(你可以在GitHub提供的.gitignore模板大全中找到它)。如Python语言的.gitignore模板如下:
  1. # Byte-compiled / optimized / DLL files
  2. __pycache__/
  3. *.py[cod]
  4. *$py.class
  5. # C extensions
  6. *.so
  7. # Distribution / packaging
  8. .Python
  9. build/
  10. develop-eggs/
  11. dist/
  12. downloads/
  13. eggs/
  14. .eggs/
  15. lib/
  16. lib64/
  17. parts/
  18. sdist/
  19. var/
  20. wheels/
  21. pip-wheel-metadata/
  22. share/python-wheels/
  23. *.egg-info/
  24. .installed.cfg
  25. *.egg
  26. MANIFEST
  27. # PyInstaller
  28. #  Usually these files are written by a python script from a template
  29. #  before PyInstaller builds the exe, so as to inject date/other infos into it.
  30. *.manifest
  31. *.spec
  32. # Installer logs
  33. pip-log.txt
  34. pip-delete-this-directory.txt
  35. # Unit test / coverage reports
  36. htmlcov/
  37. .tox/
  38. .nox/
  39. .coverage
  40. .coverage.*
  41. .cache
  42. nosetests.xml
  43. coverage.xml
  44. *.cover
  45. *.py,cover
  46. .hypothesis/
  47. .pytest_cache/
  48. # Translations
  49. *.mo
  50. *.pot
  51. # Django stuff:
  52. *.log
  53. local_settings.py
  54. db.sqlite3
  55. db.sqlite3-journal
  56. # Flask stuff:
  57. instance/
  58. .webassets-cache
  59. # Scrapy stuff:
  60. .scrapy
  61. # Sphinx documentation
  62. docs/_build/
  63. # PyBuilder
  64. target/
  65. # Jupyter Notebook
  66. .ipynb_checkpoints
  67. # IPython
  68. profile_default/
  69. ipython_config.py
  70. # pyenv
  71. .python-version
  72. # pipenv
  73. #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
  74. #   However, in case of collaboration, if having platform-specific dependencies or dependencies
  75. #   having no cross-platform support, pipenv may install dependencies that don't work, or not
  76. #   install all needed dependencies.
  77. #Pipfile.lock
  78. # PEP 582; used by e.g. github.com/David-OConnor/pyflow
  79. __pypackages__/
  80. # Celery stuff
  81. celerybeat-schedule
  82. celerybeat.pid
  83. # SageMath parsed files
  84. *.sage.py
  85. # Environments
  86. .env
  87. .venv
  88. env/
  89. venv/
  90. ENV/
  91. env.bak/
  92. venv.bak/
  93. # Spyder project settings
  94. .spyderproject
  95. .spyproject
  96. # Rope project settings
  97. .ropeproject
  98. # mkdocs documentation
  99. /site
  100. # mypy
  101. .mypy_cache/
  102. .dmypy.json
  103. dmypy.json
  104. # Pyre type checker
  105. .pyre/
复制代码
1.2 添加更多的.gitignore项目

但是这些往往是不够的的。如我们在Mac系统下用VSCode开发,那么常常还需要添加以下项目:
  1. # IDE - VSCode
  2. .vscode/
  3. # OS generated files
  4. .DS_Store
复制代码
其中.vscode/表示忽略.vscode这个包含项目配置文件的隐藏目录(注意是包括目录一起忽略,这个和Linux下诸如cp test/ .这类命令的语义有区别,参加我的博客《Linux:文件解压、复制和移动的若干坑》),.DS_Store表示忽略掉Mac操作系统下存储目录自定义属性的隐藏文件。
此外,我们再以机器学习相关的项目为例子,数据(放在data目录下)和模型(放在model目录下)通常异常巨大,我们并不想将它们放到项目文件夹下,因此我们可能倾向于添加如下的项目:
  1. # data files
  2. data/*
  3. # model files
  4. model/*
复制代码
data/*和model/*语义上表示忽视data目录下所有文件与model目录下所有文件及子目录(不包括data和model目录本身)。但是我们会发现,实际上空的data和model目录并没有成功git add到项目中
  1. (base) orion-orion@MacBook-Pro Learn-Git % git add data                  
  2. (base) orion-orion@MacBook-Pro Learn-Git % git add model                 
  3. (base) orion-orion@MacBook-Pro Learn-Git % git status                    
  4. On branch main
  5. Your branch is ahead of 'origin/main' by 1 commit.
  6.   (use "git push" to publish your local commits)
  7. nothing to commit, working tree clean
复制代码
这是因为空目录不会称为Git版本控制系统跟踪(track)。但是如果我们想保存data和model的目录架构呢?很简单,我们只需要在data和model目录下添加.gitkeep目录即可,然后将在.gitignore文件中对.gitkeep进行反选(即不忽视):
  1. # data files
  2. data/*
  3. !data/.gitkeep
  4. # model files
  5. model/*
  6. !model/.gitkeep
复制代码
可以看到由于隐藏文件的存在,现在空目录能够正常git add了:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git add data
  2. (base) orion-orion@MacBook-Pro Learn-Git % git add model
  3. (base) orion-orion@MacBook-Pro Learn-Git % git status   
  4. On branch main
  5. Your branch is ahead of 'origin/main' by 1 commit.
  6.   (use "git push" to publish your local commits)
  7. Changes to be committed:
  8.   (use "git restore --staged <file>..." to unstage)
  9.         new file:   data/.gitkeep
  10.         new file:   model/.gitkeep
复制代码
但是需要注意,如果这样写就没用:
  1. # data files
  2. data/
  3. !data/.gitkeep
复制代码
因为data/表示将data目录本身也忽略了,Git根本就不会去查看该目录,以致.gitkeep文件也就不起作用了。
额外提一下,如果我们仅仅希望忽略掉data目录下的.csv文件,可以这样写:
  1. # data files
  2. data/*.csv
复制代码
2. 移除已暂存(staged)的文件

2.1 关于跟踪与暂存

在Git中,一个文件可能在这三种区域中:工作目录(Working Directory),暂存区(Staging Area,也称索引index),Git仓库(可视为一棵提交树committed tree)。三者关系如下图所示:
1.png

当我们将文件添加到项目目录中时,我们其实是在将其添加到工作目录中。
一旦一个目录或文件被git add了一次,那么它就会被跟踪(track)并加入暂存区。此后再对其进行修改,Git会提醒你Changes not staged for commit与modified:   README.md,需要再次运行git add将其暂存(staged):
  1. (base) orion-orion@MacBook-Pro Learn-Git % echo "new version" > README.md
  2. (base) orion-orion@MacBook-Pro Learn-Git % git status
  3. On branch main
  4. Your branch is ahead of 'origin/main' by 2 commits.
  5.   (use "git push" to publish your local commits)
  6. Changes not staged for commit:
  7.   (use "git add <file>..." to update what will be committed)
  8.   (use "git restore <file>..." to discard changes in working directory)
  9.         modified:   README.md
  10. no changes added to commit (use "git add" and/or "git commit -a")
复制代码
而文件的所谓的未跟踪(untracked)、未修改(unmodified)、已修改(modified)、已暂存(staged)四种状态的关系如下所示:
2.png
2.2 清除已暂存的文件

现在假设我们搞忘了编写.gitignore,然后已经用了git add -A或git add .命令目录下所有文件及子目录都暂存了(在Git 2.0中git add -A或git add .命令等效)。而其中有很大的日志文件或一些诸如*.a的编译文件,我们如何将这些文件从暂存区域移除以取消跟踪呢?可以用git rm --cached命令完成此项工作,如:
  1. git rm --cached README.md
复制代码
注意要带上选项--cached,而不仅仅是git rm,git rm除了从暂存区域移除外,还会将磁盘上的文件也一起删了。关于参数选项可以参见我的博客《Linux:可执行程序的Shell传参格式规范 》。
使用该命令效果如下:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git rm --cached README.md
  2. rm 'README.md'
  3. (base) orion-orion@MacBook-Pro Learn-Git % git status               
  4. On branch main
  5. Your branch is ahead of 'origin/main' by 2 commits.
  6.   (use "git push" to publish your local commits)
  7. Changes to be committed:
  8.   (use "git restore --staged <file>..." to unstage)
  9.         deleted:    README.md
复制代码
注意到Changes to be committed:与deleted:    README.md,这说明当我们使用git rm --cached并commit后, 相关的文件还会被从committed tree中移除。如果我们只想移除出暂存区,可以使用下列命令:
  1. git reset HEAD README.md
复制代码
该命令等同 git reset --mixed HEAD README.md(默认参数为--mixed,它还可选的参数包括soft和--hard,我们放在3.3节讲)。使用后效果如下:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git reset HEAD *.md     
  2. Unstaged changes after reset:
  3. M       README.md
  4. (base) orion-orion@MacBook-Pro Learn-Git % git status              
  5. On branch main
  6. Your branch is ahead of 'origin/main' by 2 commits.
  7.   (use "git push" to publish your local commits)
  8. Changes not staged for commit:
  9.   (use "git add <file>..." to update what will be committed)
  10.   (use "git restore <file>..." to discard changes in working directory)
  11.         modified:   README.md
  12. 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),可以这样写:
  1. git rm -r --cached . 
复制代码
注意这个命令非常危险和暴力,一般还是建议指定具体的目录或文件名。
3. 追加与撤销git commit操作

3.1 commit历史查看

用git log命令可以看到项目的git commit历史:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git log
  2. commit 37a35d36eaf8b56c9e7b719c3c7576f3251cee36 (HEAD -> main)
  3. Author: orion-orion <orion-orion@foxmail.com>
  4. Date:   Mon May 23 14:15:21 2022 +0800
  5.     modify .gitignore
  6. commit ab7bf6e2c400c8d775cc3bc56928c7748c63c8f8
  7. Author: orion-orion <orion-orion@foxmail.com>
  8. Date:   Mon May 23 10:08:08 2022 +0800
  9.     add .gitignore
  10. commit 146c68e12fd2aebed8b38dd5cf95621f800fe4aa (origin/main, origin/HEAD)
  11. Author: 猎户座 <46917784+orion-orion@users.noreply.github.com>
  12. Date:   Sun May 22 09:48:22 2022 +0800
  13.     Initial commit
复制代码
默认不用任何参数的话,git log会按提交时间列出所有的更新,最近的更新排在最上面。 正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址(如果电子邮件名为,说明你在GitHub中将邮件名设置为私有的了,需要去修改一下)、提交时间以及提交说明。
3.2 追加commit操作/修改commit信息

现在我们又对.gitignore进行了修改。但是我们不想又commit一次,而想将其合并在最后一次的modify .gitignore里,使commit记录更为精简。我们可以用以下命令:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git add .gitignore
  2. (base) orion-orion@MacBook-Pro Learn-Git % git commit --amend
复制代码
并在commit信息的编辑界面写入modify .gitignore(如果要修改commit信息,此处改成其它的即可):
  1. modify .gitignore
  2. # Please enter the commit message for your changes. Lines starting
  3. # with '#' will be ignored, and an empty message aborts the commit.
  4. #
  5. # Date:      Mon May 23 14:15:21 2022 +0800
  6. #
  7. # On branch main
  8. # Your branch is ahead of 'origin/main' by 2 commits.
  9. #   (use "git push" to publish your local commits)
  10. #
  11. # Changes to be committed:
  12. #       modified:   .gitignore
  13. #       new file:   data/.gitkeep
  14. #       new file:   model/.gitkeep
  15. #
  16. # Changes not staged for commit:
  17. #       modified:   README.md
  18. #
  19.                           
  20. :wq!
复制代码
可以看到总的commit记录没变,所显示的最后一次commit记录的时间也没变,但新的修改已经追加进去了(SHA-1 校验和发生了变化):
  1. (base) orion-orion@MacBook-Pro Learn-Git % git log           
  2. commit a0dfeff409494165bdff60c27b24fad2bc0ed0ad (HEAD -> main)
  3. Author: orion-orion <orion-orion@foxmail.com>
  4. Date:   Mon May 23 14:15:21 2022 +0800
  5.     modify .gitignore
  6. commit ab7bf6e2c400c8d775cc3bc56928c7748c63c8f8
  7. Author: orion-orion <orion-orion@foxmail.com>
  8. Date:   Mon May 23 10:08:08 2022 +0800
  9.     add .gitignore
  10. commit 146c68e12fd2aebed8b38dd5cf95621f800fe4aa (origin/main, origin/HEAD)
  11. Author: 猎户座 <46917784+orion-orion@users.noreply.github.com>
  12. Date:   Sun May 22 09:48:22 2022 +0800
  13.     Initial commit
复制代码
我们在暂存区没有任何更新的情况下,也可以使用git commit --amend命令来单纯修改commit操作的附加信息。
但是上面的操作只能修改最近一次的commit信息,如果我们要修改几步之前的commit信息呢?这时就要用到git rebase操作了。
比如如果我们用git log查看到以下修改记录:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git log   
  2. commit 34381d5208c81b2a9846402225cd9a559163c2ef (HEAD -> master
  3. )
  4. Author: orion-orion <orion-orion@foxmail.com>
  5. Date:   Mon Aug 1 14:39:43 2022 +0800
  6.     add file4
  7. commit 12eb0b1bdbdc41a8d7b1b63ed777135de2396e25
  8. Author: orion-orion <orion-orion@foxmail.com>
  9. Date:   Mon Aug 1 14:39:27 2022 +0800
  10.     add file3
  11. commit 123ac795a8a283c89737eeb83d2a62210c55cc0c
  12. Author: orion-orion <orion-orion@foxmail.com>
  13. Date:   Mon Aug 1 14:39:11 2022 +0800
  14.     add file*
  15. commit b04ef5bbf5bf70296790c7e9fbe71b27afd04a3f
  16. Author: orion-orion <orion-orion@foxmail.com>
  17. Date:   Mon Aug 1 14:38:46 2022 +0800
  18.     add file1
复制代码
现在我们觉得导数第3个commit记录不太和谐,想把它修改为add file2。那么我们需要使用git rebase -i HEAD~3。注意:这里HEAD~0为当前工作的分支,HEAD/HEAD~1为上次提交发生之前的工作分支,HEAD~3为上上上次提交发生之前的工作分支,此处git rebase -i HEAD~3即为将HEAD指针指向上上上次提交发生之前的工作分支。
PS: 在Git中分支就是指向提交对象的可变指针
然后显示
  1. pick 123ac79 add file*
  2. pick 12eb0b1 add file3
  3. pick 34381d5 add file4
复制代码
需要在编辑模式下将第一行的pick修改为edit:
  1. edit 123ac79 add file*
  2. pick 12eb0b1 add file3
  3. pick 34381d5 add file4
复制代码
然后在底行模式输入:wq保存退出。最后再和上面一样输入git commit --amend(如果还需要追加文件,则在这之前还需要使用git add命令将文件添加到暂存区),显示:
  1. add file*
  2. # Please enter the commit message for your changes. Lines starting
  3. # with '#' will be ignored, and an empty message aborts the commit.
  4. ...
复制代码
需要再编辑模式下将add file*修改为add file2:
  1. add file2
  2. # Please enter the commit message for your changes. Lines starting
  3. # with '#' will be ignored, and an empty message aborts the commit.
  4. ...
复制代码
此时输入git log,可以看到已经修改完毕,不过HEAD指针是指向add file2对应的commit对象的:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git log   
  2. commit 19f39c0883a0b5b9e7ebcef4e9efd82c72e8bb44 (HEAD)
  3. Author: orion-orion <orion-orion@foxmail.com>
  4. Date:   Mon Aug 1 14:39:11 2022 +0800
  5.     add file2
  6. commit b04ef5bbf5bf70296790c7e9fbe71b27afd04a3f
  7. Author: orion-orion <orion-orion@foxmail.com>
  8. Date:   Mon Aug 1 14:38:46 2022 +0800
  9.     add file1
复制代码
为了回到我们原来的master分支,我们需要运行git rebase --continue命令。运行完毕后。再使用git log查看commit记录,可以看到HEAD指针已经指向了原来的add file4对应的commit对象,也就是master分支指针指向的对象,且commit记录已经正确修改:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git log   
  2. commit f4fd696d9b2ff0649adf89e93f7fd767d7c37922 (HEAD -> master)
  3. Author: orion-orion <orion-orion@foxmail.com>
  4. Date:   Mon Aug 1 14:39:43 2022 +0800
  5.     add file4
  6. commit 27de88d3e66b36f67b3d0eec45759e365951819c
  7. Author: orion-orion <orion-orion@foxmail.com>
  8. Date:   Mon Aug 1 14:39:27 2022 +0800
  9.     add file3
  10. commit 19f39c0883a0b5b9e7ebcef4e9efd82c72e8bb44
  11. Author: orion-orion <orion-orion@foxmail.com>
  12. Date:   Mon Aug 1 14:39:11 2022 +0800
  13.     add file2
  14. commit b04ef5bbf5bf70296790c7e9fbe71b27afd04a3f
  15. Author: orion-orion <orion-orion@foxmail.com>
  16. Date:   Mon Aug 1 14:38:46 2022 +0800
  17.     add file1
复制代码
此处的HEAD分支指向了最新的commit对象,也就意味着和master分支重合了,它们的关系如下图所示:
3.png
PS:注意,Git的默认分支名字就是master, 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动,所以默认的master分支会一直指向最新的提交对象。
但同样需要注意的是,Git 的master分支并不是一个特殊分支。 它就跟其它分支完全没有区别。 之所以几乎每一个仓库都有master分支,是因为git init命令默认创建它,并且大多数人都懒得去改动它。
最后,我们再说一下HEAD~{n}(Tilde)和HEAD^{n}(Caret)的区别。在大多数情况下,~ 表示在提交树上直接进行线性回溯(默认每个节点都选择其第一个parent),而^则用于在提交树上选择当前节点的不同parent。注意,在提交树没有分叉时,二者等价
我们以下面这个提交树为例子来讲解。这里有上到下对应由远及近的时间顺序,其中D、F、B、A都为merge commit。节点B和C是A节点的parents,parents按照从左到右的提交顺序。
  1. G   H   I   J
  2. \ /     \ /
  3.   D   E   F
  4.    \  |  / \
  5.     \ | /   |
  6.      \|/    |
  7.       B     C
  8.        \   /
  9.         \ /
  10.          A
复制代码
则我们有下面的等价关系:
  1. A = A~0  = A^0
  2. B = A^   = A^1     = A~1
  3. C = A^2
  4. D = A^^  = A^1^1   = A~2
  5. E = B^2  = A^^2
  6. F = B^3  = A^^3
  7. G = A^^^ = A^1^1^1 = A~3
  8. H = D^2  = B^^2    = A^^^2  = A~2^2
  9. I = F^   = B^3^    = A^^3^
  10. J = F^2  = B^3^2   = A^^3^2
复制代码
3.3 撤销git commit操作

现在我们想撤销git commit的操作。我们回到在2.2节中提到的git reset命令。不过现在我们需要使用git reset --soft方法,比如如果我们用git log`查看到以下修改记录:
  1. (base) orion-orion@MacBook-Pro Learn-Git % git log      
  2. commit 3f1ea3da4bd477b511c5a6eb0ddc8620b0d3b34f (HEAD -> master)
  3. Author: orion-orion <orion-orion@foxmail.com>
  4. Date:   Mon Aug 1 15:46:23 2022 +0800
  5.     add file4
  6. commit 0755827bbb67c463cb7ac139f1f86c93006a26e0
  7. Author: orion-orion <orion-orion@foxmail.com>
  8. Date:   Mon Aug 1 15:45:58 2022 +0800
  9.     add file3
  10. commit 19f39c0883a0b5b9e7ebcef4e9efd82c72e8bb44
  11. Author: orion-orion <orion-orion@foxmail.com>
  12. Date:   Mon Aug 1 14:39:11 2022 +0800
  13.     add file2
  14. commit b04ef5bbf5bf70296790c7e9fbe71b27afd04a3f
  15. Author: orion-orion <orion-orion@foxmail.com>
  16. Date:   Mon Aug 1 14:38:46 2022 +0800
  17.     add file1
复制代码
我们想撤销掉上一次的add file4对应的提交,需要运行git reset --soft HEAD~1命令:
  1. commit 0755827bbb67c463cb7ac139f1f86c93006a26e0 (HEAD -> master)
  2. Author: orion-orion <orion-orion@foxmail.com>
  3. Date:   Mon Aug 1 15:45:58 2022 +0800
  4.     add file3
  5. commit 19f39c0883a0b5b9e7ebcef4e9efd82c72e8bb44
  6. Author: orion-orion <orion-orion@foxmail.com>
  7. Date:   Mon Aug 1 14:39:11 2022 +0800
  8.     add file2
  9. commit b04ef5bbf5bf70296790c7e9fbe71b27afd04a3f
  10. Author: orion-orion <orion-orion@foxmail.com>
  11. Date:   Mon Aug 1 14:38:46 2022 +0800
  12.     add file1
复制代码
命令中的HEAD~1意思为将撤销掉上次提交的记录,HEAD~2则为撤销上上次提交之后的所有记录,其余以此类推。
git reset命令可连续使用(对于--mixed、--soft和我们下面将要介绍的--hard选项都如此),比如我们再使用一次命令git reset --soft HEAD~1就会再继续撤销掉上上次提交的记录:
  1. commit 19f39c0883a0b5b9e7ebcef4e9efd82c72e8bb44 (HEAD -> master)
  2. Author: orion-orion <orion-orion@foxmail.com>
  3. Date:   Mon Aug 1 14:39:11 2022 +0800
  4.     add file2
  5. commit b04ef5bbf5bf70296790c7e9fbe71b27afd04a3f
  6. Author: orion-orion <orion-orion@foxmail.com>
  7. Date:   Mon Aug 1 14:38:46 2022 +0800
  8.     add file1
复制代码
注意,reset命令还可以选择--hard选项。--hard选项与soft选项的区别在于,使用--hard选项不仅会撤销掉最后一次的commit,同时也会修改working tree,也就是当前的工作目录。事实上,--hard 标记是reset命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的reset调用都可以轻松撤消,但是--hard选项不能,因为它强制覆盖了工作目录中的文件。因此,使用--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"?
  • [9] Stack Overflow: "What's the difference between HEAD^ and HEAD~ in Git?"

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册