git学习笔记

GIT学习笔记

教程

猴子都能看懂的GIT入门

配置

忽略

一个repo

vim repo的根目录 或 其下任意一个文件夹中,创建.gitignore,则适用于.gitignore所在的文件夹

# 注释
# 以下文件会被忽略

# 相对本.gitignore文件所在文件夹的 路径
/path/relative/to/here
# 相对本.gitignore文件所在文件夹下 任意这样结尾的路径
tail/of/path

# 文件或文件夹
xx/x/xxx
/xx/x/xxx
# 文件夹
xx/x/xxx/
/xx/x/xxx/

# 通配符
xx/xx*xx/xx
xx/xx*
*xx/xx


# 不可写成 `ignore-this-dir`, 这样整个ignore-this-dir文件夹被忽略了, 下面的例外无法生效
ignore-this-dir/*
# 例外,即不忽略
!ignore-this-dir/exception

全局(整个电脑)

vim ~/.gitignore_global

  • 代码行尾勿加注释,会无法执行那一行
# attention: 代码行尾勿加注释,会无法执行那一行

# Windows
Thumbs.db
 # Thumbnail cache files
desktop.ini
# Folder view configuration files

# OS X
.DS_Store
# Folder view configuration files
.Spotlight-V100
# Files that might appear on external disks
.Trashes
# Files that might appear on external disks
._*
# Thumbnail cache files

# Compiled Python files
*.pyc

# Compiled C++ files
*.out

# Application specific files
venv
node_modules
.sass-cache

# Temp File
*.swp
*.swa
*.swo

# github merge file
*.orig

# vscode
.vscode

vim ~/.gitconfig, 添加如下

[core]
        excludesfile = .gitignore_global
        # 其他core配置...

适用于电脑上所有repo

作用

Mac系统

  • ._*: Mac系统打开windows或linux系统的文件,会把的文件xxx.xx的附属信息,写入"._xxx.xx"二进制文件
  • .DS_Store: 记录该文件夹下各文件在访达下的布局
  • 在Mac用Macfusion访问linux服务器上的git项目,需ignore上述文件

文件的状态

其中 ignore是忽略的文件, HEAD是当前所在节点的那次提交版本, index是暂存( git addgit stage)所生成的文件索引, wkdir是工作目录

ignore/HEAD/index/wkdir[file] = None if 其中无此file else 其中此file的内容

文件的状态分如下几种, git的判断机制是

  • 不存在的文件: ignore[file] == HEAD[file] == index[file] == wkdir[file] == None
  • 存在的文件: ! ( ignore[file] == HEAD[file] == index[file] == wkdir[file] == None)
    • 忽略的文件: None != ignore[file]
    • 未被忽略的文件: None == ignore[file] and ! (HEAD[file] == index[file] == wkdir[file] == None)

其中未被忽略的文件进一步判断:

  • 提交文件 commited: None != HEAD[file] == index[file] == wkdir[file]

  • 未提交/不干净的文件 uncommited: index[file] != wkdir[file] or HEAD[file] != index[file]

    • 未跟踪: untracked: None == index[file] != wkdir[file]

    • 未暂存: None != index[file] != wkdir[file]

      unstaged modified: None != index[file] != wkdir[file] != None

      unstaged deleted: None != index[file] != wkdir[file] == None

    • 暂存: HEAD[file] != index[file]

      staged new file: None == HEAD[file] != index[file]

      staged modified: None != HEAD[file] != index[file] != None

      staged deleted: None != HEAD[file] != index[file] == None and HEAD[file] != index[f] for any f in index

      staged renamed: file1-> file2: None != HEAD[file1] != index[file1] == None and HEAD[file1] == index[file2]

checkout的逻辑

改动可否带走

执行上述两命令成功的充要条件为: 一切当前不干净的文件(未跟踪/跟踪未暂存/暂存未提交), 在git checkout去往节点和HEAD节点中完全一样(是否被跟踪一样, 文件内容一样)

执行后, 原来不干净的文件, 其内容/是否被跟踪/是否被暂存 均完全未变

从历史节点开分支

去历史节点试着修改

checkout到历史的节点,可以试着做改动,然后提交(git commit),会变成一个临时分支,而不改动历史。

这个临时分支,只有一个节点hash码、没有分支名。例如下图,checkout到历史节点cf8e102,修改完一些文件再提交,产生临时分支节点883ed24

![屏幕快照 2019-08-19 20.12.00](assets/屏幕快照 2019-08-19 20.12.00.png)

将修改存成分支

处在临时分支节点,可以执行git branch 分支 && git checkout 分支 (等价于 git checkout -b 分支),从而使得临时分支变成一个有名字的分支。

若checkout到别的节点,再git log就不显示这个临时节点了,但是仍然可以git checkout [临时节点的hash码]到这个临时节点。

stash

我们有时会遇到这样的情况,正在dev分支开发新功能,做到一半时有人过来反馈一个bug,让马上解决,但是新功能做到了一半你又不想提交,这时就可以使用git stash命令先把当前进度保存起来,然后切换到另一个分支去修改bug,修改完提交后,再切回dev分支,使用git stash pop来恢复之前的进度继续开发新功能。下面来看一下git stash命令的常见用法.

参考

https://www.yiibai.com/git/git_stash.html

https://blog.csdn.net/qq_32452623/article/details/76100140

https://blog.csdn.net/daguanjia11/article/details/73810577

git stash save与git stash push区别:

save是阉割版的push, 当前推荐使用git stash push命令, save不能指定只存哪个目录, save 写mesage的格式和push不同

储藏

git stash [push [--patch] [-k|--[no-]keep-index] [-q|--quiet]
               [-u|--include-untracked] [-a|--all] [-m <message>]
               [-- <pathspec>...]]
  • -u-a: 不存未跟踪的文件; 只存跟踪且修改了的文件(不论是否add)

ScreenShot 2020-01-11 02.19.49

  • -u-a: 还存未跟踪的文件, 但不存忽略的文件
  • -a: 还存未跟踪的文件, 也不存忽略的文件, 故-a 包含了-u

ScreenShot 2020-01-11 02.59.11

  • 被存的文件都回到HEAD时的样子(包括文件的有无,文件的内容); 未跟踪的文件不变

查看储藏

git stash list
  • 会显示整个分支图中(不仅仅是HEAD的)的所有储藏

ScreenShot 2020-01-11 03.03.33

其中 stash@{0}等, 是stash编号; 在glg中显示的节点hash ( 位于 三角 的最顶上的那个点) 也可以做stash编号

  • glg 只显示 stash@{0} , 此即当前的stash, 整个分支图中仅此一个. 此stash即git stash各个命令里可缺省的stash编号

    ScreenShot 2020-01-11 03.05.22

git stash show [<stash编号>]
  • 会显示跟踪的文件的增删改

    ScreenShot 2020-01-11 03.01.04

取出储藏

当当前目录有储藏的文件改动了, 则无法取出储藏

git stash pop [stash编号] [--index]

等价于 git stash apply [stash编号] [--index]; git stash drop [stash编号]

  • 不加—index: 原来跟踪且修改的文件还原, 现在都没git add
  • 加 index: 原来跟踪且修改的文件还原, 分别还原到add和没add的状态
  • glggit stash list中看不到这个储藏了

ScreenShot 2020-01-11 02.28.46

git stash apply [stash编号] [--index]
  • 不加—index: 原来跟踪且修改的文件还原, 现在都没git add
  • 加 index: 原来跟踪且修改的文件还原, 分别还原到add和没add的状态
  • glg 和 git stash list中还能到这个储藏了

ScreenShot 2020-01-11 02.25.59

git stash drop [stash编号]
  • 当前文件的状态不变
  • glg 和 git stash list中不能到这个储藏了

ScreenShot 2020-01-11 02.28.46

git stash clear
  • 等价于 git stash drop [stash编号] 对于所有 [stash编号]
git stash branch <branchname> [<stash编号>] [--index]

等价于 git stash pop [<stash编号>] [--index]; gb <branchname>

取消apply储藏

在某些情况下,可能想应用储藏的修改,在进行了一些其他的修改后,又要取消之前所应用储藏的修改。Git没有提供类似于 stash unapply的命令,但是可以通过取消该储藏的补丁达到同样的效果:

法一

git stash show -p [stash编号] | git apply -R

可能会想要新建一个別名,在你的 Git 里增加一个 stash-unapply命令,这样更有效率。例如:

git config --global alias.stash-unapply '!git stash show -p | git apply -R'
git stash apply # 不能有 --index
# (... work work work # 不能改储藏的文件)
git stash-unapply # 不会还原 git stash push -u 所储存的未跟踪文件
# 回到干净的目录

法二

如果在应用储藏前, 目录是干净的, 则可直接

cd "`git rev-parse --show-toplevel`"  # 来到当前repo的根目录
git reset HEAD  # 取消暂存(git add)的所有文件
git checkout  . # 取消被跟踪文件的所有修改
git clean -df  # 删除未跟踪也未被忽略的文件夹(-d)和文件(-f)

rebase

带上sub-branch一起rebase

https://stackoverflow.com/questions/32652293/automatically-rebase-git-sub-branches

http://voidcanvas.com/how-to-rebase-a-branch-when-the-parent-is-rebased-with-another/

https://stackoverflow.com/questions/43711961/rebase-a-branch-that-has-child-branches

rebase 一棵树 https://stackoverflow.com/questions/14504029/git-rebase-subtree/45861361#45861361

git checkout dev
git rebase --preserve-merges master

ScreenShot 2020-01-13 16.21.31

What exactly does git’s “rebase --preserve-merges” do (and why?)

Rebase a tree: https://stackoverflow.com/questions/17315285/rebasing-a-tree-a-commit-branch-and-all-its-children

https://stackoverflow.com/questions/17106840/git-rebase-with-branches-whole-tree

对比rebase/merge的化解冲突

参考

merge

初始状态: 当前分支是A

o-A(ours,HEAD)

 o-o-B(theirs)

执行 git merge B, 得到最终状态:

o-o(ours:原A)-A(HEAD)
             /
 o-o-B(theirs)

若中间发生冲突, 文件中显示

<<<<<<< HEAD
A的内容
=======
B的内容
>>>>>>> B分支名

解决冲突的方法

  • 手动修改冲突的文件

  • 只保留A, 或只保留B

git checkout --ours 冲突的文件()# => 保留A的内容
git checkout --theirs 冲突的文件()# => 保留B的内容

若要对repo内所有文件采样上述策略, 则 冲突的文件(夹)名 写作 "$(git rev-parse --show-toplevel 2> /dev/null)”

  • 整个repo都保留B的内容, 也可执行 git merge —skip, 这个命令不存在, 只能

解决完冲突, 执行 git add -A "$(git rev-parse --show-toplevel 2> /dev/null)", 然后 git commit , 则来到最终状态. 注: git merge --continue命令在老版本的git没有, 如git2.7.4没有, git2.20.1有.

若放弃merge, 则执行 git merge --abort, 则回到初始状态.

rebase

初始状态: 当前分支是A

   A1-A2-A(HEAD)
 /
o-o-B

执行 git rebase A

则git内部会先 git checkout B, 得到中间状态:

   A1-A2-A(theirs)
 /
o-o-B(ours,HEAD)

再将A分支rebase, 然后 git checkout A , 从而变成最终状态:

      A1-A2-A(HEAD)
     /
o-o-B

若发生冲突, 则当前处在中间状态. 冲突的文件所显示如下, HEAD所指的分支正好和merge冲突时相反

<<<<<<< HEAD
B的内容
=======
A的内容
>>>>>>> 当前所rebase的A的某次提交的message (即A1或A2的message)

解决冲突的方法:

  • 手动修改冲突的文件

  • 只保留A, 或只保留B: 解决冲突的命令中, ours和theirs 也与merge冲突时相反

git checkout --ours 冲突的文件()# => 保留B的内容
git checkout --theirs 冲突的文件()# => 保留A的内容

若要对repo内所有文件采样上述策略, 则则 冲突的文件(夹)名 写作 "$(git rev-parse --show-toplevel 2> /dev/null)”

  • 整个repo都保留A的内容, 也可执行 git rebase —skip. 区别如下

    git rebase —skip (推荐 ): 若某文件, B 分支删除 而A分支当前冲突的commit有, 则此命令会删除此文件
    git checkout --ours "$(git rev-parse --show-toplevel 2> /dev/null)": 若某文件, B 分支删除 而A分支当前冲突的commit有, 则此命令不会删除此文件

解决完冲突, 执行 git add -A "$(git rev-parse --show-toplevel 2> /dev/null)", 然后git rebase --continue, 则来到最终状态

若放弃merge, 则执行 git rebase --abort, 则回到初始状态.

批量化解冲突:

一个批量处理脚本。这个脚本根据设定的规则自动执行指令,让我在三分钟之内完成了一个14步(以 git rebase --continue 计)

rebase的缺点

  • git rebase会产生连环冲突,git merge不会

    这是因为

      A1-A2-A3(A)
     /
    B1-B2-B3(B)

    在A, git rebase B, 则会依次将 A1, A2, A3 rebase 到 B3. 若B3中某文件与A1-A3均冲突, 就会连环冲突.

    例如B1中有文件a, B3中无文件a, 而A1-A3均修改了文件a, 则要保留B3的文件, 则会连环冲突, 中间有三次要执行若git rebase —skip `来化解冲突, 保留B3的文件. 最终rebase结束将无a文件.

  • 有时git rebase会产生冲突,git merge不会

      A1-A2(A)
     /
    B1-B2-B3(B)

    B1有文件a, B2,B3未动a, 动了别的文件; A1将a删除, A2将a恢复了a.

    则在A, git merge B, 不会出冲突. 而 git rebase B 则在rebase A1时会冲突.

  • push后merge可以原样保留正确运行的历史commit,而rebase不会

    例如

      A1-A2(A)
     /
    B1-B2-B3(B)

    在A, git rebase B, 则得到的A1节点, 是由 B3 git merge A1所得. 故A1节点内容可能发生变化, 从而未被能正确运行.

    而在A, git merge B, 则A1节点未改动, 故A1还能正确运行.

git rebase 的例子

  • 当前

image-20200113214519713

git rebase dev
# 或
git rebase --onto dev a2405ce f1

得到

image-20200113214619559

  • 当前

image-20200113213842403

 git rebase dev
 # 或
git rebase --onto dev a2405ce f1

得到

ScreenShot 2020-01-13 21.37.05

cherry-pick

教程: git cherry-pick的使用 cherry-pick 挑好看的小樱桃

不存在git cherry-pick —squash, 得用 git cherry-pick -n, 详见