Git配置相关

  • (全局)配置:如果是局部配置,每个仓库都需要进行配置

    1
    2
    3
    4
    5
    6
    7
    8
    
    # 设置全局配置
    # git config --global user.name "zhangqingan"
    # git config --global user.email "zhangqingannn@bupt.edu.cn"
    # git config --global https.proxy http://127.0.0.1:7890
    # git config --global https.proxy http://127.0.0.1:7890
    # 清除全局配置
    # git config --global unset user.name
    # git config --global unset user.email
    
    • 如果是针对仓库的局部配置

      1
      2
      3
      
      git config --local user.name "zhangqingan"
      git config --local user.email "zhangqingannn@bupt.edu.cn"
      # git config --local --list
      
  • 生成密钥对

    1
    2
    3
    4
    
    ssh-keygen -t rsa -C "zhangqingannn@bupt.edu.cn"
    # 并且后续生成密钥的位置自定义,注意win上这里是一个目录,linux上是文件名
    
    ssh-keygen -l -f key # 查看密钥的contents(SHA256+comments)
    
  • 添加私钥

    1
    2
    3
    4
    
    # ssh-agent bash
    
    ssh-add private_key # 将私钥添加到本地
    ssh-add -l # 查看当前添加的私钥
    

    ssh agent详解

  • 修改配置文件:修改~/.ssh/config

    1
    2
    3
    4
    5
    
    Host github(bupt)
      User QinganZhang
      HostName ssh.github.com
      IdentityFile /home/zqg/.ssh/github/bupt
      Port 443 # or 22
    
  • reference and more reading

相关概念

  • 顶层概念:

    • Workspace:工作区,本地的工作目录
    • Repository:包含.git目录的工作区,其中.git为版本库,其中保存了stage暂存区、第一个分支main、指向main的指针HEAD
    • Index/Stage:暂存区
    • Remote:远程仓库
  • 底层概念:from missing semester中的Git

    • 文件称为Blob对象(数据对象),目录称为tree,每个commit即为一个对文件和目录的快照的指针(保存了当前的整个仓库,或者说追踪最顶层的树)

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      // 文件就是一组数据
      type blob = array<byte>
      
      // 一个包含文件和目录的目录
      type tree = map<string, tree | blob>
      
      // 每个提交都包含一个父辈,元数据和顶层树
      type commit = struct {
          parent: array<commit> // 一个commit可能有多个parent
          author: string
          message: string
          snapshot: tree // 追踪最顶层的树
      }
      
    • 对象可以是文件、目录或者commit,对象通过SHA-1哈希进行按名存取(文件和目录好说,但是commit就不好访问了,因此统一使用SHA-1哈希)

      1
      2
      
      type object = blog | tree | commit
      objects = map<string, object>
      
    • 为了方便好记,使用引用来指向最近一次的commit,而不是来记一串哈希值。因此,分支就是可变的引用,而标签就是绑定到特定commit的引用,HEAD指向当前的commit

      1
      
      references = map<string, string>
      
    • 因此,Repository保存的就是对象和引用。

    • 前面说commit是一个对文件和目录的快照,是通过向暂存区进行若干次操作,确认保存操作的结果,即形成一个快照

  • 其他概念:

    • track/untrack:文件或目录是否纳入到git的版本控制范围内

基础操作

常用操作

  • git init -b main:创建.git文件夹,将当前目录变成一个仓库,默认分支为main

  • git add:将文件添加到暂存区(stage),相当于创建了快照,否则文件就是untracked的

    • git diff:比较当前工作区和暂存区快照之间的差异,即修改之后还没有暂存起来的变化内容
  • git commit -m "comment":将暂存区的修改提交到分支(多次add之后进行一次commit),commit就是一个快照

    • git diff --staged:比较暂存区快照和最近一次commit之间的差异
    • git commit -a -m "comment" :将所有跟踪过的文件暂存起来一起提交,跳过了git add步骤
  • git status:查看当前文件夹中文件的状态

  • git rm --cached <file>:取消对file的跟踪

  • git diff

  • git log:查看commit历史,最近的排在最上面

    • -p (--path):显示每次提交所引入的差异(按补丁的格式输出)
      • 补丁:即两次文件的差异
    • --stat:显示每次提交的简略统计信息
    • --ptetty:设置输出模式,可以自定义输出格式
    • --decorate:查看各个分支当前所指向的对象
    • --graph
    • -n:限制输出长度
    • --since, --until
    • --grep:搜索提交说明中的关键字
    • -S:pickaxe选项,接受一个字符串,搜索那些添加或删除了该字符串的提交
    • -- path:输出某些文件或目录的历史提交,注意这个参数是放在最后的(因此用两个短线隔开)

