git 教程、常用命令

git 教程:https://www.liaoxuefeng.com/wiki/896043488029600/897013573512192

Git官网、pro git、Git manual:https://git-scm.com/ 
小猪的Git使用总结:https://blog.csdn.net/coder_pig/article/details/54346867
菜鸟教程:git 教程:https://www.runoob.com/git/git-tutorial.html
图解git原理与日常实用指南:https://juejin.cn/post/6844903782451511303
Git 常用命令速查表(图文+表格):https://www.cnblogs.com/kenshinobiy/p/4543976.html
GitHub 秘籍:https://github.com/tiimgreen/github-cheat-sheet/blob/master/README.zh-cn.md

1、git 简介

git 的诞生

Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了。Linus 虽然创建了Linux,但Linux的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为Linux编写代码,那Linux的代码是如何管理的呢?事实是,在2002年以前,世界各地的志愿者把源代码文件通过diff的方式发给Linus,然后由Linus本人通过手工方式合并代码!你也许会想,为什么Linus不把Linux代码放到版本控制系统里呢?不是有CVS、SVN这些免费的版本控制系统吗?因为Linus坚定地反对CVS和SVN,这些集中式的版本控制系统不但速度慢,而且必须联网才能使用。有一些商用的版本控制系统,虽然比CVS、SVN好用,但那是付费的,和Linux的开源精神不符。不过,到了2002年,Linux系统已经发展了十年了,代码库之大让Linus很难继续通过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满,于是Linus选择了一个商业的版本控制系统BitKeeper,BitKeeper的东家BitMover公司出于人道主义精神,授权Linux社区免费使用这个版本控制系统。安定团结的大好局面在2005年就被打破了,原因是Linux社区牛人聚集,不免沾染了一些梁山好汉的江湖习气。开发Samba的Andrew试图破解BitKeeper的协议(这么干的其实也不只他一个),被 BitMover 公司发现了(监控工作做得不错!),于是BitMover公司怒了,要收回Linux社区的免费使用权。Linus可以向BitMover公司道个歉,保证以后严格管教弟兄们,不过这是不可能的。实际情况是这样的:Linus花了两周时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!牛就是这么定义的,大家可以体会一下。然后 Git 迅速成为最流行的分布式版本控制系统,尤其是2008年,GitHub网站上线了,它为开源项目免费提供Git存储,无数开源项目开始迁移至GitHub。

集中式 版本控制:集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。

git 教程、常用命令_第1张图片

分布式版本控制:没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。

git 教程、常用命令_第2张图片

图 2

git 教程、常用命令_第3张图片

Git 和 SVN 思想最大的差别有四个:

  • 去中心化
  • 直接记录快照,而非差异
  • 不一样的分支概念
  • 三个文件状态

git 是什么

Git 是分布式的版本控制系统,客户端不只是提取最新版本的快照,而且将整个代码仓库镜像复制下来。如果任何协同工作用的服务器发生故障了,也可以用任何一个代码仓库来恢复。而且在协作服务器宕机期间,你也可以提交代码到本地仓库,当协作服务器正常工作后,你再将本地仓库同步到远程仓库。SVN是集中化版本控制系统,集中化版本控制系统如果中央服务器宕机则会影响数据和协同开发。

为什么要用 git

能够对文件版本控制和多人协作开发
拥有强大的分支特性,所以能够灵活地以不同的工作流协同开发
分布式版本控制系统,即使协作服务器宕机,也能继续提交代码或文件到本地仓库,当协作服务器恢复正常工作时,再将本地仓库同步到远程仓库。
当团队中某个成员完成某个功能时,通过pull request操作来通知其他团队成员,其他团队成员能够review code后再合并代码。

2、使用 git

名词解释

  • Workspace:工作区,就是 .git文件夹所在的目录。
  • .git:工作区中有一个隐藏目录 .git,这个是 Git 的版本库,你的所有版本信息都存在这里。
  • Index / Stage:索引/暂存区。存放在 ".git目录下" 下的 index 文件(.git/index)中,可以把 "index/Stage" 理解成 "缓存区、临时存放区"通俗理解:当你修改一个doc文件时,会自动产生一个临时文件用来缓存,这个自动产生的临时文件就相当于 "index/Stage",如果系统意外关机,通过临时文件可以恢复doc文件,同理,git 的 "index/Stage"就相当于临时缓存文件,只不过需要手动执行 git add 生成,保存 doc 文件,就相当于执行 git commit -a
  • Repository:仓库区(或本地仓库),master:默认开发分支
  • Remote:远程仓库。origin:默认远程版本库

git  流程

图中左侧为工作区,右侧为版本库。在版本库中标记为 "index" 的区域是 "暂存区(stage/index)或者 缓存区",标记为 "master" 的是 master 分支所代表的目录树。

git 教程、常用命令_第4张图片

图中我们可以看出此时 "HEAD" 实际是指向 master 分支的一个"游标"。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。

图中的 objects 标识的区域为 Git 的对象库,实际位于 ".git/objects" 目录下,里面包含了创建的各种对象及内容。

当对工作区修改(或新增)的文件执行 "git add" 命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。

当执行提交操作(git commit)时,暂存区的目录树写到版本库(对象库)中,master 分支会做相应的更新。即 master 指向的目录树就是提交时暂存区的目录树。

当执行 "git reset HEAD" 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。

当执行 "git rm --cached " 命令时,会直接从暂存区删除文件,工作区则不做出改变。

当执行 "git checkout ." 或者 "git checkout -- " 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。

当执行 "git checkout HEAD ." 或者 "git checkout HEAD " 命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

图 1

git 教程、常用命令_第5张图片

图 2

git 教程、常用命令_第6张图片

图 3

git 教程、常用命令_第7张图片

图 4

git 教程、常用命令_第8张图片

流程简述:

按大类划分,分为两种状态:Tracked(已跟踪)和Untracked(未跟踪), 
依据是:该文件是否已加入版本控制

假设某个项目已加入版本控制系统

  • 1.新建一个文件,该文件处于 Untracked 状态;
  • 2.通过 git add 命令添加到缓存区,此时文件处于 Tracked 状态又或者说 
    此时这个文件已经被版本控制系统所跟踪,而且他处于Staged(暂存)状态;
  • 3.通过git commit命令把暂存区的文件提交提交到本地仓库,此时文件 
    处于Unmodified(未修改)状态;
  • 4.此时如果去编辑这个文件,文件又会变成Modified(修改)状态;

git 命令帮助:git --help

用法: git [-v | --version] [-h | --help] [-C ] [-c =]
           [--exec-path[=]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=] [--work-tree=] [--namespace=]
           [--config-env==] []

常用 git 命令。查看对应命令帮助。示例:git clone --help
开始一个新的工作空间 (更多帮助:git help tutorial)
   clone     克隆一个仓库到新的目录
   init      创建一个空的Git存储库,或重新初始化一个现有的Git存储库

处理当前的变化 (更多帮助: git help everyday)
   add       将文件内容添加到 index(索引)
   mv        移动或者重命名 "文件, 目录, 符号链接"
   restore   Restore working tree files
   rm        同时从工作区和暂存区(缓存区) 删除文件

查看提交历史和仓库状态 (参看: git help revisions)
   bisect    使用二进制搜索来查找引入bug的提交
   diff      显示两次提交之间的变化
   grep      打印匹配到的行
   log       显示提交日志
   show      展示各种类型的对象
   status    显示状态

"成长、标记、调整" 分支
   branch    列出、创建、删除 分支
   commit    提交到本地仓库
   merge     将两个或多个分支合并到本地当前分支
   rebase    Reapply commits on top of another base tip
   reset     将当前HEAD重置为指定状态
   switch    切换分支
   tag       创建、列出、删除或验证使用GPG签名的tag

