Better

Ethan的博客,欢迎访问交流

再看Git

再看廖雪峰git系列

git简介

集中式与分布式

集中式

  • 版本库集中存放在中央服务器
  • 比如要联网才能使用

分布式

  • 没有“中央服务器”,每个人的电脑上都是一个完整的版本库
  • 因为版本库就在你自己的电脑上,也就无需联网
  • 分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
  • Git的优势不单是不必联网这么简单,后面我们还会看到Git极其强大的分支管理,把SVN等远远抛在了后面。

和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。

创建版本库

版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

直接创建文件夹,运行git init即可,会在当前目录下多了一个.git目录,这个目录是Git来跟踪管理版本库的,如果你没有看到.git目录,那是因为这个目录默认是隐藏的,用ls -ah命令就可以看见。

所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。

添加文件到Git仓库,分两步:

  1. 使用命令git add <file>,注意,可反复多次使用,添加多个文件;
  2. 使用命令git commit -m <message>,完成。

时光穿梭机

版本回退

git status:查看当前仓库状态,比如

  • 本地修改了哪些文件(Changes not staged for commit)
  • 哪些文件待提交(Changes to be commited)
  • 哪些文件是新增的(untracked file)
  • 未合并文件(unmerged paths)

git diff <file>:查看具体修改了什么内容

你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

git log命令显示从最近到最远的提交日志,如果嫌输出信息太多,看得眼花缭乱的,可以试试加上git log --pretty=oneline参数。

我们可以看到通过SHA1计算出来的commit id。接下来我们通过 git reset 命令回退到上一个版本。

git reset --hard HEAD^

接下来通过git log来查看现在版本库的状态,会发现最新的 commit 不见了,此时如果我们想回去怎么办,可以找到之前 git log 打印出来的 commit id,通过如下命令回去(版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。)

git reset --hard commit id

此时会发现版本回去了,可是总感觉不对劲,what~~~!如果我窗口关了,找不到 commit id,难道就回不去了?你不是在开玩笑吧。

在Git中,总是有后悔药可以吃的。当你用git reset --hard HEAD^回退版本时,想再回去就必须知道之前的commit id,Git提供了一个命令git reflog用来记录你的每一次命令。

总结

  • HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id。
  • 穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。
  • 要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。

扩展

  • git reset –-soft:回退到某个版本,只回退了commit的信息,不会恢复到index file一级。如果还要提交,直接commit即可;
  • git reset -–hard:彻底回退到某个版本,本地的源码也会变为上一个版本的内容,撤销的commit中所包含的更改被冲掉
  • git reset (--mixed):回退到某个版本,撤销commit和add,工作区内容不变

工作区和暂存区

工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。

0.jpg

把文件添加到版本库,分两步的具体解释如下

  1. 第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
  2. 第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。

因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。

管理修改

为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。

如何理解这个呢?比如我们按照这个流程:第一次修改 -> git add -> 第二次修改 -> git commit,你会发现第二次修改并没有提交。

撤销修改

丢弃工作区的修改,两种方式

  • 手动还原,但可能比较啰嗦
  • 使用 git checkout -- file,这里有两种情况
    • 修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
    • 已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
    • 就是让这个文件回到最近一次git commit或git add时的状态。

git checkout -- file命令中的--很重要,没有--,就变成了“切换到另一个分支”的命令

如果你不小心使用git add加入到暂存区了,怎么办呢?用命令git reset HEAD <file>可以把暂存区的修改撤销掉(unstage),重新放回工作区

git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本

如果问题来的更加严重一点,不但改错了东西,还从暂存区提交到了版本库,此时我们还可以使用版本回退来达到目的。前提是你没有把本地版本库推送到远程。如果推送到远程了,这次提交就真的干不掉了。

删除文件

直接手动删除文件,或者使用rm删除文件,可以理解成一次本地一次修改操作。确实要从版本库中删除的,可以使用命令git add,并且git commit即可。

更快捷的方式,你可以使用git rm file,然后直接git commit即可。经实践,git rm file等同于rm file && git add file

如果是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本

git checkout -- test.txt

git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

如果删除了且添加到了暂存区,则需要两个步骤

git reset HEAD test.txt
git checkout -- test.txt

命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。

远程仓库

创建SSH Key。在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsa和id_rsa.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:

ssh-keygen -t rsa -C "youremail@example.com"

登陆GitHub,打开“Account settings”,“SSH Keys”页面:然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容即可。