版本控制

  • 版本回退

    • git reset --hard HEAD^:返回到最近一次commit
      • HEAD指向最近一次的commit,HEAD^HEAD^^分别表示上一个commit和上上个commit
    • git reset --hard commitId:返回到特定的commit
      • git log --pretty=oneline :显示所有提交过的commit,不包括已经回退的commit记录
      • git reflog:显示所有提交过的commit,包括回退的操作,比如回退之后又反悔了,需要使用reflog来找到新版本对应的commit id
  • 撤销修改

    • 如果只是在本地工作区修改了,还没有git add:git restore <file>或者git checkout -- <file>
    • 如果在本地工作区修改之后,已经git add:git restore --staged <file>或者git reset HEAD <file>
    • 如果已经git commit,则进行版本回退
  • 重新提交:如果上一次commit完成后,发现漏了些文件,此时先git add,然后使用git commit --ament,这样只会有一次提交,后一次的提交会覆盖前一次的提交

远程库

对远程库的操作实际上都是对远程库中远程分支的操作,默认远程库为origin,远程分支为 与当前本地仓库分支 同名的远程分支

  • 远程库的几种使用场景:

    • 克隆别人的仓库:git clone默认只有main分支,使用git checkout -b dev origin/dev在本地创建dev分支,并且与远程的dev分支关联起来

    • 自己在GitHub上新建一个仓库,然后clone下来进行开发

    • 自己在本地目录下,先git init创建本地仓库,然后将本地仓库与远程仓库关联起来:git remote add <shortname> <url>

      • <url>支持多种协议,<shortname>即代表了该url

      • 一般远程库的名字就叫origin

      • 可以关联多个远程库,比如本地仓库关联一个共有的和一个私有的仓库

  • 查看远程库信息:git remote -v

    • 远程库可能有多个
  • 获取远程库的更新:

    • git fetch <remote>:只会将远程库的更新下载到本地仓库,不会自动进行合并
    • git pull:如果当前分支设置了跟踪远程分支,则git pull会拉取更新,并自动进行合并。如果有冲突,需要手动解决冲突。
    • 详解git pull和git fetch的区别
  • 将本地库的内容推送到远程库:git push <remote> <branch>

    • 默认remote是origin,默认将本地分支推送到远程的同名分支上
    • git push -u origin main:第一次push时,远程库是空的,此时不但会将本地库的内容推送到远程库,而且将本地的main分支和远程库的main分支关联起来
    • 如果他人先于你push到远程,你的push会被拒绝,此时需要拉取更新,手动修改冲突的部分,合并之后才能再push
  • 查看某个远程仓库:git remote show <remote>

  • 远程仓库重命名:git remote rename old-name new-name

  • 解除本地和远程库之间的关联关系:git remote rm origin

分支管理

分支是指向commit的可变指针,默认名字为main,main分支在每次提交时都自动向前移动

HEAD是一个指向当前所在的本地分支的指针(可以想象为当前分支的别名)

基本操作

注意这些操作都是在本地仓库的

  • 创建分支:git branch dev

    • --merged:查看哪些分支已经合并到当前分支
    • --no-merged:查看所有包含未合并工作的分支(即没有汇聚到当前分支的那些分支)
  • 重命名当前分支:git branch -M newname

  • 切换分支:git checkout dev

  • 创建并切换分支:git checkout -b dev或者git switch -c dev

  • 合并指定分支到当前分支:git merge dev

    • 如果当前分支main和待合并分支dev存在冲突,此时进行了合并,但是没有创建一个新的commit,需要先解决冲突,再手动进行commit,解决冲突就是手动将git merge失败的文件手动进行编辑,可以使用git status查看unmerged的文件,然后使用git merge --continue继续合并过程。
    • 因此使用分支时应该在main分支中生成多个dev分支,最后选择合适的dev分支进行合并,而非直接在main分支上进行修改
    • 默认合并分支时使用Fast forward模式,此时删除分支之后,会丢掉分支信息。如果禁用fast forward模式(--no-ff),git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息
      • fast forward模式的含义:比如dev是master的直接后继,即master之后没有分叉,此时将dev merge到master上时,直接移动master的指针即可
  • 查看分支:git branch

    • -vv:列出每一个分支正在跟踪的远程分支,以及ahead和behind信息
      • 如果需要查看最新的信息,则需要更新远程的信息,git fetch --all; git branch -vv
  • 删除分支:git branch -d dev

    • 如果当前分支还没有被合并,而且需要删除当前分支,需要使用git branch -D dev强行删除
