Git 学习笔记

这个工作流很重要。

主要就是 3 部分:

  1. 工作区(workspace):是当前实际看到的文件目录

  2. 暂存区(Index 或叫 stage):可以 临时 保存你的改动

  3. HEAD:是一个指针,指向当前的版本 (目前是自动创建的第一个分支master

其中,暂存区 stageHEAD所在的区域叫仓库(或版本库)repository

为什么叫仓库?因为 repository 保存了当前版本的项目的所有内容,而工作区workspace只是一个外在的表现,是能看到的实际文件和目录,版本仓库不仅有暂存文件改动的stage,还有历史所有版本的代码或文件。

以下简称 repositoryrepo

所有操作都是在这 3 个区域之间转换,repo 实际在项目根目录的 .git 文件夹里。

注意图中箭头之间的关系:

第二个图简单点,workspace经过 add操作,添加到暂存区stage里面,此时,版本库repository还没有生成一个版本,只是临时保存了改动,必须经过 commit 操作,把改动提交到一个分支(branch),代码才会永久保留在某个版本中。

常用的操作


# 添加文件到 repo 的暂存区

git add filename



# 即添加所有改动过(与repo不一致),或未在repo中的文件

git add *



# -m 表示 --message 参数,每次提交都必须说明这次提交的信息,比如增加了什么特性、fix 了什么bug等

git commit -m "代码提交的信息"

Git 中总有后悔药吃


# 查看当前工作区与仓库的状态:是否有改动未 add 到暂存区,是否未提交等等

git status



# 查看历史提交记录

git log



# 查看 HEAD 对应版本的 filename 文件和工作区的filename文件的区别

git diff HEAD -- filename



# -- 表示不提供参数(这样默认是HEAD指针)



# 在 add 之前想撤销工作区的修改

git checkout -- filename



# 如果已经add到暂存区了,还未commit

# 在切换版本的同时,git reset 也把工作区文件更新了

# --hard 参数表示恢复工作区和暂存区

git reset HEAD filename # 回退 filename 文件为 HEAD 版本

git reset --hard HEAD # 回退所有文件为 HEAD 版本

git reset --hard commit_id # 回退所有文件为 commit_id 版本

# HEAD^ 是上个版本,HEAD^^是上上个版本, HEAD~100是100个版本之前



# 查看某一版本的 commit id,即使它被 git reset 掉了

# git log 只能显示所有提交过的版本信息,reset 掉就没有了

# git reflog 可以看到所有 commit id

git reflog

删除文件

如果在工作区删除了文件,和repo不一致了,这个时候不是git add,而是用git rm从 repo 的暂存区删掉该文件


git rm filename

如果误删了,还没有add到暂存区,依然可以用git checkout来恢复为暂存区的版本


git checkout -- deleted_file

Github 等远程仓库

首先依然要有ssh登录服务,然后在自己的 Gtihub 设置页面 https://github.com/settings/profile SSH中添加你的公钥

公钥生成:


ssh-keygen -t rsa -C "你的邮箱"

复制 .ssh 目录下的 id_rsa.pub 文件的内容

image-20181007223216544

添加远程仓库

之前讲的都是我们本地.git目录的repo,还可以添加远程仓库:git remote add remote_name remote_address


git remote add origin git@github.com:yourName/yourRepository.git

一般origin是远程仓库默认的名字,也可改成别的。

推送给远程仓库

如果是先有本地仓库,后建远程仓库,本地的 HEAD 版本推送给远程仓库:


git push -u origin master # 第一次 push 给远程仓库,添加 -u 参数(相当于是关联),以后则不用加

# master 依然是分支

git push origin master

如果是先在 Github 上创建仓库,后在本地创建


git clone git@github.com:michaelliao/gitskills.git

git clone https://github.com/michaelliao/gitskills.git

# 这个可以在 Github 上复制,有ssh的地址,也有https的地址

# 区别:默认是 git:// 即使用ssh,Github添加公钥后不用输密码,且速度快;而 https 每次都要输密码(github的密码),而且速度相对慢



# 只要是 clone 过来的,自然建立了 origin master 和本地repo的关联,不需要 git push -u 参数

分支管理

分支又是一个很重要的特性


git branch # 查看分支, 带 * 表示当前分支

git branch new_name # 创建新分支

git checkout branch_name # 切换到名为branch_name的分支

git checkout -b branch_name # -b 可直接创建+切换,省去创建分支的命令

git merge another_branch # 合并 another_branch 到当前分支,如冲突则要解决冲突

git branch -d branch_name # 删除分支 branch_name

比如创建dev分支,并从master分支切换到 dev分支


$ git checkout -b dev



$ git branch # 当前分支前面会标一个*号

* dev

master



# 假设我们已经在 dev 分支上做了一些修改

$ git checkout master # 切换回 master 分支

$ git merge dev # 合并 dev (如果是 Fast-forward 模式,,即直接把master指向dev,合并速度回很快)



$ git branch -d dev # 合并完成后,dev 没用就可以放心删除了

切换到 dev 并修改提交

merge dev

删除 dev

解决冲突

当两个分支有矛盾之处 (有修改) 时, 再 merge会产生冲突


git merge feature1 # 当前在 master 下



# 发生冲突

Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容


Git is a distributed version control system.

Git is free software distributed under the GPL.

Git has a mutable index called stage.

Git tracks changes of files.

<<<<<<< HEAD # 这是当前 master 版本的内容

Creating a new branch is quick & simple.

=======

Creating a new branch is quick AND simple. # 这是 feature1 版本的内容

>>>>>>> feature1

必须手动解决冲突后再提交,比如对 master 的那个文件修改,记得删除<<< === >>>那几行

然后提交就好了


git add readme.txt

git commit -m "conflict fixed"

git branch -d feature1 # 可以删除 feature1 分支了



git log --graph # 可以看到分支合并图



* cf810e4 (HEAD -> master) conflict fixed

|\

| * 14096d0 (feature1) AND simple

* | 5dc6824 & simple

|/

* b17d20e branch test

* d46f35e (origin/master) remove test.txt

* b84166e add test.txt

* 519219b git tracks changes

* e43a48b understand how stage works

* 1094adb append GPL

* e475afc add distributed

* eaadf4e wrote a readme file

分支管理策略

合并分支,Git 会尽量用Fast forward模式,但这种模式,删除分支会丢掉分支信息

如果要强制禁用 Fast foward--no-ff),Git 在 merge 的时候生成一个新的 commit,这样从分支历史上就可以看出分支信息