note:非必须,只不过方便我们提交,同时通过添加多个 SSH Key 方便协同开发

添加远程库

在 Github 上 Create a new repo 仓库后,将本地仓库和远程仓库进行关联,然后把本地仓库的内容推送到 GitHub 仓库。添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。

git remote add origin https://github.com/liuxinqiong/learngit.git
git push -u origin master

把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。

由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。

-u参数的作用

  • 如果当前分支与多个主机存在追踪关系,那么这个时候-u选项会指定一个默认主机,这样后面就可以不加任何参数使用git push
  • 如果不想推送默认主机呢,可以通过 git push origin master,推送特定主机的特定分支

从远程库clone

git clone

分支

分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

创建与合并分支

为什么 git 相比 svn,创建分支,切换分支,合并分支,会那么快呢?因此至始至终都只是操作了指针而已。

比如新建 dev 分支,底层就是新建一个dev指针,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上。

从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变。

分支合并,直接把master指向dev的当前提交,就完成了合并。

合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉。

下面具体操作一下

# 新建分支并切换 -b表示切换
git checkout -b dev
# 相当于一下两条命令
git branch dev
git checkout dev
# git branch命令会列出所有分支,当前分支前面会标一个*号
git branch
# 切换回master
git checkout master
# git merge合并指定分支到当前分支
git merge dev
# 合并完成后,删除dev分支
git branch -d dev

因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。

分支合并有两种方式,一种是merge,一种是rebase,rebase可以多了解一下

解决冲突

人生不如意之事十之八九,合并分支往往也不是一帆风顺的。

比如我们新建feature1分支,然后对readme.txt文件进行了修改,然后切换回去master分支,对同一个文件进行了修改(或者是队友进行了修改),这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突。

此时Git会提示我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件。

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,我们修改如下后保存。

git add readme.txt 
git commit -m "conflict fixed"

用带参数的git log也可以看到分支的合并情况

git log --graph --pretty=oneline --abbrev-commit

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

# 请注意--no-ff参数,表示禁用Fast forward
# 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
git merge --no-ff -m "merge with no-ff" dev

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

在实际开发中,我们应该按照几个基本原则进行分支管理:

  • master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活
  • 干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
  • 你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

现场储存

在实际开发中,你在dev上进行的工作还没有提交,因为你只进行到一半,还没发提交,但是需要紧急修复bug,此时怎么办呢。

Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作。

git stash

首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支

git checkout master
git checkout -b issue-101

修复完bug后,切换回master分支,合并且删除issue-101分支,此时继续自己的开发,回到dev分支,此时如何恢复现场呢

# 查看工作现场列表
git stash list
# 恢复不删除,需要额外使用git stash drop删除
git stash apply
# 恢复删除
git stash pop

可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,git stash apply stash@{0}

feature分支

主要知识点:未被合并的分支删除会报错,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。

多人协作

查看远程库信息

git remote
# 查看更详细信息
git remote -v

推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上

git push origin master
git push origin dev

并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?

  • master分支是主分支,因此要时刻与远程同步;
  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
  • feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

抓取分支

当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支。

你的小伙伴要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支

# 在本地创建和远程分支对应的分支,本地和远程分支的名称最好一致;
git checkout -b dev origin/dev

小伙伴修改了文件,提交到了远程。此时你也对同样的文件进行了修改,并试图推送。这是Git提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送。这样其实也很美好。问题在于git pull也失败了。心塞了。原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接

git branch --set-upstream-to=origin/dev dev

此时再次git pull就成功啦。接下来要做的就是解决冲突,并提交即可。

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin 推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin 推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to origin/

标签管理

发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。

Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。

Git有commit,为什么还要引入tag?tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

# 创建tag
git tag v1.0
# 查看所有tag
git tag
# 默认标签是打在最新提交的commit上的。也可以指定commit
git tag v0.9 f52c633
# 标签不是按时间顺序列出,而是按字母排序的。可以用git show <tagname>查看标签信息
git show v0.9
# 可以创建带有说明的标签,用-a指定标签名,-m指定说明文字
git tag -a v0.1 -m "version 0.1 released" 1094adb

注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

如果tag打错了,使用如下命令删除

git tag -d v0.1

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除

如果要推送某个标签到远程,使用命令git push origin 推送指定tag,全可以推送全部tags

# 指定tag
git push origin v1.0
# 一次性推送全部尚未推送到远程的本地标签
git push origin --tags

推送成功后,可以在github的release下看到具体记录!

