版本控制工具入门——GIT
GIT 与 SVN 的区别
SVN 服务器
↗ ↑ ↖
/ | \
/ | \
/ | \
↙ ↓ ↘
SVN 客户端 SVN 客户端 SVN 客户端
SVN 是 集中式管理
,版本库
位于 SVN 服务器
上,优点是便于管理员掌控 开发进度
,也容易给每个开发人员 授权
。缺点是,服务器可能发生 单点故障
,并且 容错性较差
。
共享版本库
——————————————————————
/ / ↗ \ \ ↖
/ / / \ \ \
/ / / \ \ \
/ / Push Clone\ \
/ Pull / \ Pull \
Clone / / \ \ Push
↙ ↙ / ↘ ↘ \
开发人员 —— 开发人员 ——
↑ | ↑ |
| | | |
——————Commit ——————Commit
GIT 是 分布式版本控制系统
,没有中央服务器,每个开发人员都 拥有完整的版本库
,开发时无需联网,修改完毕后再提交给 共享版本库
即可。
简单来说,GIT 拥有 本地仓库
,而 SVN 必须连接到远程仓库修改代码。
GIT 版本控制流程图
———————————————Pull(Fetch + Merge)—————————————
| ↓
远程仓库 ——Clone——→ 本地仓库 ————Checkout——→ 工作区
Remote ←——Push—— Repository Workspace
↑ 暂存区 |
Commit——————— Index ←——————Add
Stage
GIT 常用命令详解
GIT 配置
同时操作 Github 和公司私有仓库需要配置不同的邮箱和私钥;Github 配置代理才可高速访问,公司不需要。以上需求都可通过 git config
预先配置。该命令有三个作用域选项,--system
,--global
和 --local
,分别用来对系统,全局和项目局部进行配置,优先级由低到高,默认 --local
。除了使用命令,也可直接编辑配置文件,--global
对应 $HOME/.gitconfig
,--local
对应项目工作目录下的 .git/config
。
# 查看所有配置
git config --list
用户和密钥配置
配置密钥可避免每次提交输入密码。
# 生成多个密钥,-t(type),-C(comment),-f(file)
ssh-keygen -t rsa -C "[email protected]" -f ~/.ssh/id_rsa_github
ssh-keygen -t rsa -C "[email protected]" -f ~/.ssh/id_rsa_gitlab
# 将加载密钥脚本添加到 bash 启动文件
cat >> ~/.bashrc <<"EOF"
# 启动 ssh-agent 管理 ssh session
eval `ssh-agent -s` > /dev/null 2>&1
keys=(`ls ~/.ssh/*.pub | sed 's/.pub//g' | xargs`)
for key in ${keys[@]}
do
ssh-add "${key}" > /dev/null 2>&1
done
EOF
cat >> ~/.bash_logout <<"EOF"
eval $(ps -ef | grep ssh-agent | grep -v grep | awk '{ print "kill "$2 }')
EOF
# 重启 bash
/usr/bin/env bash
ssh -T [email protected]
# 向全局配置文件添加不同 git 站点
cat >> $HOME/.ssh/config <<'EOF'
# gitlab
Host git.iboxpay.com
HostName git.iboxpay.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_gitlab
User admin
# github
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_github
User admin
EOF
# 切换到每个项目目录,当独设置用户名和邮箱
git config user.email "[email protected]"
git config user.name "admin"
# 取消设置
git config --unset user.name
git config --unset user.email
代理配置
为 Github 设置代理可以加快代码同步速度,对大项目很有必要。
# 设置
git config --global http.https://github.com.proxy socks5://127.0.0.1:1080
# 取消设置
git config --global --unset http.https://github.com.proxy
初始化仓库
新建文件夹 repositories/repo1
作为工作区 Working Directory/Workspace
。进入工作区,通过 git init
或 git clone
命令创建一个本地仓库/版本库或下载远程版本库,该操作将会在工作区初始化一个 .git
文件夹,存储 版本库
。
# 递归创建目录
mkdir -p repositories/repo1
# 进入目录
cd repositories/repo1
# 初始化本地仓库
git init
# 不创建工作目录/工作区,用作远端共享版本库
# git init --bare
# 下载远端仓库
# git clone ssh://[email protected]/home/git/repo1
# 指定远程分支
# git clone -b dev ssh://[email protected]/home/git/repo1
提交变更
版本库中最重要的是暂存区 Stage/Index
,每次在工作区 新建、修改或删除
文件后,都要使用 git add
将变更添加到暂存区,随后使用 git commit
将暂存区中的变更提交到当前 HEAD
指针指向的分支。首次 commit
时,GIT 会自动创建第一个分支 master
和指向 master
的指针 HEAD
,下面是工作区和版本库的示意图。
Workspace | Repository
dir1 | HEAD
|--file1 | ↘
|--file2 | Stage master
|--file3 add——→ | dir2 dir1
|--dir2 ↗ | |--file1 |--file1
|--file4 | |--file2 |--fiel2
|--file5 | ↘ |--file3
| commit——→ |--dir2
| |--file4
| |--file5
# 新建 HelloWorld.java 文件
cat > HelloWorld.java <<"EOF"
public class HelloWorld {
public static void main(String[] args) {
// Prints "Hello, World" to the terminal window.
System.out.println("Hello, World");
}
}
EOF
# 添加单个修改文件到暂存区
git add HelloWorld.java
# 添加所有修改文件
# git add .
# 提交变更到本地仓库
git commit -m "Initial commit."
# 查看提交记录
git log --oneline
# 撤销某次之后的所有提交
git reset <commit_hash>
commit message
包含 Header
,Body
和 Footer
三部分,一般仅使用 Header
。按照 type(scope): subject
格式填写 Header
更有助于团队合作。scope 表示功能模块,subject 代表主题,type 为类型,一般定义如下:
- feat:新功能
- fix:修复 bug
- style:更改格式
- refactor:代码重构
- chore:项目重建
git commit -m "fix(security): upgrade lodash.template"
辅助提交
nodejs
插件 commitizen
,可以帮助你填入标准的 commit message
,在此之前需要安装 nodejs
,可以参考 该教程。
# 安装 commitizen
npm install commitizen -g
# 使用 cz-conventional-changelog 包初始化 commitizen,
#+ 需要在项目仓库运行
npm init
commitizen init cz-conventional-changelog --save-exact
# 使用 git cz 代替 git commit 提交,之后根据提示填写 message
git cz
生成 Changelog
使用 conventional-changelog
插件,可以方便地生成标准 Changelog
,默认根据 commit message
的 feat
和 fix
生成。
# 安装 conventional-changelog
npm install conventional-changelog -g
# 生成 Changelog,-p(preset),
#+ -r 0 从首次 commit 开始生成,覆盖之前的 CHANGELOG.md
conventional-changelog -p angular -i CHANGELOG.md -s -r 0
文件状态追踪
仓库中的文件分为 已追踪
和 未追踪
两种。已追踪文件在版本库中存在记录,用户工作一段时间后,这些文件仍可被查看。下面是文件状态变更图。
sequenceDiagram
Unmodified->>Modified: Edit the file
Modified->>Staged: Stage the file(git add)
Unmodified->>Untracked: Remove the file
Untracked->>Staged: Add the file
Staged->>Unmodified: Commit
通过 git status
可以查看到最近的文件状态变更,主要意义在于提醒开发者 commit
前应先 add
,之后才可以 push
。
# 假设之前已经 commit 过
# 新增一个文件
touch newFile
# 修改一个文件
echo 1 >> oldFile1
# 删除一个文件
rm oldFile2
# 查看文件状态变更
git status
# newFile 属于 Untracked 文件
# oldFile1 属于 Modified 文件
# oldFile2 属于 Unstaged 中的 Deleted 文件
除了查看所有文件状态,通过 git diff
还可查看当前对某个文件的具体修改。
# 查看未暂存文件修改信息
git diff
# 查看已暂存文件修改信息
git diff --staged
忽略特定文件/文件夹
在某个目录新建 .gitignore
文件,并将需要忽略的文件/文件夹写入其中,即可在 git commit
时忽略它们,该文件的语法规则如下:
- 每行一条规则
- 空行用于增强可读性
- 以
#
开头的行将被当作注释,不参与解析 - 以
\
开头的规则必须使用\\
- 行尾的若干空格将被忽略,除非使用
\
注释每个空格 !
前缀用于否定前面的规则,但对上级目录设定的规则无效,也不会对子目录中的文件生效,如文件以!
开头,要使用\!
注释- 在名称后添加
/
解析为文件夹,如foo/
,表示忽略当前目录的foo/
及其子文件夹,不忽略foo
文件 - 不含
/
的规则会被当作shell glob
匹配:*
匹配除了/
的任何字符串,?
匹配除了/
的单个字符,[]
匹配特定范围的单个字符 - 行首的
/
用来避免递归,如/*.c
匹配cat-file.c
,不匹配mozilla-sha1/sha1.c
与完整路径名匹配的连续两个
**
可能有特殊意义:- 以
**
开头后跟/
匹配所有路径下的文件。如**/foo
匹配任何地方的foo
文件或路径。**/foo/bar
匹配foo
目录下任何地方的bar
文件或路径 - 以
/**
结尾的规则匹配目录下的所有文件和路径,如abc/**
匹配abc
及其子目录下的所有文件 /**/
匹配零个或多个目录。如a/**/b
匹配a/b
,a/x/b
,a/x/y/b
等等
- 以
操作远程仓库
远程仓库、origin、本地仓库、暂存区和工作目录的关系。
remote repository ——-
| |
git fetch | | git pull
↓ |
origin(remote name) |
local repository ←--
↑
git commit|
|
index
↑
git add |
|
working directory
# 与多个远程仓库建立连接,origin 是远程仓库别名
git remote add origin ssh://[email protected]/home/git/repo0
git remote add origin1 ssh://[email protected]/home/git/repo1
# 删除与远程仓库的连接
git remote remove origin
# 查看所有 remote 地址
git remote -v
# 将本地分支推送至远程仓库的 master 分支
#+ -u | --set-upstream,设置默认提交上游,
#+ 执行一次后,之后提交直接 git push 即可
git push -u origin master
# git push -u origin1 master
# 删除远程分支
git push origin -d dev
# git push origin1 -d dev
# 推送 tag 到远程分支
git push --tag origin dev
处理冲突
假如你的协作者和你同时拉取最新版本代码并对同一文件进行修改,当你想把变更推送至远端,而他人先于你推送时便会发生冲突,此时需要使用 git merge
或 git rebase
手动合并你的变更,随后才能推送。GIT 会把文件中的冲突区域标记在 <<<<<<< HEAD
和 >>>>>>> [other/branch/name]
之间,中间用 =======
隔开。
使用 git merge
合并时,会一次性解决之前所有提交的冲突,而 git rebase
仅解决一次提交发送的冲突,这意味着开发者之后还要执行多次 git rebase --continue
操作。
# 保存本地代码
# git stash
# 使用 merge 合并
# git fetch
# git merge
git pull
# 使用 rebase 合并
# git rebase
# git rebase --continue
# 合并本地与远程
# git stash pop
# 冲突区域示意
cat conflictFile
<<<<<<< HEAD
int a=1;
=======
int a= 0;
>>>>>>> master
冲突处理完毕后需要将其重新添加到暂存区,再提交到本地仓库,随后提交到远程仓库。
git add .
git commit -m "refactor(Login): Merge file"
git push
管理分支
开始时,HEAD
指针指向 master 分支,master 指向最新提交,,两个指针随着 commit
不断后移如此,如此就能确定当前分支和当前提交点。
HEAD
↓
master
↓
◯——————◯——————◯
v1 v2 v3
创建新分支并切换
一般 master
分支用于发布新版本,dev
分支用来开发,hotfix
分支用来修复 bug
。创建新分支并切换时,HEAD 指针会执行它。
# 创建分支
# git branceh dev
# 切换分支
# git checkout dev
# 可合并为一条命令
git checkout -b dev
master
↓
◯——————◯——————◯
v1 v2 v3
↖
\
dev
↑
HEAD
此时,如果再进行一次提交,master
仍会停留在原位置,dev
后移指向最新提交。
# 一些修改
# ...
# 提交变更
git commit -m "refactor(Login): change code structure"
master
↓
◯——————◯——————◯
v1 v2 v3
\
\
◯
dev1
↑
dev
↑
HEAD
合并分支
假设开发到 dev2
时已趋于稳定,计划合并到主分支,可通过 git merge
和 git rebase
进行分支合并,若使用前者,两个分支在合并后都会指向公共的提交;若使用后者,将被合并分支的提将被拷贝到当前分支上,被合并分支没有提交记录。
HEAD
↓
master
↓
◯——————◯——————◯——————◯——————◯——————◯
v1 v2 v3 v4 v5 v6
\ /
\ /
◯ /
dev1 /
| /
◯———————————————
dev2
↑
dev
[git merge]
--------------------------------------------
[git rebase]
HEAD
↓
master
↓
◯——————◯——————◯——————◯——————◯——————◯——————◯
v1 v2 v3 v4 v5 dev1' dev2'
\
\
◯
dev1
|
◯
dev2
↑
dev
# 切换回最终分支
git checkout master
# git fetch orign master
# git pull 等于 git fetch + git merge
# 将 dev 合并过来
git merge dev
# git rebase dev
# 撤销合并
git merge --abort
# git rebase --abort
# 回滚到合并前状态
# git reset --hard
# git reset <commit_hash> --hard
删除分支
# 删除
git branch -d dev
# 查看所有分支
git branch
标签管理
标签一般用作版本号,打上的标签是固定的,不像分支那样可以移动位置。
HEAD
↓
master
↓
◯——————◯——————◯
| | |
v1 v2 v3
# 查看所有标签
git tag
# 查看某些标签,-l(list)
git tag -l v0.0.*
# 为当前提交加标签,-a(add) -m(message)
git tag -am "This is the first version." v1
# 为之前某次提交打标签
# 查看提交记录
git log --oneline
# 加上 commit hash 前几位(可区分即可)
git tag 4a48e8e5f60c -am "This is the first version." v0.9
# 删除标签,-d(delete)
git tag -d v1
# 推送标签
git push --tag
撤销和回滚
可以撤销 commit
之前和之后的变更,commit
之前的变更包括未进暂存区和已进入暂存区的更改。
# 修改文件
echo "new content" >> oldFile
git status
# 未进暂存区,使用 checkout 撤销
# 撤销单个文件
git checkout --oldFile
# 撤销所有文件
git checkout
git status
##############################
echo "new content" >> oldFile
git add .
git status
# 已进入暂存区,使用 reset HEAD 将其拉出
# 拉出单个文件
git reset HEAD oldFile
# 拉出所有文件
git reset HEAD
git status
##############################
echo "new content" >> oldFile
git add .
git commit -m "refactor[Login]: add some function"
git push
git log
# 撤销已有提交,revert 实际上是一次新的 commit
git revert <commit_hash>
# 再次 revert 又可恢复提交
git revert <revert_commit_hash>
git push
当需要回滚到某次提交时,可使用 get reset
命令,与 revert
不同,该操作不会生成提交记录,因此是不可逆操作,须谨慎使用。
git reset --hard <commit_hash>
git push --force
在 IDEA 中操作 GIT
添加工程到本地仓库
- 依次进入菜单
File -> Settings -> Version Control -> Git
,配置git.exe
路径 - 新建项目,依次进入菜单
VCS -> Import into Version Control -> Create Git Repository
,选择项目上层目录 - 此时面板上会出现 Git 菜单,点击
Commit
图标,选择需要提交的文件和文件夹,填入message
提交即可
远程仓库的克隆和推送
- 依次进入菜单
File -> New -> Project from Version Contrl -> Git
,添加远程仓库地址 - 测试
ssh
方式Windows
下不可用,使用https
方式,输入用户名密码确定即可克隆 - 推送时,点击 Git 菜单的
Commit
,选择Commit and Push
操作分支
- 依次进入菜单
VCS -> Git -> Branches
,选择New Branch
或Checkout Tag or Revision
完整项目示例
############## 项目创建 ##############
# 创建文件夹
mkdir repo
# 进入文件夹
cd repo
# 初始化仓库
git init
# 配置用户名
git config user.name "logi"
# 配置邮箱
git config user.email "[email protected]"
# 配置命令别名
git config --global alias.pull pl
git config --global alias.push ps
git config --global alias.commit cm
git config --global alias.merge mg
git config --list
# 添加 .gitignore 文件
cat > .gitignore <<'EOF'
# nodejs 相关
node_modules/
npm-debug.log*
yarn-debug.log*
npm-error.log*
# 编译后文件
/dist/
# 编辑器配置
.DS_Store
.idea
.vscode
*.suo
*.njsproj
*.sln
EOF
# 添加 README 文件
cat > README.md << 'EOF'
# Git 命令详解
EOF
# 进行首次提交
git add .
git commit -m "chore(all): initial project"
# 与远程仓库建立连接
git remote add origin ssh://[email protected]/home/git/repo
# 查看所有远程仓库
git remove -v
# 推送到远程仓库
git push -u origin master
############## 本地开发 ##############
# 建立新分支
git branch logi
# 切换到新分支
git checkout logi
# 在 logi 分支模拟开发
cat > index.js << 'EOF'
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
EOF
# 查看最近变更
git status
# 将变更加入暂存区
git add .
# 提交变更
git commit -m "feat(index): add project index"
# 推送到远程仓库
git push -u origin logi
############## 项目上线 ##############
# 切换到 master 分支
git checkout master
# 合并 logi 分支
git merge logi
# 推送到远端 master 分支
git push -u origin master
# 打 tag
git tag -a v0.1.0 -m "First version"
# 推送 tag 到远程分支
git push --tag origin master
############## 问题修复 ##############
# 建立新分支并切换
git checkout -b hotfix-718
# 模拟修复过程
echo >> index.js <<'EOF'
function hotfix718() { //... }
hotfix718;
EOF
# 提交代码
git add .
git commit -m "fix(index): fix ui bugs"
git push -u origin hotfix-415
# 切换回 master 分支
git checkout master
git merge hotifix-718
git push
# 打上新版本并提交
git tag -a v0.1.1 -m "Fix bugs"
git push --tag origin master
# 删除 hotfix 分支
git branch -d hotfix-718
git push origin -d hotfix-718
############## 继续开发 ##############
# 切换到个人分支 logi
git checkout logi
# 修改某个模块
echo > index.js <<'EOF'
// ...
EOF
# 发现忘记拉取 master 最新代码,
#+ 此时需要保存工作
git stash
# 拉取最新代码
git pull origin master
# 恢复暂存文件,之后可能需要处理冲突
git stash pop
# 提交修改
git add .
git commit -m "feat(index): add auth module"
git push
# 合并到 master 并推送
git checkout master
git merge logi
git push origin master
# 打上新版本并提交
git tag -a v0.1.2 -m "Fix bugs"
git push