一、从项目说起
我们从上一章用Maven搭建的项目讲起,老样子怎么安装Git不说,前面结合IDEA讲解操作,后面都是基于命令行的教程,在终端输入git --version
输出版本表示已经安装成功:
$ git--version
git version 2.17.0
以后表示输入命令都使用
$
开头
1. 添加配置
# 配置用户 $ git config --global user.name "AiJiangnan" # 配置用户邮箱 $ git config --global user.email "904629998@qq.com" # 查看配置 $ git config -l
带有--global
参数的是全局配置
2. 创建仓库
使用IDEA打开项目,点击菜单:VCS -> Enable Version Control Integration,弹出成功框后当前项目就已经创建好本地仓库了。如果没有成功,检查IDEA设置是否集成了Git。
打开设置(Ctrl+Alt+S
):File -> Settings -> Version Control -> Git 进行配置:
在第一项设置 Path to Git executable 配置Git的执行文件路径,点击右侧的 Test 弹出成功框,表示配置成功。配置成功后再执行一下前面创建仓库的操作即可。打开IDEA的版本管理界面(Alt+9
)如下:
在左侧菜单图标找到 Group By 勾选 Directory 就可以将修改的文件以目录的形式显示。
3. 提交代码
发现项目中的文件并不是所有的文件都要交给Git仓库管理,我们只需要管理源代码和必要的资源文件,像IDEA的配置文件和编译生成的文件都可以忽略,在项目根目录创建一个.gitignore
文件,并输入需要忽略的文件或目录。下面是参考内容:
# idea .idea *.iml # maven target # java *.class *.log
此时 Local Changes 中那些忽略的文件和文件夹已经没有了。全选中 Local Changes 中的文件右击,然后选择 Commit (Ctrl+K
),弹出下面窗口:
左侧上方显示选中的提交的文件,下方填写本次修改的信息,为了记录每一次提交都做了什么改变。右侧是配置一些在提交前和提交后的操作,比如在提交前(Before Commit)我们可以:
- Reformat code:格式化代码
- Optimize imports:优化导包
- Check TODO(Show All):检查TODO
配置自己想要的操作后单击 Commit (Ctrl+Enter
)完成提交。此时在 Log 界面可以看到这次提交记录。
4. 分支管理
单击IDEA状态栏右下角的 Git: master 然后选择 New Branch,创建一个新分支,取名为dev
为我们的开发分支。
单击IDEA状态栏右下角的 Git: dev -> master -> Checkout,检出主分支。对分支的操作都可以在这进行,比如我们常用的操作:
- Checkout:检出选中分支
- Checkout As...:从选中分支创建新分支
- Compare With...:当前分支与选中分支比较
- Merge into Current:合并选中分支到当前分支
- Rename...:重命名选中分支
- Delete:删除选中分支
5. 解决冲突
当不同的分支上编辑了同一个文件时,必定会产生冲突,我们在合并的时候需要解决冲突,在IDEA里面解决冲突很简单,在master
和dev
分支上同时修改.gitignore
文件,然后将dev
合并到主分支上:
操作:
- Accept Yours:接受当前分支的修改
- Accept Theirs:接受选择分支的修改
- Merge...:合并操作
选择 Merge... 操作后:
Accept Left 和 Accept Right 的作用是选择接受哪一方的全部修改。
点击冲突区域的 << 和 >> 完成合并操作点击 Apply ,整个解决冲突的过程结束。
6. 推送代码
远程仓库不管是使用Github、Gitlab还是码云,操作都类似,在这拿Github举例。
添加SSH公钥
查看之前是否生成过SSH公钥:
$ cd ~/.ssh/ | ls # 公钥文件: id_rsa.pub
没有的自己生成,输入下面命令,然后一路回车:
$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com" Generating public/private rsa key pair. Enter a file in which to save the key (/home/you/.ssh/id_rsa): [Press enter]
生成后把公钥文件id_rsa.pub
里的内容复制下来。
# 查看公钥文件内容 $ cat ~/.ssh/id_rsa.pub
注册并登录Github,在首页点击头像下拉菜单,点击:Settings -> SSH and GPG keys -> New SSH key,输入 Title ,把前面复制的公钥输入到 Key 输入框中,点击 Add SSH key 完成。
新建并绑定远程仓库
在网站上新建一个仓库 Repository ,复制下它的地址:git@github.com:AiJiangnan/spring-boot.git
,注意确保协议选择SSH。
IDEA进入Git操作的方式:
- 菜单:VCS -> Git
- 右击项目 -> Git
- 右击打开的文件 -> Git
右击项目或文件方式进入时,当IDEA打开多个仓库时可以区分仓库
在IDEA中给当前项目添加远程仓库:Git -> Repository -> Remotes...,点击 + 号添加远程仓库,需要添加 Name 和 URL ,默认远程仓库名称为origin
,将前面复制远程仓库地址粘贴到 URL 栏中点击 OK 完成操作。
Git支持的协议:
- Local
- HTTP
- SSH
- Git
推送代码
打开推送操作:Git -> Repository -> Push... (Ctrl+Shift+K
)
左侧展示将要推送的历史提交,可以修改推送的远程仓库和远程分支。右侧展示每次提交修改的文件,可以查看每个文件的Diff
。下面 Push Tags 设置是否要推送标签,选择推送哪个标签。配置好后点击 Push (Ctrl+Enter
) 完成操作。
我们在后面代码的修改中和版本的维护中都会重复下列的操作:
Commit
:提交Pull
:拉取(更新)代码,有冲突情况Merge
:合并,有冲突情况Push
:推送,有冲突情况,确保Push
前执行Pull
不建议存在冲突。Reset
:回滚
Git的操作远不止如此,后面我们会基于命令行模式深入讲解Git,掌握了命令行操作,再回过头来使用IDEA就感觉很轻松了,对代码的掌控能力将会大大提升。
二、Git
Git是一个分布式版本管理软件,不需要服务端的支持就能完成工作,基于仓库设计思想,每次提交的版本直接记录快照,而不是记录差异。Git 更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。
后面讲解如何配置并初始化一个仓库(repository)、开始或停止跟踪(track)文件、暂存(stage)或提交(commit)更改。本章也将向你演示如何配置 Git 来忽略指定的文件和文件模式、如何迅速而简单地撤销错误操作、如何浏览你的项目的历史版本以及不同提交(commits)间的差异、如何向你的远程仓库推送(push)以及如何从你的远程仓库拉取(pull)文件。
1. 版本管理
在进行后面的操作,我们要提前做一些前面讲到的配置,如果已配置,请忽略:
# 配置用户 $ git config --global user.name "AiJiangnan" # 配置用户邮箱 $ git config --global user.email "904629998@qq.com" # 查看配置 $ git config -l
1.1 创建仓库
1.1.1 在现有目录中初始化仓库
进入目录执行下面命令:
$ git init
执行后会在目录下创建一个.git
的子目录,这个目录包含所有初始化的Git仓库所有必须的文件。
1.1.2 克隆已有的仓库
已有的仓库地址或者Github上的地址:git@github.com:AiJiangnan/spring-boot.git
$ git clone git@github.com:AiJiangnan/spring-boot.git
你想从公司远程仓库克隆项目或者想为某个开源项贡献自己的一份力,就可以用此命令创建仓库。
1.2 提交代码
1.2.1 文件的生命周期
在工作目录下文件的状态:
- 未跟踪(
untracked
):没有纳入版本控制的文件- 已跟踪(
tracked
):已经纳入版本控制的文件- 未修改(
unmodified
):没有修改的文件- 已修改(
modified
):已经修改的文件- 已暂存(
staged
):已经放入暂存区的文件
已跟踪文件在上一次快照中有它们的记录,工作一段时间后,它们的状态可以是未修改、已修改或已暂存。未跟踪文件既不存在于快照记录中,也没有放入暂存区。克隆方式创建的仓库,工作目录中所有文件都是已跟踪文件,并处于未修改状态。编辑过某些文件之后,由于自上次提交你对它们做了修改,Git将它们标记为已修改文件。我们逐步将这些修改过的文件放入暂存区,然后提交所有暂存了的修改,如此反复。使用 Git 时文件的生命周期如下:
1.2.2 检查当前文件状态
创建一个目录并通过前面讲的git init
初始化仓库,在当前仓库下操作。
# 初始化仓库 $ git init 已初始化空的 Git 仓库于 /home/ajn/Code/csdn/git/.git/ # 查看哪些文件处于什么状态 $ git status 位于分支 master 尚无提交 无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪)
从上面命令返回结果我们知道当前没有提交,创建一个README.txt
文件:
$ echo 'Hello world' > README.txt $ git status 位于分支 master 尚无提交 未跟踪的文件: (使用 "git add <文件>..." 以包含要提交的内容) README.txt 提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
返回结果为当前没有提交,并且存在未跟踪的文件README.txt
,未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件,Git 不会自动将之纳入跟踪范围,除非你手动添加跟踪,所以你不必担心不想被跟踪的文件包含进来。
1.2.3 跟踪新文件
# 跟踪新文件 $ git add README.txt $ git status 位于分支 master 尚无提交 要提交的变更: (使用 "git rm --cached <文件>..." 以取消暂存) 新文件: README.txt
从返回结果我们可知,该文件已跟踪,并且处于已暂存状态。此时提交,该文件此时此刻的版本就会被保留在历史记录中。
git add
后面参数可以是文件或目录,如果是目录,会递归跟踪该目录下所有的文件。
1.2.4 暂存已修改文件
如果你修改了一个已跟踪的文件CONTRIBUTING.txt
,然后查看状态:
$ git status 位于分支 master 要提交的变更: (使用 "git reset HEAD <文件>..." 以取消暂存) 新文件: README.txt 尚未暂存以备提交的变更: (使用 "git add <文件>..." 更新要提交的内容) (使用 "git checkout -- <文件>..." 丢弃工作区的改动) 修改: CONTRIBUTING.txt
从结果可知,说明已跟踪文件的内容发生了变化,但还没有放到暂存区。要暂存这次更新,需要运行git add
命令。这是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。将这个命令理解为“添加内容到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。现在让我们运行git add
将CONTRIBUTING.txt
放到暂存区,然后再看看git status
的输出:
$ git add CONTRIBUTING.txt $ git status 位于分支 master 要提交的变更: (使用 "git reset HEAD <文件>..." 以取消暂存) 修改: CONTRIBUTING.txt 新文件: README.txt
现在两个文件都已暂存,下次提交时就会一并记录到仓库。假设此时,你想要再修改 CONTRIBUTING.txt
里的内容,重新编辑保存后,准备好提交。再运行 git status
看看:
$ vim CONTRIBUTING.txt $ git status 位于分支 master 要提交的变更: (使用 "git reset HEAD <文件>..." 以取消暂存) 修改: CONTRIBUTING.txt 新文件: README.txt 尚未暂存以备提交的变更: (使用 "git add <文件>..." 更新要提交的内容) (使用 "git checkout -- <文件>..." 丢弃工作区的改动) 修改: CONTRIBUTING.txt
现在CONTRIBUTING.txt
文件同时出现在暂存区和非暂存区。实际上Git只不过暂存了你运行git add
命令时的版本,如果你现在提交,CONTRIBUTING.txt
的版本是你最后一次运行git add
命令时的那个版本,而不是你运行git commit
时,在工作目录中的当前版本。所以,运行了git add
之后又作了修订的文件,需要重新运行git add
把最新版本重新暂存起来:
$ git add CONTRIBUTING.txt $ git status 位于分支 master 要提交的变更: (使用 "git reset HEAD <文件>..." 以取消暂存) 修改: CONTRIBUTING.txt 新文件: README.txt
git add
使用参数-u
可以只暂存已跟踪的文件。
1.2.5 状态简览
# git status -s 或 git status --short $ git status -s M CONTRIBUTING.txt A README.txt
状态符号含义:
??
:未跟踪文件A
:新添加到暂存区中的文件M
:修改过的文件MM
:左侧的表示该文件被修改并已放入暂存区,右侧表示文件被修改没放入暂存区
1.2.6 忽略文件
一般我们总会有些文件无需纳入Git的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。在这种情况下,我们可以创建一个名为.gitignore
的文件,列出要忽略的文件模式。来看一个实际的例子:
# no .a files *.a # but do track lib.a, even though you're ignoring .a files above !lib.a # only ignore the TODO file in the current directory, not subdir/TODO /TODO # ignore all files in the build/ directory build/ # ignore doc/notes.txt, but not doc/server/arch.txt doc/*.txt # ignore all .pdf files in the doc/ directory doc/**/*.pdf
文件.gitignore
的格式规范如下:
- 所有空行或者以
#
开头的行都会被忽略。- 可以使用标准的glob模式匹配。
- 匹配模式可以以(
/
)开头防止递归。- 匹配模式可以以(
/
)结尾指定目录。- 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(
!
)取反。所谓的glob模式是指shell所使用的简化了的正则表达式。
- 星号(
*
)匹配零个或多个任意字符;[abc]
匹配任何一个列在方括号中的字符;- 问号(
?
)只匹配一个任意字符;- 如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如
[0-9]
表示匹配所有0到9的数字);- 使用两个星号(
*
) 表示匹配任意中间目录,比如a/**/z
可以匹配a/z
,a/b/z
或a/b/c/z
等。
1.2.7 查看已暂存和未暂存的修改
假如再次修改README.txt
文件后暂存,然后编辑CONTRIBUTING.txt
文件后先不暂存,运行status
命令将会看到:
$ git status 位于分支 master 要提交的变更: (使用 "git reset HEAD <文件>..." 以取消暂存) 修改: README.txt 尚未暂存以备提交的变更: (使用 "git add <文件>..." 更新要提交的内容) (使用 "git checkout -- <文件>..." 丢弃工作区的改动) 修改: CONTRIBUTING.txt # 查看尚未暂存的文件更新了哪些部分 $ git diff diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index afce56f..d4ef1b9 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -1,3 +1,2 @@ Contributing Contributing -Contributing # 查看已暂存的将要添加到下次提交里的内容 $ git diff --cached diff --git a/README.txt b/README.txt index 802992c..57bf5a7 100644 --- a/README.txt +++ b/README.txt @@ -1 +1,2 @@ Hello world +Hello world
我们使用 git diff
来分析文件差异。如果你喜欢通过图形化的方式或其它格式输出方式的话,可以使用git difftool
命令来用 Araxis ,emerge 或 vimdiff 等软件输出 diff 分析结果。使用 git difftool --tool-help
命令来看你的系统支持哪些 Git Diff 插件。
1.2.8 提交更新
现在的暂存区域已经准备妥当可以提交了。在此之前,确认还有什么修改过的或新建的文件还没有 git add
过,否则提交的时候不会记录这些还没暂存起来的变化。这些修改过的文件只保留在本地磁盘。所以,每次准备提交前,先用 git status
看下,是不是都已暂存起来了, 然后再运行提交命令:
$ git commit # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: #new file: README #modified: CONTRIBUTING.md # ~ ~ ~ ".git/COMMIT_EDITMSG" 9L, 283C
使用git config --global core.editor
命令设定你喜欢的编辑软件,这里用的是vim。
另外,你也可以在commit
命令后添加 -m
选项,将提交信息与命令放在同一行,如下所示:
$ git commit -m 'update master' [master 52a10fa] update master 2 files changed, 1 insertion(+), 1 deletion(-)
1.2.9 跳过使用暂存区域
$ git commit -a -m 'update master'
1.2.10 移除文件
$ git rm PROJECTS.md # 取消跟踪文件,从git仓库中移除而不从硬盘上删除 $ git rm --cached PROJECTS.md
1.2.11 移动文件
$ git mv file_from file_to
1.3 查看日志
# 查看所有提交日志 $ git log commit 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master) Author: AiJiangnan <904629998@qq.com> Date: Wed Dec 5 18:48:41 2018 +0800 update master commit 0907ee43bf485a05492e84bf7a22c4174e2f9df3 Author: AiJiangnan <904629998@qq.com> Date: Wed Dec 5 18:08:46 2018 +0800 update commit f3f5b5be459610325085ad0183f330576a0742d8 Author: AiJiangnan <904629998@qq.com> Date: Wed Dec 5 12:01:41 2018 +0800 add CONTRIBUTING.txt # 参数:-p 查看每次提交的内容差异,-1 显示最近1次提交的日志 $ git log -p -1 commit 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master) Author: AiJiangnan <904629998@qq.com> Date: Wed Dec 5 18:48:41 2018 +0800 update master diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index afce56f..d4ef1b9 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -1,3 +1,2 @@ Contributing Contributing -Contributing diff --git a/README.txt b/README.txt index 802992c..57bf5a7 100644 --- a/README.txt +++ b/README.txt @@ -1 +1,2 @@ Hello world +Hello world # 参数:--stat 查看每次提交简略的统计 $ git log --stat commit 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master) Author: AiJiangnan <904629998@qq.com> Date: Wed Dec 5 18:48:41 2018 +0800 update master CONTRIBUTING.txt | 1 - README.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) # 参数:--pretty 格式化输出日志 默认格式:(onelone,short,full,fuller) $ git log --pretty=oneline 52a10fad34dd38fdf0099e5c52cda526a0c1cd57 (HEAD -> master) update master 0907ee43bf485a05492e84bf7a22c4174e2f9df3 update f3f5b5be459610325085ad0183f330576a0742d8 add CONTRIBUTING.txt # 参数:--pretty 格式化输出日志 自定义格式 $ git log --pretty=format:"%h - %an, %ar : %s" 52a10fa - AiJiangnan, 2 天前 : update master 0907ee4 - AiJiangnan, 2 天前 : update f3f5b5b - AiJiangnan, 2 天前 : add CONTRIBUTING.txt
选项 | 说明 |
%H |
提交对象(commit)的完整哈希字串 |
%h |
提交对象的简短哈希字串 |
%T |
树对象(tree)的完整哈希字串 |
%t |
树对象的简短哈希字串 |
%P |
父对象(parent)的完整哈希字串 |
%p |
父对象的简短哈希字串 |
%an |
作者(author)的名字 |
%ae |
作者的电子邮件地址 |
%ad |
作者修订日期(可以用 --date= 选项定制格式) |
%ar |
作者修订日期,按多久以前的方式显示 |
%cn |
提交者(committer)的名字 |
%ce |
提交者的电子邮件地址 |
%cd |
提交日期 |
%cr |
提交日期,按多久以前的方式显示 |
%s |
提交说明 |
# 参数:--graph 使用字符图形化展示分支,合并历史 $ git log --pretty=format:"%h %s" --graph * 2d3acf9 ignore errors from SIGCHLD on trap * 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit |\ | * 420eac9 Added a method for getting the current branch. * | 30e367c timeout code and tests * | 5a09431 add timeout protection to grit * | e1193f8 support for heads with slashes in them |/ * d6016bc require time for xmlschema * 11d191e Merge branch 'defunkt' into local
选项 | 说明 |
-p |
按补丁格式显示每个更新之间的差异。 |
--stat |
显示每次更新的文件修改统计信息。 |
--shortstat |
只显示 --stat 中最后的行数修改添加移除统计。 |
--name-only |
仅在提交信息后显示已修改的文件清单。 |
--name-status |
显示新增、修改、删除的文件清单。 |
--abbrev-commit |
仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。 |
--relative-date |
使用较短的相对时间显示(比如,“2 weeks ago”)。 |
--graph |
显示 ASCII 图形表示的分支合并历史。 |
--pretty |
使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。 |
--oneline |
--pretty=oneline 的简写 |
1.3.1 限制输出长度
# 参数:--since 配置日志输出的起始时间 $ git log --since=2.weeks # 参数:-S 搜索修改了某些特定内容的日志 $ git log -Sfunction_name
选项 | 说明 |
-(n) |
仅显示最近的 n 条提交 |
--since , --after |
仅显示指定时间之后的提交。 |
--until , --before |
仅显示指定时间之前的提交。 |
--author |
仅显示指定作者相关的提交。 |
--committer |
仅显示指定提交者相关的提交。 |
--grep |
仅显示含指定关键字的提交 |
-S |
仅显示添加或移除了某个关键字的提交 |
1.4 回滚代码
1.4.1 重置原理
树 | 用途 |
HEAD | 上一次提交的快照 |
Index(索引) | 预期的下一次提交的快照(暂存区域) |
Working Directory | 工作目录 |
工作流程
1.4.2 撤消操作
# 前一次提交中追加修改 $ git commit -m "update README.txt" $ git add CONTRIBUTING.txt $ git commit --amend # 取消暂存的文件 $ git add * $ git status -s M CONTRIBUTING.txt M README.txt $ git reset HEAD CONTRIBUTING.txt 重置后取消暂存的变更: MCONTRIBUTING.txt $ git status -s M CONTRIBUTING.txt M README.txt # 撤消对文件修改 $ git checkout -- CONTRIBUTING.txt $ git status -s M README.txt
1.4.3 回滚操作
# 移动 HEAD (参数:--soft,回滚到commit操作前) $ git reset --soft HEAD~ $ git status -s M CONTRIBUTING.txt # 更新索引 (参数:--mixed(默认),回滚到add操作前) $ git reset --mixed HEAD~ 重置后取消暂存的变更: MCONTRIBUTING.txt $ git status -s M CONTRIBUTING.txt # 更新工作目录 (参数:--hard,回滚到修改前) $ git reset --hard HEAD~ HEAD 现在位于 52a10fa update master $ git status -s $ git log --oneline 52a10fa (HEAD -> master) update master 0907ee4 update f3f5b5b add CONTRIBUTING.txt # 回滚并将回滚的操作当作一次提交 $ git revert 0907ee4
Git中任何已提交的东西都可以恢复,然而你未提交的东西丢失后可能再也找不回来了。回滚中定位版本可以用指针HEAD~
或者版本哈希码0907ee4
来指定,例如:前一个版本HEAD~
,前两个版本HEAD~2
。
1.5 数据恢复
- 删除了正在工作的分支
- 回滚了你想要的提交
当你遇到上面两种情况:
# 查看日志并回滚到前三次提交 $ git log --oneline ac705e4 (HEAD -> master) update 4f9a890 update 52a10fa update master 0907ee4 update f3f5b5b add CONTRIBUTING.txt $ git reset --hard 0907ee4 HEAD 现在位于 0907ee4 update $ git log --oneline 0907ee4 (HEAD -> master) update f3f5b5b add CONTRIBUTING.txt # 查看操作日志恢复数据版本 $ git reflog -5 0907ee4 (HEAD -> master) HEAD@{0}: reset: moving to 0907ee4 ac705e4 HEAD@{1}: commit: update 4f9a890 HEAD@{2}: reset: moving to 4f9a890 52a10fa HEAD@{3}: reset: moving to 52a10fa 52a10fa HEAD@{4}: checkout: moving from dev to master $ git reset --hard ac705e4 HEAD 现在位于 ac705e4 update $ git log --oneline ac705e4 (HEAD -> master) update 4f9a890 update 52a10fa update master 0907ee4 update f3f5b5b add CONTRIBUTING.txt