如果想删除已经推送到远程的tag,操作如下,相对麻烦一些

# 本地删除
git tag -d v0.9
# 推送远程
git push origin :refs/tags/v0.9

使用GitHub

如何参与开源项目呢?

  1. 点fork,在自己帐号下克隆项目,然后从自己的账号下clone。一定要从自己的账号下clone仓库,这样你才能推送修改。
  2. 代码写完后,往自己仓库推送
  3. 如果希望官方库能接受你的修改,你就可以在GitHub上发起一个pull request。

使用码云

主要知识点,学会如何关联多个远程库。如果我们在码云上,新建远程仓库,想和和我们本地库关联。

git remote add origin url

会提示报错remote origin already exists.,这说明本地库已经关联了一个名叫origin的远程库,此时,可以先用git remote -v查看远程库信息:

git remote -v
# 删除远程关联,这样再次关联码云就不会报错,但是这里我们想关联多个
git remote rm origin
# 主要原因是origin名称已经被占用,我们换个名字就好了,比如使用gitee名称
git remote add gitee url

添加多个后,推送就需要指定推送到那个远程库啦

git push github master
git push gitee master

自定义git

忽略特殊文件

在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。

不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore

忽略文件的原则是:

  • 忽略操作系统自动生成的文件,比如缩略图等;
  • 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  • 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

设置别名

通过设置别名达到简写命令的目的

git config --global alias.shortName fullName

配置Git的时候,加上--global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。

  • 配置文件放哪了?每个仓库的Git配置文件都放在.git/config文件中
  • 当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中

团队协作

github flow工作流:基于分支的工作流

  1. 创建一个分支
  2. 添加新版本
  3. 开启PR
  4. 讨论和代码审核
  5. 合并分支和部署

代码协作开发两种情况

  • 彼此互相熟悉的团队间的协作
    1. 给队员添加写权限:Settings -> Collaborators -> add collaborator
    2. 如果是小功能,直接一起推送master也可以,如果是大功能,开发周期长,需要讨论和测试的,就有必要走完整的github flow流程了
  • 开源项目中和互不相识的开源贡献者合作

贡献开源项目流程:核心也是PR

  • fork项目,因为不可能像自己团队一样,给每个人添加写权限
  • clone项目,修改且同步
    • 作者如何知道谁fock了和查看commit的内容呢。graph -> network
  • pull request
    • 贡献者在fork的项目下,pull request -> new pull request
    • 此时会对比作者源项目分支和你自己项目的分支,填写内容确定即可。
    • 这样一来作者就可以看到你提交的PR

quick pull request:如果只是简单的修改某一个文件,完全可以不借助客户端和编辑器,直接找到想要修改的文件,点击edit,GitHub检测到你没有权限时,会自动fork到你账户下,你修改后,支持直接pull request。如果你本身对这个项目有写权限,就会有一个额外的选项,直接提交到分支。

项目基础设施

  • GitHub pages搭建项目主页
  • wiki
  • issue
    • 通过右侧的Assignee,可以选择分配给谁处理
    • 讨论区可以@任意用户
    • 通过>来表示回答某个讨论
    • 引用已经写过的内容,点击issue下某个讨论的时间,复制浏览器链接,然后发布即可
    • 通过#number直接指向某个issue
    • issue能不能提交代码呢?可以的。添加代码时,在版本留言里通过#number关联特定issue,此时在issue中可以看到关联的代码
    • 如果修复了issue,直接在版本留言里写,xxx fix #number,可以直接关闭指定的issue

GitHub Pages

  • 用户或组织网站
  • 项目网站
  • 方式
    • 自动生成器
    • 手写
  • 手写方式
    • 创建gh-pages分支(对名字有特殊要求)
    • username.github.io/projectname

GitHub密码机关

经验

  • 多用客户端工具,少用命令行(source tree、github官方客户端)
  • 每次提交代码前,diff自己的代码,以免提交错误的代码
  • 下班之前,整理好自己的工作区
  • 并行的代码,一定要使用分支开发
  • 遇到冲突时,搞明白冲突的原因,千万不要随意丢弃别人的代码
  • 产品发布后,记得打tag,方便我们日后拉分支修bug

git flow工作流

git flow:围绕项目发布的严格分支模型(感觉source tree就是为他定制的哈)

o_git-workflow-release-cycle-4maintenance.png

release分支:只应该做Bug修复、文档生成和其他面向发布的任务



留言