git checkout -b dev # 创建并切换到 dev 分支



# 做一些修改,提交新的 commit ("add merge"),再切换回master



git checkout master



git merge --no-ff -m "merge with no-ff" dev # 合并dev分支,--no-ff 表示禁用 Fast forward

# 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。



git log



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

* e1e9c68 (HEAD -> master) merge with no-ff

|\

| * f52c633 (dev) add merge

|/

* cf810e4 conflict fixed

...



# 只要就保留了 dev 节点,而不是直接将 master 指向 dev 节点

Bug 分支 和 stash(储藏)功能

想要修 bug 了,可以新增一个临时分支来修复,修复完再合并到主分支,然后删除临时分支

比如bug代号101,创建分支issue-101

!但是,假设我们在 dev上面还有一些开发工作没有 commit,因为项目只完成一半,没有办法提交。而修复 Bug 时间又恨紧迫,怎么办?

可以使用stash将工作区的临时改动都“储藏”起来,以后还可能恢复现场继续工作


git stash

# Saved working directory and index state WIP on dev: 6224937 add merge

# HEAD is now at 6224937 add merge



git status

# 发现工作区是干净点 ,没有提示未 add 之类的



# 现在可以放心地创建分支修 bug 了



git checkout master # 假设是要基于 master 分支修复bug



git checkout -b issue-101 # 在 master 基础上创建、切换分支 issue-101





# ... 修复 bug

git commit -m "fix bug 101"



git checkout master

git merge --no-ff -m "merge buf fix 101" issue-101 # 非 fast forward 模式,不让 issue-101 分支在记录中消失

git branch -d issue-101 # 删除bug分支



# 继续回到 dev 分支干活

git checkout dev

git status # 现在还是干净的工作区

git stash list # 查看工作现场

# stash@{0}: WIP on dev: f52c633 add merge



# 方法一:

git stash pop # 恢复最近现场,并删除stash内容

# 方法二:

git stash apply # 应用(恢复)现场,但stash内容还在

git stash apply stash@{0} # 多次 stash 可以先用 git stash list 查看,然后指定其编号

git stash drop # 删除现场

Feature 分支

添加一些新功能,可能是实验性的,不想搞乱主分支,所以每添加一个新功能,最好新建一个 feature 分支

假设新特性代号为 Vulcan


git checkout -b feature-vulcan # 当前还是在dev分支上开发



# ... 开发新功能完毕



git commit -m "add feature vulcan"



git checkout dev # 切回 dev 准备合并



# 突然取消新特性

git branch -d feature-vulcan # 还没有被合并的分支,branch -d 会提示未合并



# branch -D 大写的 D 可以强行删除

git branch -D feature-vulcan

多人协作

当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin


git remote # 查看 remote 的名称

# origin



git remote -v # 不近可以查看 remote 仓库的名称,还能看地址

# origin git@github.com:michaelliao/learngit.git (fetch)

# origin git@github.com:michaelliao/learngit.git (push)



# 上面显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。

推送分支


git push origin master # 推送本地的master分支到 origin 的 master 分支



git push origin dev # 推送本地的dev分支到 origin 的 dev 分支



# master 主分支要时刻与远程同步

# dev 是开发分支,所有成员都在上面工作,也需要与远程同步

# bug 分支只在本地修复 bug,没必要推送远程

# feature 分支是否推送远程,取决于是否与多人合作开发

git pull 和 git fetch 的区别

git fetch:相当于是从远程获取最新版本到本地,不会自动merge, 在实际使用中,git fetch更安全一些


git fetch origin master

git log -p master..origin/master

git merge origin/master

git pull:相当于是从远程获取最新版本并merge到本地


git pull origin master

Rebase