远程分支

远程跟踪分支是远程分支状态的引用,相当于书签。比如远程仓库命名为origin,拉取该仓库的main分支,因此本地就将对应的commit叫做origin/main,在本地仓库同样有一个main分支,比如本地进行多个commit会ahead of origin/main。Git官方文档-3.5远程分支

  • 比如当拉取本地没有的、位于远程的新的分支b时,本地只会有一个不可修改的origin/b指针,本地不会自动生成一份可编辑的副本。因此,需要使用git merge origin/b将远程分支b合并到本地当前分支,或者git checkout -b b origin/b将远程分支拉取到本地的新分支b上(如果本地仓库没有分支b,而且远程分支只有一个叫做b的分支,则一个快捷方式为:git checkout b
  • 修改或设置跟踪的上游分支 :git branch -u origin/b
  • 删除远程分支:git push origin --delete b
    • 这个操作只是从服务器上移除这个指针,实际物理删除需要等到过一段时间git服务器进行垃圾回收时,因此误删通常是容易恢复的
最佳实践-修复Bug
  • 背景:比如main分支上有一个bug,但是当前在dev分支上,而且针对dev的工作还没有完成(即当前不能commit到dev分支,从而清空status)
  • 大致流程为:
    • 使用git stash将当前工作区保存起来,此时工作区恢复到最近一次commit时的状态
    • 然后修复bug:先切换到main分支,创建修复bug的分支,修复bug,然后再合并到main分支,同时也需要将修复bug这个commit合并到其他dev分支,切换到dev分支,然后git cherry-pick
      • git cherry-pick commit_id:复制一个特定的commit到当前分支(相当于在dev分支上将修复bug的操作重新进行commit,因此生成的commit id和在main分支上的commit id不同)
    • 修复完bug再恢复当前工作区
      • 查看暂存的工作区:git stash list
      • 恢复暂存的工作区:
        • git stash apply:恢复之前保存的工作区,但是保存的工作区内容还在stash中,需要使用git stash drop进行删除。可以恢复指定的工作区:git stash apply stash{0}
        • git stash pop:恢复工作区的公式,也将保存在stash中的内容删除
Rebase

用来将一组commit按照顺序(即某一分支上的commit)合并到一个特定的commit后面(即另一个分支的最后)

标签管理

标签就是指向commit的指针,但是分支可以移动,标签不能移动

在关键commit节点,使用commit id不方便,因此标签绑定到该commit id

  • 打标签:git tag v1.0

    • 在某个特定的commit上打tag,并添加说明:git tag -a v1.0 -m "comments" commitId
    • 这里说的标签指的是轻量标签(某个特定提交的引用),附注标签指的是上面添加的说明
  • 查看所有标签:git tag

    • 标签不是按照时间顺序列出,而是按照字母排列列出
    • 查看标签信息:git show v1.0
  • 删除标签:git tag -d v0.9可以删除本地标签

    • 删除远程标签时,先从本地删除,然后使用从远程删除。从远程删除:
      • 第一种方法:git push origin :refs/tags/v0.9(即将冒号前面的空值推送到远程)
      • 第二种方法:git push origin --delete v0.9
  • 推送标签到远程:git push origin v1.0

    • 默认情况下git push不会将标签推送到远程仓库上
    • 一次性推送所有标签到远程:git push origin --tags
  • 检出标签:git checkout <tag>,即将HEAD移动到指向某个标签,此时仓库处于detached HEAD状态

自定义Git

忽略特殊文件.gitignore

一些.gitignore模板

虽然某个文件可以匹配到gitignore的规则,但是需要强制添加:git add -f myfile

或者某个文件应该可以添加但仍然被忽略了,说明gitignore规则有问题,找出对应的规则条目:git check-ignore -v myfile

在线生成gitignore文件:Gitignore Online Generator

配置别名

  • 几个例子:

    • git config --global alias.st status

    • git config --global alias.unstage 'restore --stage':将add到暂存区的修改撤销掉

    • git config --global alias.last 'log -1':显示最后一次提交信息

    • git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit":自定义git log显示

  • 删除别名时,只需要在仓库的配置文件.git/config或是用户的配置文件.gitconfig[alias]段落中,删除掉特定的行即可

工作流

向一个项目贡献

提交准则
  • 提交不应包含trailing whitespace。git apply应用补丁时会检测空白错误,默认情况下,尾部空白,包含空白的空行,初始tab缩进之后紧跟的空白字符会被认为是错误。参考

    • 在git apply patch时,应该先git diff --check,将会找到可能的whitespace errors并列出来

    By default, trailing whitespaces (including lines that consist solely of whitespaces) and a space character that is immediately followed by a tab character inside the initial indent of the line are considered whitespace errors.

    —— from git-diff

  • 让每个commit解决一个问题,不要多个问题混在一个commit中

  • 重视写commit message

私有开发项目

Git文档中一个私有开发项目的例子

私有管理团队

多个开发者在feature分支上工作,只有整合者才能将feature分支merge到master分支

Git文档中一个私有管理团队的例子

派生的公开项目

先fork公开项目,然后自己进行修改,最后通过Pull Request请求合并

Git文档中一个派生的公开项目的例子

通过邮件的公开项目

使用git format-patch生成mbox文件,它将每一个提交转换为一封电子邮件,其中保留了所有的提交信息。最后通过git send-email发送补丁。

维护项目

在主题分支中工作
应用来自邮件的补丁
  • 使用git apply应用补丁
    • 补丁是通过git diff生成的,可以对补丁进行检查git apply --check
    • git apply要么全部应用补丁,要么全部不应用,不会部分应用补丁
    • git apply之后,需要手动暂存并提交
  • 使用git am应用补丁(推荐)
    • 补丁是通过git format-patch生成的,此时补丁中包含了作者信息和commit message,因此更加推荐
    • git am是为了读取mbox文件而构建的,mbox是一种用来在单个文本文件中存储一个或多个电子邮件消息的简单纯文本格式
    • git am会自动创建一个新的提交,作者信息和提交消息来自于mbox文件,并自动应用mbox指向的补丁
    • 如果发生冲突,则同样需要手动进行修改,然后暂存,再git am --resolved继续应用下一个补丁
  • git生成patch和打patch
检出远程分支
  • 背景:别人fork了自己的仓库,并且在某个分支上进行了修改,想提交贡献,此时我得到了它的仓库的URL和对应的分支
    • 如果想与他人建立长期的合作交流:将其仓库添加为远程仓库,fetch到本地并在本地checkout到该分支,进行测试
    • 如果别人只是偶尔提供一个贡献
      • 直接pull到本地(不会将该URL添加为远程仓库),然后切换分支并进行测试
      • 或者使用电子邮件来接受patch(或者使用托管服务)
确定引入了哪些东西
  • 检查main分支未包含的commit,比如检查某个分支contrib上引入的修改:git log contrib --not main

  • 如果想具体查看contrib分支上相对于原来main,到底有什么区别(即diff),使用git diff main...contrib,即对contrib分支的最新提交和两个分支的最近共同祖先进行比较(注意三个点)

    • 因为在进行contrib分支上的工作时,main分支可能同时继续向前,diff比较时的main分支应该为原来的位置,即为contrib和现在main分支的最近共同祖先
将贡献的工作整合进来
  • 合并工作流:将主题分支合并到main分支,然后删除主题分支

    • 如果项目很重要,可以使用两阶段循环合并,即维护两个长期分支(main和develop分支),新代码首先合并到develop分支,打标签发布时才将main分支更新到稳定的develop分支
  • 为了保持线性的提交历史,可以在 main分支上对贡献来的工作进行变基而不是直接合并。另一种类似效果的方式是,提取分支的补丁,然后应用到当前分支上

  • Rerere:重用已记录的冲突解决方案,是一种简化冲突解决的方法。当启用 rerere 时,Git 将会维护一些成功合并之前和之后的镜像,当 Git 发现之前已经修复过类似的冲突时, 便会使用之前的修复方案,而不需要你的干预。

发布
  • 为发布打标签
  • 生成一个构建号
  • 创建一个归档文件
  • 制作提交简报

基于Github的工作流

常见问题

ssh -T git@github.com 连接超时

示例:image-20240129141458740

ssh -T git@github.com的含义:

image-20240129140801719

解决方法:

  • 如果使用ssh协议:修改HostName或者修改Port

    • .ssh文件夹下的config文件中修改:参考

      1
      2
      3
      4
      
      Host github.com 
          HostName ssh.github.com
          User xxx
          IdentityFile xxx
      
      image-20240129143747231
  • 如果使用https协议:参考