合作 (参看: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help ' or 'git help '
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

git 命令速查手册

everything-is-local

Git 常用6个命令:git clonegit pushgit add 、git commitgit checkoutgit pull

git init    初始化仓库。
git clone    拷贝一份远程仓库。
git add .   添加文件到暂存区。
git commit  将暂存区内容添加到仓库中。

git add        添加文件到暂存区
git status    查看仓库当前的状态,显示有变更的文件。
git diff    比较文件的不同,即暂存区和工作区的差异。
git commit    提交暂存区到本地仓库。
git commit -am ""    直接提交全部修改,相当于 add 和 commit 一起执行了。
git reset    回退版本。reset 是替换整个目录树,多余的文件将被删除。
git checkout
git rm        将文件从暂存区和工作区中删除。
            使用 --cached 只从暂存区中删除。
            使用 –rf 可删除指定目录下的所有文件和子目录。
git mv        在工作区和暂存区中进行移动或重命名。若 不为一个目录名,则执行重命名。
            如果为一个目录名,则执行移动。
git checkout    分支切换。对多余的文件保留不做任何处理。
git switch      更清晰地切换分支。(Git 2.23 版本引入)    
git restore     恢复或撤销文件的更改。(Git 2.23 版本引入)
git log            查看历史提交记录
git blame     以列表形式查看指定文件的历史修改记录
git remote      远程仓库操作
git fetch      从远程获取最新版本到本地,不会自动merge
git merge     从指定的commit(s)合并到当前分支,用来合并两个分支;
git pull      下载远程代码,并合并到本地
git push      上传并合并到远程代码
git pull 相当于 git fetch + git merge

git diff:查看工作区与暂存区的不同。
git diff –cached []:查看暂存区与指定提交版本的不同,版本可缺省(为HEAD)。
git diff :查看工作区与指定提交版本的不同。
git diff ..:查看2个指定提交版本的不同,其中任一可缺省(为HEAD)。
git diff ...:查看2个不同分支指定提交版本的不同,其中任一可缺省(为HEAD),该命令相当于git diff $(git-merge-base A B) B。

git 教程、常用命令_第9张图片

git init                                                  # 初始化本地git仓库(创建新仓库)
git config --global user.name "xxx"                       # 配置用户名
git config --global user.email "[email protected]"              # 配置邮件
git config --global color.ui true                         # git status等命令自动着色
git config --global color.status auto
git config --global color.diff auto
git config --global color.branch auto
git config --global color.interactive auto
git config --global --unset http.proxy                    # remove  proxy configuration on git
git clone git+ssh://[email protected]/VT.git             # clone远程仓库
git status                                                # 查看当前版本状态(是否修改)
git add xyz                                               # 添加xyz文件至index
git add .                                                 # 增加当前子目录下所有更改过的文件至index
git commit -m 'xxx'                                       # 提交
git commit --amend -m 'xxx'                               # 合并上一次提交(用于反复修改)
git commit -am 'xxx'                                      # 将add和commit合为一步
git rm xxx                                                # 删除index中的文件
git rm -r *                                               # 递归删除
git log                                                   # 显示提交日志
git log -1                                                # 显示1行日志 -n为n行
git log -5
git log --stat                                            # 显示提交日志及相关变动文件
git log -p -m
git show dfb02e6e4f2f7b573337763e5c0013802e392818         # 显示某个提交的详细内容
git show dfb02                                            # 可只用commitid的前几位
git show HEAD                                             # 显示HEAD提交日志
git show HEAD^                                            # 显示HEAD的父(上一个版本)的提交日志 ^^为上两个版本 ^5为上5个版本
git tag                                                   # 显示已存在的tag
git tag -a v2.0 -m 'xxx'                                  # 增加v2.0的tag
git show v2.0                                             # 显示v2.0的日志及详细内容
git log v2.0                                              # 显示v2.0的日志
git diff                                                  # 显示所有未添加至index的变更
git diff --cached                                         # 显示所有已添加index但还未commit的变更
git diff HEAD^                                            # 比较与上一个版本的差异
git diff HEAD -- ./lib                                    # 比较与HEAD版本lib目录的差异
git diff origin/master..master                            # 比较远程分支master上有本地分支master上没有的
git diff origin/master..master --stat                     # 只显示差异的文件,不显示具体内容
git remote add origin git+ssh://[email protected]/VT.git # 增加远程定义(用于push/pull/fetch)
git branch                                                # 显示本地分支
git branch --contains 50089                               # 显示包含提交50089的分支
git branch -a                                             # 显示所有分支
git branch -r                                             # 显示所有原创分支
git branch --merged                                       # 显示所有已合并到当前分支的分支
git branch --no-merged                                    # 显示所有未合并到当前分支的分支
git branch -m master master_copy                          # 本地分支改名
git checkout -b master_copy                               # 从当前分支创建新分支master_copy并检出
git checkout -b master master_copy                        # 上面的完整版
git checkout features/performance                         # 检出已存在的features/performance分支
git checkout --track hotfixes/BJVEP933                    # 检出远程分支hotfixes/BJVEP933并创建本地跟踪分支
git checkout v2.0                                         # 检出版本v2.0
git checkout -b devel origin/develop                      # 从远程分支develop创建新本地分支devel并检出
git checkout -- README                                    # 检出head版本的README文件(可用于修改错误回退)
git merge origin/master                                   # 合并远程master分支至当前分支
git cherry-pick ff44785404a8e                             # 合并提交ff44785404a8e的修改
git push origin master                                    # 将当前分支push到远程master分支
git push origin :hotfixes/BJVEP933                        # 删除远程仓库的hotfixes/BJVEP933分支
git push --tags                                           # 把所有tag推送到远程仓库
git fetch                                                 # 获取所有远程分支(不更新本地分支,另需merge)
git fetch --prune                                         # 获取所有原创分支并清除服务器上已删掉的分支
git pull origin master                                    # 获取远程分支master并merge到当前分支
git mv README README2                                     # 重命名文件README为README2
git reset --hard HEAD                                     # 将当前版本重置为HEAD(通常用于merge失败回退)
git rebase
git branch -d hotfixes/BJVEP933                           # 删除分支hotfixes/BJVEP933(本分支修改已合并到其他分支)
git branch -D hotfixes/BJVEP933                           # 强制删除分支hotfixes/BJVEP933
git ls-files                                              # 列出git index包含的文件
git show-branch                                           # 图示当前分支历史
git show-branch --all                                     # 图示所有分支历史
git whatchanged                                           # 显示提交历史对应的文件修改
git revert dfb02e6e4f2f7b573337763e5c0013802e392818       # 撤销提交dfb02e6e4f2f7b573337763e5c0013802e392818
git ls-tree HEAD                                          # 内部命令:显示某个git对象
git rev-parse v2.0                                        # 内部命令:显示某个ref对于的SHA1 HASH
git reflog                                                # 显示所有提交,包括孤立节点
git show HEAD@{5}
git show master@{yesterday}                               # 显示master分支昨天的状态
git log --pretty=format:'%h %s' --graph                   # 图示提交日志
git show HEAD~3
git show -s --pretty=raw 2be7fcb476
git stash                                                 # 暂存当前修改,将所有至为HEAD状态
git stash list                                            # 查看所有暂存
git stash show -p stash@{0}                               # 参考第一次暂存
git stash apply stash@{0}                                 # 应用第一次暂存
git grep "delete from"                                    # 文件中搜索文本“delete from”
git grep -e '#define' --and -e SORT_DIRENT
git gc
git fsck

git 详解及实用指南之一 (本地操作):https://segmentfault.com/a/1190000016720411
git 详解及实用指南之二 (远程操作):https://segmentfault.com/a/1190000016737370
git 详解及实用指南之三 (分支管理):https://segmentfault.com/a/1190000016755475
git 详解及实用指南之四 (标签管理):https://segmentfault.com/a/1190000016790744

1. 配置 git

每个仓库的Git配置文件都放在.git/config文件中:

$ cat .git/config 
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[remote "origin"]
    url = [email protected]:michaelliao/learngit.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master
[alias]
    last = log -1

配置文件

  • 加上 --global 是针对当前用户起作用的,配置后在用户主目录下(全局配置)
  • 如果不加,那只针对当前的仓库起作用。配置后在项目目录下(项目配置)。

别名就在[alias]后面,要删除别名,直接把对应的行删掉即可。

当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中:

$ cat .gitconfig
[alias]
    co = checkout
    ci = commit
    br = branch
    st = status
[user]
    name = Your Name
    email = [email protected]

配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。

git config --list                显示当前的Git配置
git config -e [--global]    编辑Git配置文件

# 设置提交代码时的用户信息
git config --global user.name "[name]"    
git config --global user.email "[email address]"

git archive    生成一个可供发布的压缩包
git config --global core.autocrlf false    设置换行符为LF
git config --global core.safecrlf true     拒绝提交包含混合换行符的文件

安装完成后,还需要最后一步设置,在命令行输入:

$ git config --global user.name "Your Name"
$ git config --global user.email "[email protected]"

因为Git是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和Email地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都是善良无知的群众,其次,真的有冒充的也是有办法可查的。

注意git config命令的--global参数,用了这个参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。

配置 别名

有没有经常敲错命令?比如git statusstatus这个单词真心不好记。如果敲 git st 就表示git status那就简单多了,当然这种偷懒的办法我们是极力赞成的。我们只需要敲一行命令,告诉Git,以后 st 就表示 status:$ git config --global alias.st status

好了,现在敲 git st 看看效果。

当然还有别的命令可以简写,很多人都用co表示checkoutci表示commitbr 表示branch

$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch

以后提交就可以简写成:$ git ci -m "bala bala bala..."

--global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。

配置一个unstage别名:$ git config --global alias.unstage 'reset HEAD' ,当你敲入命令 git unstage test.py 实际上Git执行的是 git reset HEAD test.py

配置一个git last,让其显示最后一次提交信息:git config --global alias.last 'log -1'

这样,用git last就能显示最近一次的提交:git last

甚至还有人丧心病狂地把lg配置成了: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 lg 的效果:

git-lg

为什么不早点告诉我?别激动,咱不是为了多记几个英文单词嘛!

忽略 特殊文件

有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files ...,有强迫症的童鞋心里肯定不爽。好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。

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

忽略文件的原则是:

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

举个例子:

假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini文件,因此你需要忽略Windows自动生成的垃圾文件,然后,继续忽略Python编译产生的.pyc.pyodist等文件或目录,加上你自己定义的文件,最终得到一个完整的.gitignore文件,内容如下:

# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini

# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build

# My configurations:
db.ini
deploy_key_rsa

最后一步就是把 .gitignore 也提交到Git,就完成了!当然检验.gitignore的标准是git status命令是不是说working directory clean。有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore忽略了:

$ git add App.class
The following paths are ignored by one of your .gitignore files:
App.class
Use -f if you really want to add them.

如果你确实想添加该文件,可以用-f强制添加到Git:$ git add -f App.class,或者可能是.gitignore写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore命令检查:$ git check-ignore -v App.class

小结:

  • 忽略某些文件时,需要编写.gitignore;
  • .gitignore文件本身要放到版本库里,并且可以对.gitignore做版本管理

2. 新建 版本库 ( 代码库 )

git init                            初始化当前目录为Git代码库,会在当前目录下创建 .git 文件夹
git init [project-name]    新建一个目录,并将其初始化为Git代码库
git clone [url]                 下载一个项目和它的整个代码历史

Git 的版本库里存了很多东西,其中最重要的就是称为 "stage/index 的暂存区",还有 Git 自动创建的第一个分支master,以及指向 master 的一个指针叫 HEAD

git-repo

把文件添加到 "Git版本库的本地仓库" 是分两步执行的:

  • 第一步:用 git add 把文件添加进去,实际上就是把文件修改添加到暂存区;
  • 第二步:用 git commit 提交更改,实际上就是把暂存区的所有内容提交到当前分支。
  • 可以简单理解为:需要提交的文件修改通通放到暂存区,然后一次性提交暂存区的所有修改。

俗话说,实践出真知。

创建 readme.txt 和 LICENSE 文件,使用 git add readme.txt LICENSE 后,用 git status 查看:

git-stage

所以,git add 命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是 "干净" 的。

执行 git status 查看,现在版本库变成了这样,暂存区就没有任何内容了:

git-stage-after-commit

暂存区 是 Git 非常重要的概念,弄明白了暂存区,就弄明白了Git的很多操作到底干了什么。

3. 增加、删除、移动、重命名

git add 就是告诉 Git,把文件添加到 缓存区:

git 教程、常用命令_第10张图片

git add [file1] [file2] ...    添加指定文件到暂存区
git add [dir]        添加指定目录到暂存区,包括子目录
git add .             添加当前目录的所有文件到暂存区
git add -p     # -p,--patch  以交互方式,对于同一个文件的多处变化,可以实现分次提交
git rm [file1] [file2] ...    删除工作区文件,并且将这次删除放入暂存区
git rm --cached [file]    停止追踪指定文件,但该文件会保留在工作区
git mv [file-original] [file-renamed]    改名文件,并且将这个改名放入暂存区

命令 git rm 用于删除一个文件。如果一个文件已经被提交到版本库,那么永远不用担心误删

git mv 命令用于移动或重命名一个文件、目录。
git mv  
git mv  
其实,运行 git mv 就相当于运行了下面三条命令:
mv README.md README
git rm README.md
git add README

git status 查看仓库的当前状态,随时掌握工作区的状态。
git diff 顾名思义就是查看 difference。示例:git diff test.txt 查看更改内容
git log  查看提交的历史记录
git log --pretty=oneline

4. 提交 修改

git commit -m [message]    提交暂存区到仓库区
git commit [file1] [file2] ... -m [message]    提交暂存区的指定文件到仓库区
git commit -a    提交工作区自上次commit之后的变化,直接到仓库区
git commit -v    提交时显示所有diff信息

使用一次新的commit,替代上一次提交
如果代码没有任何新变化,则用来改写上一次commit的提交信息
git commit --amend -m [message]
重做上一次commit,并包括指定文件的新变化
git commit --amend [file1] [file2] ...    

为什么 Git 比其他版本控制系统设计得优秀,因为 Git 跟踪并管理的是修改,而非文件。什么是修改?比如:新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。

示例:验证 "管理的是修改,而不是文件"

对 readme.txt 随便修改下,例如,随便 增加、删除一行

提交:git add readme.txt ,查看状态:git status

然后再修改 readme.txt,并再次提交:git commit -m "修改",提交后再看看状态:git status

怎么第二次的修改没有被提交?别激动,我们回顾一下操作过程:

第一次修改 -> git add -> 第二次修改 -> git commit

前面讲了,Git 管理的是修改,当用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。

提交后用 git diff HEAD -- readme.txt 可以查看工作区版本库里面最新版本的区别。

那怎么提交第二次修改呢?你可以继续 git add git commit,也可以别着急提交第一次修改,先 git add第二次修改,再git commit,就相当于把两次修改合并后一块提交了:第一次修改 -> git add -> 第二次修改 -> git add -> git commit ,现在就把第二次修改提交了。

5. 分  支

Git 鼓励大量使用分支:

  • 查看分支:
            git branch     列出所有本地分支
            git branch -r  列出所有远程分支
            git branch -a  列出所有本地分支和远程分支   
  • 新建一个分支,但依然停留在当前分支:git branch
  • 新建一个分支,指向指定commit:git branch [branch] [commit]
  • 切换到指定分支,并更新工作区:git checkout   
  • 切换到上一个分支:git checkout -
  • 创建+切换分支,新建一个分支,并切换到该分支:git checkout -b
  • 合并指定的分支到当前分支:git merge
  • 选择一个commit,合并进当前分支:git cherry-pick [commit]
  • 删除分支:git branch -d
  • 删除远程分支:
        $ git push origin --delete [branch-name]
        $ git branch -dr [remote/branch]
  • 新建一个分支与指定的远程分支建立追踪关系:git branch --track [branch] [remote-branch]
  • 在现有分支与指定的远程分支之间建立追踪关系:git branch --set-upstream [branch] [remote-branch]

Git 的分支本质是一个指向提交快照的指针,是从某个提交快照往回看的历史。当创建/切换分支的时候,只是变换了指针指向。

分支 就像 科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!

learn-branches

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

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

"创建、合并" 分支

在版本回退里,已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在 Git 里,这个分支叫主分支,即 master 分支。HEAD 严格来说不是指向提交,而是指向 mastermaster 才是指向提交的,所以,HEAD 指向的就是 当前分支。一开始的时候,master 分支是一条线,Git 用 master 指向最新的提交,再用 HEAD 指向 master,就能确定当前分支,以及当前分支的提交点:

git-br-initial

每次提交时master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长:

video: http://liaoxuefeng.gitee.io/git-resources/master-branch-forward.mp4

git 教程、常用命令_第11张图片

当创建新的分支,例如 dev 时,Git 新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

git-br-create

可以看到Git 创建一个分支很快,因为除了增加一个 dev 指针,改改 HEAD的指向,工作区的文件都没有任何变化!不过,从现在开始,对工作区的修改和提交就是针对 dev分支了,比如新提交一次后,dev 指针往前移动一步,而master指针不变:

git-br-dev-fd

假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:

git-br-ff-merge

所以Git合并分支也很快!就改改指针,工作区内容也不变!合并完分支后,甚至可以删除 dev 分支。删除 dev分支就是把 dev 指针给删掉,删掉后,我们就剩下了一条 master 分支:

git-br-rm

真是太神奇了,你看得出来有些提交是通过分支完成的吗?

video: http://liaoxuefeng.gitee.io/git-resources/master-and-dev-ff.mp4

git 教程、常用命令_第12张图片

下面开始实战。

创建dev分支,然后切换到dev分支

    git checkout -b dev
等价于
    git branch dev
    git checkout dev

用 git branch 命令查看当前分支:

$ git branch    命令会列出所有分支,当前分支前面会标一个*号。
* dev
  master

然后,我们就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行。然后提交:

$ git add readme.txt 
$ git commit -m "branch test"

现在 dev 分支的工作完成,我们就可以切换回master分支:git checkout master

切换回 master 分支后,再查看一个 readme.txt 文件,刚才添加的内容不见了!因为那个提交是在dev 分支上,而 master 分支此刻的提交点并没有变:

git-br-on-master

现在,我们把 dev 分支的工作成果合并到master分支上:git merge dev

合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。

注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward,后面会讲其他方式的合并。

合并完成后,就可以放心地删除dev分支了:git branch -d dev

删除后,查看 branch,就只剩下master分支了:

$ git branch
* master

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

解决 冲突

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

准备新的feature1分支,继续我们的新分支开发:$ git checkout -b feature1

修改 readme.txt 在最后一行随便添加些内容,在feature1分支上提交:

$ git add readme.txt
$ git commit -m "add"

切换到 master 分支:$ git checkout master

Git 会自动提示我们当前 master 分支比远程的master分支要超前1个提交。在master分支上把readme.txt 文件的最后一行随便修改,然后提交:

$ git add readme.txt 
$ git commit -m "add"

现在,master分支和feature1分支各自都分别有新的提交,变成了这样:

git-br-feature1

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:$ git merge feature1

果然冲突了!Git 告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:

我们可以直接查看readme.txt的内容:

        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
        Creating a new branch is quick & simple.
        =======
        Creating a new branch is quick AND simple.
        >>>>>>> feature1

Git用 <<<<<<<=======>>>>>>>标记出不同分支的内容,修改冲突的部分后保存,再提交:

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

现在,master分支和feature1分支变成了下图所示:

git-br-conflict-merged

git log也可以看到分支的合并情况:$ git log --graph --pretty=oneline --abbrev-commit

        *   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

最后,删除feature1分支:$ git branch -d feature1

工作完成。

小结:当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。用 git log --graph 命令可以看到分支合并图。

分支 合并:merge

含义:从目标 commit 和当前 commit (即 HEAD 所指向的 commit)分叉的位置起,把目标 commit 的路径上的所有 commit 的内容一并应用到当前 commit,然后自动生成一个新的 commit。

当执行git merge branch1操作,Git 会把 5 和 6 这两个 commit 的内容一并应用到 4 上,然后生成一个新的提交 7 。

merge 的特殊情况:

  • (1)merge冲突:你的两个分支改了相同的内容,Git 不知道应该以哪个为准。如果在 merge 的时候发生了这种情况,Git 就会把问题交给你来决定。具体地,它会告诉你 merge 失败,以及失败的原因;这时候你只需要手动解决掉冲突并重新add、commit(改动不同文件或同一文件的不同行都不会产生冲突);或者使用git merge --abort放弃解决冲突,取消merge
  • (2)HEAD 领先于目标 commit:merge是一个空操作,此时merge不会有任何反应。

    git 教程、常用命令_第13张图片

  • (3)HEAD 落后于 目标 commit且不存在分支(fast-forward):

    git会直接把HEAD与其指向的branch(如果有的话)一起移动到目标commit。

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样从分支历史上就可以看出分支信息。

下面实战一下 --no-ff 方式的 git merge

首先,仍然创建并切换dev分支:$ git checkout -b dev

修改 readme.txt文件,并提交一个新的 commit:

$ git add readme.txt 
$ git commit -m "add merge"

现在,我们切换回master:$ git checkout master

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

因为本次合并要创建一个新的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
        ...

可以看到,不使用 Fast forward模式,merge 后就像这样:

git-no-ff-mode

实际开发时 分支合并

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

首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;

你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。所以,团队合作的分支看起来就像这样:

git-br-policy

小结:Git 分支十分强大,在团队开发中应该充分应用。合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

临时存放工作目录的改动

stash 指令可以帮你把工作目录的内容全部放在你本地的一个独立的地方,它不会被提交,也不会被删除,你把东西放起来之后就可以去做你的临时工作了,做完以后再来取走,就可以继续之前手头的事了。
操作步骤:
(1)git stash可以加上save参数后面带备注信息(git stash save '备注信息'
(2)此时工作目录已经清空,可以切换到其他分支干其他事情了
(3)git stash pop弹出第一个stash(该stash从历史stash中移除);或者使用git stash apply达到相同的效果(该stash仍存在stash list中),同时可以使用git stash list查看stash历史记录并在apply后面加上指定的stash返回到该stash。
注意:没有被track的文件会被git忽略而不被stash,如果想一起stash,加上-u参数。

Bug 分支

软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。

当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交:git status ,并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?幸好,Git 还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:$ git stash,现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。

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

$ git checkout master
$ git checkout -b issue-101

现在修复bug,需要把“Git is free software ...”改为“Git is a free software ...”,然后提交:

$ git add readme.txt 
$ git commit -m "fix bug 101"

修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:

$ git checkout master
$ git merge --no-ff -m "merged bug fix 101" issue-101

太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev分支干活了!

$ git checkout dev
$ git status

工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:$ git stash list

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:

  • 方法 1:用git stash apply恢复,但是恢复后stash内容并不删除,需要用git stash drop来删除;
  • 方法 2:用git stash pop,恢复的同时把stash内容也删了:$ git stash pop,再用 git stash list 查看,就看不到任何stash内容了

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

小结:修复bug时,会通过创建新的bug分支进行修复,然后合并,最后删除;当手头工作没有完成时,先把工作现场git stash一下,然后去修复bug,修复后,再git stash pop,回到工作现场。

Feature 分支

软件开发中,总有无穷无尽的新的功能要不断添加进来。添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。

假设你现在接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。

于是准备开发:$ git checkout -b feature-vulcan

5分钟后,开发完毕:

$ git add vulcan.py
$ git status
$ git commit -m "add feature vulcan"

切回dev,准备合并:$ git checkout dev

一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。但是!就在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:$ git branch -d feature-vulcan

销毁失败。Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。。

现在我们强行删除:$ git branch -D feature-vulcan,终于删除成功!

小结:开发一个新feature,最好新建一个分支;如果要丢弃一个没有被合并过的分支,可以通过git branch -D 强行删除。

克隆远程分支:git clone

多人协作,需要从远程仓库克隆,克隆时Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。查看远程库的信息:git remote 或者 git remote -v 显示更详细的信息。可以看到拉取和推送的origin的地址。如果没有推送权限,就看不到push的地址。

git clone 用于克隆一个远程仓库到本地。它会复制整个仓库的历史记录、分支和代码到本地机器上。通常在开始一个新项目或者从头开始参与一个已有项目时会使用 git clone 命令。

拉取代码并合并到当前分支:git pull

git pull 命令用于从远程仓库拉取代码并合并到当前分支。它的作用相当于执行了 git fetch 和 git merge 两个命令的组合。当你执行 git pull 命令时,Git 会自动下载最新的代码变更,并将其合并到当前分支中。

git fetch:用于从远程仓库获取最新的代码变更,但不会自动将其合并到当前分支。它只是将远程分支的更新下载到本地,并更新对应的远程跟踪分支。这样你可以查看远程分支的变更,然后决定是否将更新合并到当前分支。

这些命令都需要与远程仓库进行交互

  • git pull:从远程仓库拉取代码并合并到当前分支。
  • git clone:克隆远程仓库到本地。
  • git fetch:从远程仓库获取最新的代码变更,但不会自动合并到当前分支。

推送分支:git push

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

        格式:git push 远程分支名 本地分支名
        示例:git push origin master # 推送本地master分支到远程origin分支的master
        示例:git push origin dev  # 推送本地dev分支到远程origin分支的master

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

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

总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!

示例:多人协作

多人协作时,大家都会往 master 和 dev 分支上推送各自的修改。现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:git clone [email protected]:michaelliao/learngit.git ,

当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支。不信可以用git branch命令看下。如果你的小伙伴要在dev分支上开发,就必须创建远程origindev分支到本地,于是他用这个命令创建本地dev分支:git checkout -b dev origin/dev

现在他就可以在dev上继续修改,然后,时不时地把dev分支push到远程:

git add env.txt
git commit -m "add env"
git push origin dev

你的小伙伴已经向origin/dev分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:

git add env.txt
git commit -m "add new env"
git push 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 ,这回推送成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:

        git commit -m "fix env conflict"
        git push origin dev

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

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

如果 git pull 提示 no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令 git branch --set-upstream-to origin/ ,这就是多人协作的工作模式,一旦熟悉了,就非常简单。

  • 查看远程库信息:git remote -v;
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • 从本地推送分支,使用 git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
  • 在本地创建和远程分支对应的分支,使用 git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • 建立本地分支和远程分支的关联,使用 git branch --set-upstream branch-name origin/branch-name;
  • 从远程抓取分支,使用 git pull,如果有冲突,要先处理冲突。

团队工作基本模型:

  • 你写了代码,commit,push 到远程仓库,
  • 然后他 pull 到他的本地;他再写代码,commit, push 到远程仓库,
  • 然后你再 pull 到你的本地。你来我往,配合得不亦乐乎。(但是有时候push会失败)

最流行的工作流

  • (1)任何新的功能(feature)或 bug 修复全都新建一个 branch 来写;
  • (2)branch 写完后,合并到 master,然后删掉这个 branch(可使用git origin -d 分支名删除远程仓库的分支)。

优势:(1)代码分享:写完之后可以在开发分支review之后再merge到master分支
(2)一人多任务:当正在开发接到更重要的新任务时,你只要稍微把目前未提交的代码简单收尾一下,然后做一个带有「未完成」标记的提交(例如,在提交信息里标上「TODO」),然后回到 master 去创建一个新的 branch 进行开发就好了。

HEAD、branch、引用的本质、push的本质

  • HEAD:当前commit的引用。当前 commit 在哪里,HEAD 就在哪里,这是一个永远自动指向当前 commit 的引用,所以你永远可以用 HEAD 来操作当前 commit,
  • branch:HEAD 是 Git 中一个独特的引用,它是唯一的。而除了 HEAD 之外,Git 还有一种引用,叫做 branch(分支)。HEAD 除了可以指向 commit,还可以指向一个branch,当指向一个branch时,HEAD会通过branch间接指向当前commit,HEAD移动会带着branch一起移动:

branch 包含了从初始 commit 到它的所有路径,而不是一条路径。并且,这些路径之间也是彼此平等的。

像上图这样,master 在合并了 branch1 之后,从初始 commit 到 master 有了两条路径。这时,master 的串就包含了 1 2 3 4 7 和 1 2 5 6 7 这两条路径。而且,这两条路径是平等的,1 2 3 4 7 这条路径并不会因为它是「原生路径」而拥有任何的特别之处

创建branch:git branch 名称
切换branch:git checkout 名称(将HEAD指向该branch)
创建+切换:git checkout -b 名称
在切换到新的 branch 后,再次 commit 时 HEAD 就会带着新的 branch 移动了:

而这个时候,如果你再切换到 master 去 commit,就会真正地出现分叉了:

删除branch:git branch -d 名称
注意:

  • (1)HEAD 指向的 branch 不能删除。如果要删除 HEAD 指向的 branch,需要先用 checkout 把 HEAD 指向其他地方。
  • (2)由于 Git 中的 branch 只是一个引用,所以删除 branch 的操作也只会删掉这个引用,并不会删除任何的 commit。(不过如果一个 commit 不在任何一个 branch 的「路径」上,或者换句话说,如果没有任何一个 branch 可以回溯到这条 commit(也许可以称为野生 commit?),那么在一定时间后,它会被 Git 的回收机制删除掉)
  • (3)出于安全考虑,没有被合并到 master 过的 branch 在删除时会失败(怕误删未完成branch)把-d换成-D可以强制删除

引用的本质

所谓引用,其实就是一个个的字符串。这个字符串可以是一个 commit 的 SHA-1 码(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也可以是一个 branch(例:ref: refs/heads/feature3)。

Git 中的 HEAD 和每一个 branch 以及其他的引用,都是以文本文件的形式存储在本地仓库 .git 目录中,而 Git 在工作的时候,就是通过这些文本文件的内容来判断这些所谓的「引用」是指向谁的。

push的本质:把 branch 上传到远程仓库

  • (1)把当前branch位置上传到远程仓库,并把它路径上的commits一并上传
  • (2)git中(2.0及以后版本),git push不加参数只能上传到从远程仓库clone或者pull下来的分支,如需push在本地创建的分支则需使用git push origin 分支名的命令
  • (3)远端仓库的HEAD并不随push与本地一致,远端仓库HEAD永远指向默认分支(master),并随之移动(可以使用git br -r查看远程分支的HEAD指向)。

rebase:给 commit 重新设置基础点

  • rebase 操作可以把本地未push的分叉提交历史整理成直线;
  • rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。

rebase:变基、复位、重定基准、 重置为原始基本

  • 在 Git 中,Rebase 的主要作用是将一个分支上的提交应用到另一个分支上,通常用于合并代码或保持分支的更新。与传统的合并(merge)不同,Rebase 可以将提交应用到目标分支的最新提交之后,使提交历史更加整洁和线性。

Rebase 的基本用法如下:

  • 切换到目标分支:首先,你需要切换到想要应用提交的目标分支。
  • 执行 rebase 命令:运行 git rebase <源分支> 命令,其中 <源分支> 是你希望从中获取提交的分支。这将会将源分支上的提交逐个应用到目标分支上。
  • 解决冲突:如果在 Rebase 过程中发生冲突,Git 会暂停 Rebase,并提示你解决冲突。你需要手动解决冲突并提交更改,然后使用 git rebase --continue 继续执行 Rebase。
  • 完成 Rebase:当所有提交都成功应用到目标分支上,Rebase 就完成了。

多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。每次合并再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

总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?其实是可以做到的!Git有一种称为rebase的操作,有人把它翻译成 "变基"。

rebase

先不要随意展开想象。我们还是从实际问题出发,看看怎么把分叉的提交变成直线。

在和远程分支同步后,我们对hello.py这个文件做了两次提交。用git log命令看看:

        $ git log --graph --pretty=oneline --abbrev-commit
        * 582d922 (HEAD -> master) add author
        * 8875536 add comment
        * d1be385 (origin/master) init hello
        *   e5e69f1 Merge branch 'dev'
        |\  
        | *   57c53ab (origin/dev, dev) fix env conflict
        | |\  
        | | * 7a5e5dd add env
        | * | 7bd91f1 add new env
        ...

注意到Git用(HEAD -> master)(origin/master)标识出当前分支的HEAD和远程origin的位置分别是582d922 add authord1be385 init hello,本地分支比远程分支快两个提交。

现在我们尝试推送本地分支:$ git push origin master

很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先pull一下:$ git pull

再看下状态:$ git status

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

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

对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push到远程,有没有问题?有!什么问题?不好看!有没有解决方法?有!这个时候,rebase就派上了用场。我们输入命令git rebase试试:$ git rebase

输出了一大堆操作,到底是啥效果?看下log:git log --graph --pretty=oneline --abbrev-commit

原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello,而是基于f005ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。

这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。最后,通过push操作把本地分支推送到远程:git push origin master

再用git log看看效果:git log --graph --pretty=oneline --abbrev-commit

远程分支的提交历史也是一条直线。

有些人不喜欢 merge,因为在 merge 之后,commit 历史就会出现分叉,这种分叉再汇合的结构会让有些人觉得混乱而难以管理。如果你不希望 commit 历史出现分叉,可以用 rebase 来代替 merge。

可以看出,通过 rebase,5 和 6 两条 commits 把基础点从 2 换成了 4 。通过这样的方式,就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作,就是 rebase 的含义。另外,在 rebase 之后,记得切回 master 再 merge 一下,把 master 移到最新的 commit。

为什么要从 branch1 来 rebase,然后再切回 master 再 merge 一下这么麻烦,而不是直接在 master 上执行 rebase?从图中可以看出,rebase 后的每个 commit 虽然内容和 rebase 之前相同,但它们已经是不同的 commit 了(每个commit有唯一标志)。如果直接从 master 执行 rebase 的话,就会是下面这样:

这就导致 master 上之前的两个最新 commit (3和4)被剔除了。如果这两个 commit 之前已经在远程仓库存在,这就会导致没法 push :

git 教程、常用命令_第14张图片

所以,为了避免和远程仓库发生冲突,一般不要从 master 向其他 branch 执行 rebase 操作。而如果是 master 以外的 branch 之间的 rebase(比如 branch1 和 branch2 之间),就不必这么多费一步,直接 rebase 就好。需要说明的是,rebase 是站在需要被 rebase 的 commit 上进行操作,这点和 merge 是不同的。

6. 标  签

git tag            列出所有tag
git tag [tag]    新建一个tag在当前commit
git tag [tag] [commit]    新建一个tag在指定commit
git tag -d [tag]              删除本地tag
git push origin :refs/tags/[tagName]    删除远程tag
git show [tag]                   查看tag信息
git push [remote] [tag]      提交指定tag
git push [remote] --tags    提交所有tag
git checkout -b [branch] [tag]    新建一个分支,指向某个tag

Git 有 commit,为什么还要引入 tag?“请把上周一的那个版本打包发布,commit号是6a5819e...” “一串乱七八糟的数字不好找!”如果换一个办法:“请把上周一的那个版本打包发布,版本号是v1.2”“好的,按照tag v1.2查找commit就行!”所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。Git 的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。

创建标签

在Git中打标签非常简单,首先,切换到需要打标签的分支上:

$ git branch
$ git checkout master

然后,敲命令git tag 就可以打一个新标签:$ git tag v1.0

可以用命令git tag查看所有标签:$ git tag

默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?

可以找到历史提交的commit id,然后打上就可以了:$ git log --pretty=oneline --abbrev-commit

假如要对add merge提交打标签,它对应的commit id是f52c633,执行命令:$ git tag v0.9 f52c633

再用命令git tag查看标签:$ git tag

注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show 查看标签信息:git show v0.9,可以看到,v0.9 确实打在add merge这次提交上。

还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:$ git tag -a v0.1 -m "version 0.1 released" 1094adb

用命令git show 可以看到说明文字:$ git show v0.1

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

小结

  • 命令git tag 用于新建一个标签,默认为HEAD,也可以指定一个commit id;
  • 命令git tag -a -m "blablabla..."可以指定标签信息;
  • 命令git tag可以查看所有标签。

操作标签

如果标签打错了,也可以删除:$ git tag -d v0.1

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

如果要推送某个标签到远程,使用命令git push origin :$ git push origin v1.0

或者,一次性推送全部尚未推送到远程的本地标签:$ git push origin --tags

如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:$ git tag -d v0.9

然后,从远程删除。删除命令也是push,但是格式如下:$ git push origin :refs/tags/v0.9

要看看是否真的从远程库删除了标签,可以登陆GitHub查看。

小结

  • 命令git push origin 可以推送一个本地标签;
  • 命令git push origin --tags可以推送全部未推送过的本地标签;
  • 命令git tag -d 可以删除一个本地标签;
  • 命令git push origin :refs/tags/可以删除一个远程标签。

7. 查看信息

git status    显示有变更的文件
git log        显示当前分支的版本历史
git log --stat    显示commit历史,以及每次commit发生变更的文件
git log -S [keyword]    搜索提交历史,根据关键词
git log [tag] HEAD --pretty=format:%s    显示某个commit之后的所有变动,
git log [tag] HEAD --grep feature    根据搜索条件显示某个commit之后的所有变动
git log --follow [file]    显示某个文件的版本历史,包括文件改名
git whatchanged [file]
git log -p [file]    显示指定文件相关的每一次diff
git log -5 --pretty --oneline     显示过去5次提交
git shortlog -sn    显示所有提交过的用户,按提交次数排序
git blame [file]    显示指定文件是什么人在什么时间修改过
git diff    显示暂存区和工作区的差异
git diff --cached [file]    显示暂存区和上一个commit的差异
git diff HEAD    显示工作区与当前分支最新commit之间的差异
git diff [first-branch]...[second-branch]    显示两次提交之间的差异
git diff --shortstat "@{0 day ago}"    显示今天你写了多少行代码
git show [commit]    显示某次提交的元数据和内容变化
git show --name-only [commit]    显示某次提交发生变化的文件
git show [commit]:[filename]    显示某次提交时,某个文件的内容
git reflog    显示当前分支的最近几次提交

reflog:引用记录的 log

可以查看git的引用记录,不指定参数,默认显示HEAD的引用记录;如果不小心把分支删掉了,可以使用该命令查看引用记录,然后使用checkout切到该记录处重建分支即可。

注意:不再被引用直接或间接指向的 commits 会在一定时间后被 Git 回收,所以使用 reflog 来找回被删除的 branch 的操作一定要及时,不然有可能会由于 commit 被回收而再也找不回来。

log:查看已提交内容

git log -p可以查看每个commit的改动细节(到改动文件的每一行)
git log --stat查看简要统计(哪几个文件改动了)
git show 指定commit 指定文件名查看指定commit的指定文件改动细节

diff:查看未提交内容

git diff --staged可以显示暂存区和上一条提交之间的不同。换句话说,这条指令可以让你看到「如果你立即输入 git commit,你将会提交什么」
git diff可以显示工作目录和暂存区之间的不同。换句话说,这条指令可以让你看到「如果你现在把所有文件都 add,你会向暂存区中增加哪些内容」
git diff HEAD可以显示工作目录和上一条提交之间的不同,它是上面这二者的内容相加。换句话说,这条指令可以让你看到「如果你现在把所有文件都 add 然后 git commit,你将会提交什么」(不过需要注意,没有被 Git 记录在案的文件(即从来没有被 add 过的文件,untracked files 并不会显示出来。因为对 Git 来说它并不存在)实质上,如果你把 HEAD 换成别的commit,也可以显示当前工作目录和这条 commit 的区别。

8. 远程 同步

git fetch [remote]        下载远程仓库的所有变动
git remote -v        显示所有远程仓库
git remote show [remote]        显示某个远程仓库的信息
git remote add [shortname] [url]        增加一个新的远程仓库,并命名
git pull [remote] [branch]        取回远程仓库的变化,并与本地分支合并
git push [remote] [branch]        上传本地指定分支到远程仓库
git push [remote] --force        强行推送当前分支到远程仓库,即使有冲突
git push [remote] --all        推送所有分支到远程仓库

创建 github 仓库

Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?肯定是有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。

你肯定会想,至少需要两台机器才能玩远程库不是?但是我只有一台电脑,怎么玩?

其实一台电脑上也是可以克隆多个版本库的,只要不在同一个目录下。不过,现实生活中是不会有人这么傻的在一台电脑上搞几个远程库玩,因为一台电脑上搞几个远程库完全没有意义,而且硬盘挂了会导致所有库都挂掉,所以我也不告诉你在一台电脑上怎么克隆多个仓库。

实际情况往往是这样,找一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。

在继续阅读后续内容前,请自行注册GitHub账号。由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:

  • 第 1 步:创建 SSH Key。在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有 id_rsa 和 id_rsa.pub 这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建 SSH Key:ssh-keygen -t rsa -C "[email protected]"  你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有id_rsa和id_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。
  • 第 2 步:登陆 GitHub,打开 Account settings ---> SSH Keys页面,然后点 Add SSH Key,填上任意 Title,在 Key 文本框里粘贴 id_rsa.pub 文件的内容,点 Add Key,你就应该看到已经添加的 Key

为什么 GitHub 需要 SSH Key 呢?因为 GitHub 需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。最后友情提示:在 GitHub 上免费托管的 Git 仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub 把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。另一个办法是自己动手,搭一个Git服务器,因为是你自己的Git服务器,所以别人也是看不见的,这个相当简单,公司内部开发必备。

添加 远程 库

情景:假设你已经在本地创建了一个Git 仓库后,又想在 GitHub 创建一个 Git 仓库,并且让这两个仓库进行远程同步,这样,GitHub 上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

  • 首先,登陆 GitHub,然后,在右上角找到 “Create a new repo” 按钮,创建一个新的仓库:在Repository name 填入 learn_git,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库。目前,在GitHub上的这个 learn_git 仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
  • 现在,根据 GitHub 的提示,在本地的 learn_git 仓库下运行命令:
    $ git remote add origin [email protected]:michaelliao/learn_git.git
    # 远程库名字叫origin,对应地址为:[email protected]:自己的github账号/learn_git.git
    添加后,远程库的名字就是 origin,这是 Git 默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。 
  • 把本地库的所有内容推送到远程库上:git push -u origin master
    用 git push 把当前master分支,推送到远程叫origin的库。由于远程库是空的,当第一次推送 master分支时,加上了 -u 参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的 master分支和远程的 master分支关联起来,在以后的推送或者拉取时就可以简化命令。
  • 推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一模一样。从现在起,只要本地作了提交,就可以通过命令:git push origin master。把本地 master分支 的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!

从 远程库 克隆

上面的操作是先有本地库,后有远程库,然后关联远程库。现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。

首先,登陆 GitHub创建一个新的仓库名字叫 gitskills,勾选 Initialize this repository with a README,这样 GitHub 会自动为我们创建一个README.md文件。创建完毕后可以看到README.md文件,现在远程库已经准备好了,下一步是用命令git clone克隆一个本地库:it clone [email protected]:自己的github账号/gitskills.git ,然后进入gitskills目录看看,已经有README.md文件了,如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。

克隆时 GitHub 给出的地址不止一个,还可以用 https://github.com/michaelliao/gitskills.git 这样的地址。实际上,Git支持多种协议,默认的 git:// 使用 ssh,也可以使用 https 等其他协议。使用 https 除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http 端口的公司内部就无法使用 ssh 协议而只能用 https。

使用 github

我们一直用GitHub作为免费的远程仓库,如果是个人的开源项目,放到GitHub上是完全没有问题的。其实GitHub还是一个开源协作社区,通过GitHub,既可以让别人参与你的开源项目,也可以参与别人的开源项目。

在GitHub出现以前,开源项目开源容易,但让广大人民群众参与进来比较困难,因为要参与,就要提交代码,而给每个想提交代码的群众都开一个账号那是不现实的,因此,群众也仅限于报个bug,即使能改掉bug,也只能把diff文件用邮件发过去,很不方便。

但是在GitHub上,利用Git极其强大的克隆和分支功能,广大人民群众真正可以第一次自由参与各种开源项目了。

如何参与一个开源项目呢?比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,你可以访问它的项目主页 https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:git clone [email protected]:自己github账号/bootstrap.git 。一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址[email protected]:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。

Bootstrap的官方仓库twbs/bootstrap、你在GitHub上克隆的仓库my/bootstrap,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:

git 教程、常用命令_第15张图片

如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。

如果你希望bootstrap的官方库能接受你的修改,你就可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。

如果你没能力修改bootstrap,但又想要试一把pull request,那就Fork一下我的仓库:https://github.com/michaelliao/learngit,创建一个your-github-id.txt的文本文件,写点自己学习Git的心得,然后推送一个pull request给我,我会视心情而定是否接受。

小结

  • 在GitHub上,可以任意Fork开源仓库;
  • 自己拥有Fork后的仓库的读写权限;
  • 可以推送pull request给官方仓库来贡献代码。

搭建 Git 服务器

GitHub就是一个免费托管开源代码的远程仓库。但是对于某些视源代码如生命的商业公司来说,既不想公开源代码,又舍不得给GitHub交保护费,那就只能自己搭建一台Git服务器作为私有仓库使用。搭建Git服务器需要准备一台运行Linux的机器,强烈推荐用Ubuntu或Debian,这样,通过几条简单的apt命令就可以完成安装。

假设你已经有sudo权限的用户账号,下面,正式开始安装。

  • 第一步,安装git:$ sudo apt-get install git
  • 第二步,创建一个git用户,用来运行git服务:$ sudo adduser git
  • 第三步,创建证书登录:收集所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个。
  • 第四步,初始化Git仓库:先选定一个目录作为Git仓库,假定是/srv/sample.git,在/srv目录下输入命令:$ sudo git init --bare sample.git,Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。然后,把owner改为git:$ sudo chown -R git:git sample.git
  • 第五步,禁用shell登录,出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行 git:x:1001:1001:,,,:/home/git:/bin/bash 改为 git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell ,这样git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。
  • 第六步,克隆远程仓库,现在可以通过git clone命令克隆远程仓库了:git clone git@server:/srv/sample.git ,剩下的推送就简单了。

管理公钥

如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。这里我们不介绍怎么玩Gitosis了,几百号人的团队基本都在500强了,相信找个高水平的Linux管理员问题不大。

管理权限

有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。这里我们也不介绍Gitolite了,不要把有限的生命浪费到权限斗争中。

小结

  • 搭建Git服务器非常简单,通常10分钟即可完成;
  • 要方便管理公钥,用Gitosis;
  • 要像SVN那样变态地控制权限,用Gitolite

9. 撤销、回退

git checkout [file]    恢复暂存区的指定文件到工作区

git checkout *     一键还原 所有文件
git checkout [commit] [file]    恢复某个commit的指定文件到暂存区和工作区
git checkout .    恢复暂存区的所有文件到工作区
git reset [file]    重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset --hard    重置暂存区与工作区,与上一次commit保持一致
git reset [commit]    重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset --hard [commit]    重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --keep [commit]    重置当前HEAD为指定commit,但保持暂存区和工作区不变
git revert [commit]    新建一个commit用来撤销指定commit,后者的所有变化都将被前者抵消,并且应用到当前分支
# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

git HEAD
git HEAD~        //上一个版本
git HEAD~100     //往上100个版本
git checkout          //恢复未提交的更改
git reset HEAD        //取消之前 git add 添加
git reset --hard HEAD~              //回退到上一个版本
git reset --hard        //回退到指定版本
如果远程分支也想要回退,git push -f (known changes)。
git revert HEAD                  //撤销前一次commit

git revert 和 git reset 的区别

  1. git revert是用一次新的commit来回滚之前的commit,git reset 是直接删除指定的commit。
  2. 在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入。
  3. git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。

版本 回退

在 git 中,用 HEAD 表示当前版本也就是最新的提交,上一个版本就是HEAD^上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成 HEAD~100

回退到上一个版本:git reset --hard HEAD^

git log 看现在版本库的状态,可以发现已经回退

回退到特定版本:git reset --hard 1094a(提交id)  # 版本号前几位就可以,Git会自动去找

git reflog 用来记录你的每一次命令

总结:

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

"工作区、暂存区" 回退

git checkout -- readme.txt  可以丢弃工作区的修改。

命令 git checkout -- readme.txt 是把文件readme.txt在工作区的修改全部撤销,有两种情况:

  • readme.txt 自修改后还没有被放到暂存区,现在撤销修改就回到和版本库一模一样的状态;
  • readme.txt 已经添加到暂存区后,随后又作了修改,现在撤销修改,就是回到添加到暂存区后的状态。
  • git checkout 其实是用 "版本库里的版本或者暂存区的版本" 替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。总之,就是让这个文件回到最近一次git commit或git add时的状态。git checkout -- file 命令中的 -- 很重要,没有--就变成了 "切换到另一个分支" 的命令

git reset 命令既可以回退版本,也可以把暂存区的修改回退到工作区。HEAD 表示最新的版本
git reset HEAD 可以把暂存区的修改撤销掉(unstage),重新放回工作区:git reset HEAD readme.txt
再用 git status 查看一下,现在暂存区是干净的,工作区有修改:
丢弃工作区的修改:git checkout -- readme.txt

小结

  • 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令 git checkout -- file ,会直接用暂存区最后一次提交的覆盖工作区的。
  • 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD ,就回到了场景1,第二步按场景1操作。
  • 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考 版本回退,不过前提是没有推送到远程库。

在 Git 中删除也是一个修改操作。直接在文件管理器中把没用的文件删了,或者用rm命令删了,这个时候,Git 知道你删除了文件,因此,工作区和版本库就不一致了,git status 命令会立刻告诉你哪些文件被删除了。现在你有两个选择,

  • 一是确实要从版本库中删除该文件,那就用命令 git rm test.txt 删掉并且 git commit,现在文件就从版本库中被删除了。
  • 另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:git checkout -- test.txt

刚刚提交的代码发现写错了怎么办?

再提一个修复了错误的commit?可以是可以,不过还有一个更加优雅和简单的解决方法:commit --amend。
具体做法:
(1)修改好问题
(2)将修改add到暂存区
(3)使用git commit --amend提交修改,结果如下图:

减少了一次无谓的commit。

错误不是最新的提交而是倒数第二个?

使用rebase -i(交互式rebase):
所谓「交互式 rebase」,就是在 rebase 的操作执行之前,你可以指定要 rebase 的 commit 链中的每一个 commit 是否需要进一步修改,那么你就可以利用这个特点,进行一次「原地 rebase」。

操作过程:
(1)git rebase -i HEAD^^

说明:在 Git 中,有两个「偏移符号」: ^ 和 ~。
^ 的用法:在 commit 的后面加一个或多个 ^ 号,可以把 commit 往回偏移,偏移的数量是 ^ 的数量。例如:master^ 表示 master 指向的 commit 之前的那个 commit; HEAD^^ 表示 HEAD 所指向的 commit 往前数两个 commit。
~ 的用法:在 commit 的后面加上 ~ 号和一个数,可以把 commit 往回偏移,偏移的数量是 ~ 号后面的数。例如:HEAD~5 表示 HEAD 指向的 commit往前数 5 个 commit。

上面这行代码表示,把当前 commit ( HEAD 所指向的 commit) rebase 到 HEAD 之前 2 个的 commit 上:

(2)进入编辑页面,选择commit对应的操作,commit为正序排列,旧的在上,新的在下,前面黄色的为如何操作该commit,默认pick(直接应用该commit不做任何改变),修改第一个commit为edit(应用这个 commit,然后停下来等待继续修正)然后:wq退出编辑页面,此时rebase停在第二个commit的位置,此时可以对内容进行修改:

git 教程、常用命令_第16张图片

(3)修改完后使用add,commit --amend将修改提交
(4)git rebase --continue继续 rebase 过程,把后面的 commit 直接应用上去,这次交互式 rebase 的过程就完美结束了,你的那个倒数第二个写错的 commit 就也被修正了:

想直接丢弃某次提交?

reset --hard 丢弃最新的提交:git reset --hard HEAD^

HEAD^ 表示 HEAD 往回数一个位置的 commit 

用交互式 rebase 撤销历史提交

操作步骤与修改历史提交类似,第二步把需要撤销的commit修改为drop,其他步骤不再赘述。

git 教程、常用命令_第17张图片

用 rebase --onto 撤销提交

git rebase --onto HEAD^^ HEAD^ branch1
上面这行代码的意思是:以倒数第二个 commit 为起点(起点不包含在 rebase 序列里),branch1 为终点,rebase 到倒数第三个 commit 上。

错误代码已经push?

有的时候,代码 push 到了远程仓库,才发现有个 commit 写错了。这种问题的处理分两种情况:

出错内容在自己的分支

假如是某个你自己独立开发的 branch 出错了,不会影响到其他人,那没关系用前面几节讲的方法把写错的 commit 修改或者删除掉,然后再 push 上去就好了。但是此时会push报错,因为远程仓库包含本地没有的 commits(在本地已经被替换或被删除了),此时直接使用git push origin 分支名 -f强制push。

问题内容已合并到master

(1)增加新提交覆盖之前内容
(2)使用git revert 指定commit 它的用法很简单,你希望撤销哪个 commit,就把它填在后面。如:git revert HEAD^
上面这行代码就会增加一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。在 revert 完成之后,把新的 commit 再 push 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit。

reset:不止可以撤销提交

git reset --hard 指定commit你的工作目录里的内容会被完全重置为和指定commit位置相同的内容。换句话说,就是你的未提交的修改会被全部擦掉。
git reset --soft 指定commit会在重置 HEAD 和 branch 时,保留工作目录和暂存区中的内容,并把重置 HEAD 所带来的新的差异放进暂存区。
什么是「重置 HEAD 所带来的新的差异」?就是这里:

git reset --mixed(或者不加参数) 指定commit保留工作目录,并且清空暂存区。也就是说,工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作区。简而言之,就是「把所有差异都混合(mixed)放在工作区中」。

checkout:签出指定commit

checkout的本质是签出指定的commit,不止可以切换branch还可以指定commit作为参数,把HEAD移动到指定的commit上;与reset的区别在于只移动HEAD不改变绑定的branch;git checkout --detach可以把 HEAD 和 branch 脱离,直接指向当前 commit。

强势安利《git原理详解与实用指南》

3、Git 命令思维导图

git 流程图(如果看不清楚图片,可以 ——> 右键——> 在新标签页中打开图片):

图 1

git 教程、常用命令_第18张图片

图 2

git 教程、常用命令_第19张图片


 

你可能感兴趣的:(工具,Git,git)