如果多人在同一分支上写作,很容易出现冲突,即使无冲突,后push的不得不先pull,在本地合并才能push成功(因为push时前一个人已经改了远程代码)

每次 merge 之后,再 push,分支会变成这样:


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



* d1be385 (HEAD -> master, origin/master) init hello

* e5e69f1 Merge branch 'dev'

|\

| * 57c53ab (origin/dev, dev) fix env conflict

| |\

| | * 7a5e5dd add env

| * | 7bd91f1 add new env

| |/

* | 12a631b merged bug fix 101

|\ \

| * | 4c805e2 fix bug 101

|/ /

* | e1e9c68 merge with no-ff

|\ \

| |/

| * f52c633 add merge

|/

* cf810e4 conflict fixed

太多分支 merge,Git 的提交历史看上去会很乱

Git 有一种 rebase 操作,可以让提交历史变成干净的直线

git mergegit rebase 做的事其实是有一样的,都是将一个分支的更改并入另一个分支,只是方式不同。

git rebase通过更改本地的历史,把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。

现在在和远程分支同步后,我们对hello.py这个文件做了两次提交。


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

* 582d922 (HEAD -> master) add author

* 8875536 add comment

* d1be385 (origin/master) init hello # 在 init hello 之后,添加了 comment 和 author

* e5e69f1 Merge branch 'dev'

|\

| * 57c53ab (origin/dev, dev) fix env conflict

| |\

| | * 7a5e5dd add env

| * | 7bd91f1 add new env

...

注意(HEAD -> master)(origin/master)标识出当前分支的HEAD和远程origin的位置分别,本地分支比远程分支快两个提交。

现在尝试推送分支,发现有人比我们先推送到远程了,所以要先git pull 一下合并那个人的更改。


$ git status #加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。

On branch master

Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)



nothing to commit, working tree clean

查看 log


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

* e0ea545 (HEAD -> master) Merge branch 'master' of github.com:michaelliao/learngit

|\

| * f005ed4 (origin/master) set exit=1 # 另一个人在另一分支上的更改

* | 582d922 add author # 我们的更改

* | 8875536 add comment # 我们的更改

|/

* d1be385 init hello # 原来的 origin master

...

这样也可以 push 到远程,但是很不直观


git rebase



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

* 7e61ed4 (HEAD -> master) add author # 我们的修改

* 3611cfe add comment # 我们的修改

* f005ed4 (origin/master) set exit=1 # 另一个人的修改(因为他先push,所以在此基础上修改)

* d1be385 init hello # 原来的 origin master

...

Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello,而是基于f005ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。

最后,通过push操作把本地分支推送到远程:


git push origin master

fork 和 pull request

一般团队合作一个项目,

pull request 的意思就是,请求别人 pull 你的更改(即拿到你的更改并且合并)。

  1. fork别人的仓库,相当于拷贝一份到你的GitHub上,一般都不会有人让你直接动master
  1. clone 到本地分支,做一些bug fix
  1. 发起pull request给原仓库,让它看到你的修改
  1. 原仓库 review 这个 bug,如果是正确的话,就会 merge 到他自己的项目中

git remote -v



# fork 之后一般称主项目为 upstream (上游),而自己 fork 的版本还是 origin

git remote add upstream https://github.com/XXX.git # 注意添加的是上游仓库地址,而不是自己的远程仓库地址

# 当然自己的远程仓库地址也需要添加,但一般都是 git clone 下来自动就有了

我们 pull request 的时候,可能会提示落后几个提交,这时要更新到最新的上游代码


# 从上游仓库fetch分支和提交点,提交给本地master,并会被存储在一个本地分支 upstream/master

git fetch upstream



git checkout master # 切换到本地主分支(如果不在)



git merge upstream/master # 合并上游分支



git push origin master # 更新到自己的远程 fork 项目

然后到该项目的 Github 主页

然后添加 pull request 的信息:比如请求对方合并的理由等等

最后等待对方合并我们的记录,close 这个 pull request 即可。

Git 自定义

.gitignore 忽略特殊文件

在项目根目录下,添加 .gitignore文件,记得add到repo


# Windows:

Thumbs.db

ehthumbs.db

Desktop.ini



# Python:

*.py[cod]

*.so

*.egg

*.egg-info

dist

build



# My configurations:

db.ini

deploy_key_rsa

配置别名


git config --global alias.st status

git config --global alias.co checkout

git config --global alias.ci commit

git config --global alias.br branch

git config --global alias.unstage 'reset HEAD'

git config --global alias.last 'log -1'



# 美化 log 显示

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/config文件中


[core]

repositoryformatversion = 0

filemode = true

bare = false

logallrefupdates = true

ignorecase = true

precomposeunicode = true

[remote "origin"]

url = git@github.com:michaelliao/learngit.git

fetch = +refs/heads/*:refs/remotes/origin/*

[branch "master"]

remote = origin

merge = refs/heads/master

[alias]

last = log -1

全局配置文件在 .gitconfig


[alias]

co = checkout

ci = commit

br = branch

st = status

[user]

name = Your Name

email = your@email.com

参考资料

廖雪峰 Git 教程