SVN迁移到Git:保持提交记录

1.前言

最近组里要把之前在 SVN 上管理的项目迁移到 SVN 上去,所以花了一个晚上研究了一下 SVN 向 Git 迁移的方法。这里首先给出一种能保存提交记录的方案。

2.实施准备

再实施迁移之前,有几件事情是必须做的:

  1. 获取 SVN 路径权限
  2. 将本机的公钥提交到 Git 服务器上
  3. 在 Git 服务器上建立仓库

3.实施过程

实施的过程分为以下几个步骤:

  1. 将 SVN checkout 到本地
  2. 获取用户名列表
  3. 用 git svn clone 创建 Git 仓库
  4. 导出 .gitignore 文件
  5. 调整分支和标签
  6. 推送到远程

a. 将 SVN checkout 到本地

svn checkout --username USER_NAME --password PASSWORD https://svn.xxx/dir_name  dir_name

b. 获取用户名列表

通过如下命令获得用户列表:

svn log --xml | grep -P "^<author" | sort -u | perl -pe 's/<author>(.*?)<\/author>/$1 = /'

然后按照如下格式放入一个文件中(比如 user.txt):

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

c. 用 git svn clone 创建 Git 仓库

git svn clone --no-metadata -A users.txt https://svn.xxx/dir_name  dir_name

d. 导出 .gitignore 文件

git svn show-ignore > .gitignore
git add .gitignore
git commit -m 'Convert svn:ignore properties to .gitignore.'

e. 调整分支和标签

首先要移动标签,把它们从奇怪的远程分支变成实际的标签,然后把剩下的分支移动到本地。

cp -Rf .git/refs/remotes/tags/* .git/refs/tags/
rm -Rf .git/refs/remotes/tags

接下来,把 refs/remotes 下面剩下的索引变成本地分支:

cp -Rf .git/refs/remotes/* .git/refs/heads/
rm -Rf .git/refs/remotes

f. 推送到远程

git remote add origin git@my-git-server:myrepository.git
git push origin --all

4. Python 脚本

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import sys
import commands

if len(sys.argv) < 3:
    print 'Usage: python %s USERNAME PASSWORD' % sys.argv[0]
    exit(255)

user_name = sys.argv[1]
user_pwd  = sys.argv[2]

svn_repos = [
        svn_path,
        ......
        ]

git_repos = [
        git_path,
        ......
        ]

users = {}

def init():
    run_cmd('.', 'mkdir git')
    run_cmd('.', 'mkdir svn')

def run_cmd(dir_name, cmd):
    cmd_str = 'cd %s && %s' % (dir_name, cmd)
    print cmd_str
    status,output = commands.getstatusoutput(cmd_str)
    print 'status: %d' % status
    print 'output: \n%s' % output
    print '=========================================================\n'

    return status,output

def clone_svn_to_local(svn_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    run_cmd('svn', "svn checkout --username %s --password %s %s %s" % (user_name, user_pwd, svn_addr, repo_dir_name))
    _,output = run_cmd('svn/' + repo_dir_name, "svn log --xml | grep -P '^<author' | sort -u | perl -pe 's/<author>(.*?)<\/author>/$1 = /'")
    lines = output.split("\n")
    for line in lines:
        name = line.split('=')[0].strip()
        users[name] = ('%s = %s <%s@didichuxing.com>' % (name, name, name))

def write_users():
    with open('git/users.txt', 'w+') as f:
        for name in users.values():
            f.write('%s\n' % name)

def git_svn_clone(svn_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    run_cmd('git', 'git svn clone --no-metadata -A users.txt %s %s' % (svn_addr, repo_dir_name))
    run_cmd('git/' + repo_dir_name, 'git svn show-ignore > .gitignore')
    run_cmd('git/' + repo_dir_name, 'git add .gitignore')
    run_cmd('git/' + repo_dir_name, "git commit -m 'Convert svn:ignore properties to .gitignore.'")
    run_cmd('git/' + repo_dir_name, 'cp -Rf .git/refs/remotes/tags/* .git/refs/tags/')
    run_cmd('git/' + repo_dir_name, 'rm -Rf .git/refs/remotes/tags')
    run_cmd('git/' + repo_dir_name, 'cp -Rf .git/refs/remotes/* .git/refs/heads/')
    run_cmd('git/' + repo_dir_name, 'rm -Rf .git/refs/remotes')

def diff_git_and_svn_repos(svn_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    git_repo_path = 'git/' + repo_dir_name
    svn_repo_path = 'svn/' + repo_dir_name
    _,output = run_cmd('.', 'diff -r %s %s' % (git_repo_path, svn_repo_path))
    expected = 'Only in git/%s: .git\nOnly in git/%s: .gitignore\nOnly in svn/%s: .svn' % (repo_dir_name, repo_dir_name, repo_dir_name)
    if expected != output:
        raise Exception('迁移后内容不一致:\n%s' % output)

def push_to_remote(svn_addr, git_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    run_cmd('git/' + repo_dir_name, 'git checkout -b svn_migrate_branch')
    run_cmd('git/' + repo_dir_name, 'git remote add origin %s' % git_addr)
    run_cmd('git/' + repo_dir_name, 'git push origin --all')

def get_repo_dir_name(svn_addr):
    return svn_addr.split('/')[-1]

def main():
    init()

    for svn_addr in svn_repos:
        clone_svn_to_local(svn_addr)

    write_users()

    repo_nums = len(git_repos)
    for i in range(0,repo_nums):
        git_svn_clone(svn_repos[i])
        diff_git_and_svn_repos(svn_repos[i])
        push_to_remote(svn_repos[i], git_repos[i])

if __name__ == '__main__':
    main()

4. 参考文献

5. 一点想法

其实大家都能看出来这个脚本大部分都是对 shell 命令的调用。为什么舍近求远呢?
因为本人一直觉得 shell 的内部命令,关于流程控制以及字符串处理等实在不好用。我更倾向于 Python 和 Ruby 这样强大的脚本语言。
其实我对 Python 用的也不是很多,在写脚本的过程中查阅了一些 Python 调用 shell 的资料,也和 Ruby 中类似的代码做个比较。
在上面的脚本中我使用了 commands 包的 getstatusoutput('cmd') 方法 。该函数返回了一个元组,通过序列解包可以同时获得返回值和标准输出流的输出。一般调用方法为:

(status, output) = commands.getstatusoutput('shell 命令')
print status, output

而 Ruby 中也有类似的方法,即 Kernel 模块中的 `cmd` 方法。但是该方法直接返回的只有标准输出,返回值还要靠 $? 来获取。

irb(main):014:0> `echo hello && exit 99`
=> "hello\n"
irb(main):015:0> $?.exitstatus
=> 99

在便利性上,Python 的序列解包使得多个函数返回值变成可能,减少了封装也提高了使用的便利性。

Spring MVC 中的 url 问题

这两天在学习 Spring MVC 的时候与到了一个关于 url 的问题让我很困惑,查找资料和同学讨论没找出一个结论。现在先这里 Mark 一下,等找出原因以后再补完这篇文章。

1. 问题描述

首先上一个表单:

<form:form commandName="book" action="book_update" method="post">
<fieldset>
  <legend>Add a book</legend>
  <form:hidden path="id"/>
    <p>
      <label for="category">Category</label>
      <form:select id="category" path="category.id" items="${categories}"
                   itemLabel="name" itemValue="id"></form:select>
    </p>
    <p>
      <label for="title">Title</label>
      <form:input id="title" path="title" />
    </p>
    <p>
      <label for="author">Author</label>
      <form:input id="author" path="author" />
    </p>
    <p>
      <label for="isbn">ISBN</label>
      <form:input id="isbn" path="isbn" />
    </p>
    <p id="buttons">
      <input id="reset" type="reset" tabindex="4" />
      <input id="submit" type="submit" tabindex="5" 
             value="Update Book" />
    </p>
</fieldset>
</form:form>

这个表单在我访问 http://127.0.0.1:8080/book-demo/book_edit/{id} 时会展示。简单来说,这个 url 的格式可以表示为:

http://hostname[:端口号]/context/action/{id}

在这里 hostname[:端口号] 即为 127.0.0.1:8080context 为 webapp 的名字 book-demo,而后面的 action 则使用了路径变量。

从上面的表单代码可以看到表单提交对应的 actionbook_update。这个表单提交的时候,我预期的 url 是:

http://127.0.0.1:8080/book-demo/book_update

但实际表单提交后访问的 action 为:

http://127.0.0.1:8080/book_demo/book_edit/book_update

也就是说程序把 /book_demo/book_edit 作为了 context,把路径变量作为了 action

2. 尝试的解决方案

显然,错误出在访问 action 的时候产生了错误的 url,所以针对这个错误原因我尝试了两种解决方案:

1) action=”/book_update”

我把表单的 action 改了一下,结果产生的 url 是:

http://127.0.0.1:8080/book_update

显然连 url 的 context 都丢了,action 中带了 / 之后有了一点根路径的意思。

2) action=”/book-demo/book_update”

结果正确,获得了我想要的 url 。

3. 思考

a. 关于 url 产生机制

这个问题不禁引人思考 url 路径产生的机制。

之前做过 ruby on rails 的开发,作为对比:tomcat 运行应用的时候都会带上这个应用的名称,也就构成了第一级的上下文。对比 ruby on rails 在运行应用的时候 url 不会带上应用名称。因此,这个表单在 ruby on rails 中使用不会有问题,但是在这里就会有!

从绝对路径与相对路径的角度来看:首先 action=”book_update” 的写法相当于使用相对路径,把 /book_demo/book_edit 当做了上一级路径。而 action=”/book_update” 以及 action=”/book-demo/update” 则属于绝对路径的范畴。

b. 关于 RESTful 的 url

上面的表单其实并不符合 RESTful 的风格(我觉得 rails 提倡的 RESTful 风格的 url 很是优雅,之前做 rails 的时候一直想写写来着)。比如更新一本书的信息,对应的 action 可以叫 update,路径可以映射为 /books/{id}/update。上文的表单是把书本的 id 信息作为一个隐藏域来传递,虽然可行但绝对不是一个良好的风格。这里附一下 RESTful 风格的路由:
Rails:

url HTTP 方法 对应的 action 操作说明
/books GET list 获取所有书籍的信息
/books/new GET new 返回新增书籍的页面
/books POST create 新增一本书籍
/books/{id} GET show 显示某特定id书本的详细信息
/books/{id}/edit GET edit 返回编辑书本信息的页面
/books/{id} PUT/PATCH update 更新书本信息
/books/{id} DELETE destroy 删除书本信息

Rails 的 HTTP 方法使用了一些小技巧来实现 PUT/PATCH 和 DELETE 方法。通过在表单内增加一个隐藏域来实现:

<input type="hidden" name="_method" value="put">

对应地,我们可以借鉴并稍作修改在 Spring MVC 中应用:

url HTTP 方法 对应的 action 操作说明
/books GET list 获取所有书籍的信息
/books/new GET new 返回新增书籍的页面
/books/create POST create 新增一本书籍
/books/{id} GET show 显示某特定id书本的详细信息
/books/{id}/edit GET edit 返回编辑书本信息的页面
/books/{id}/update POST update 更新书本信息
/books/{id}/destroy POST destroy 删除书本信息

Rails 事务

Rails 中的事务

Rails 的事务使用方法有两种:

第一种使用方法:直接使用 ActiveRecord::Base 中的事务,如:

ActiveRecord::Base.transaction do
  或许会产生异常的处理
end
  不产生异常时的处理
rescue => e
  产生异常时的处理

第二种,使用模型的事务。由于模型继承了 ActiveRecord::Base,所以模型也包含了事务:

模型.transaction do
  或许会产生异常的处理
end
  不产生异常时的处理
rescue => e
  产生异常时的处理

Rails 中多表事务

Rails 在使用事务时经常会涉及多张表。比如一个 Release 的实例会关联多个 WorkOrder 实例,那么在事务中先存储 Release,然后存储 WorkOrder 时触发异常(比如 redmine_id 的唯一性约束):

Release.transaction do
  release = Release.create
  release.work_orders.create!(redmine_id:1)
  release.work_orders.create!(redmine_id:1)
end

经实验,ReleaseWorkOrder 都会回滚。

Ruby Mechanize 使用小结

1. Mechanize 简介

The Mechanize library is used for automating interaction with websites. Mechanize automatically stores and sends cookies, follows redirects, and can follow links and submit forms. Form fields can be populated and submitted. Mechanize also keeps track of the sites that you have visited as a history.

简单来说:

Mechanize 是一个用来自动化和网站交互的类库。Mechanize 有着自动存储和发送 cookies、跟踪重定向和链接以及提交表单的功能。Mechanize 可以产生和提交表单的域。Mechanize 还能够记录你访问的历史记录。

2. Mechanize 简单使用

使用 Mechanize 需要 require 一下:

require 'mechanize'

2.1 Mechanize 获取页面

我们首先获取一个浏览器的代理:

@agent = Mechanize.new

我们可以对这个代理进行设置,伪装成各种主流的浏览器(此处不表)。然后通过 URL 获取指定页面:

@page = @agent.get('www.github.com')

2.2 表单操作

2.2.1 搜索框

@agent = Mechanize.new
@page = @agent.get('xxx.xxx.com')
form = @page.forms.first
form["q"] = "xxxxx"
result = form.submit

上述代码中使用 @page.forms.first 获取了第一个表单,在页面中包含多个表单时,可以使用 @page.form_with(action: "search") 这样的方法获取特定的表单。

2.2.2 登录表单

@agent = Mechanize.new
@page = @agent.get('xxx.xxx.com')
form = @page.form_with(action: "login")
form["username"] = "username"
form["password"] = "password"
result = form.submit

form 使用 hash 的形式进行参数的赋值,表单中的每个元素用 name 属性来获取。

2.3 获取页面中的元素

从页面中获取元素常见的方法是 @page.links 或者 @page.forms 这样的内建方法。但是要从页面中获取某个特定标签时,可以使用 XPath 来实现:

@page.parser.xpath("//div[name='content']/p[id='id']")

上述语句获取的是页面中一个 name 属性值为 contentdiv 标签下的 id 属性为 idp 标签。XPath 的具体使用方法请看参考文献。

但是偶尔也会碰上你想获取的元素既没有 id 也没有 name 属性的情况,这个时候只能靠类似下文的方法暴力匹配了:

link.children[0].children[2].children[1].text

2.4 POST 数据到指定 URI

Mechanize 不支持 Javascript,对于某些需要 Javascript 支持的表单,Mechanize 无法帮你获取到数据。但是我们可以直接发出 POST 请求。
一般来说,我们可以先使用 Firefox 或者 Chrome 浏览器查看执行 POST 请求时发送的数据中参数的名称、格式以及地址。然后将其作为模板,用我们实际使用的参数替换模板中的参数。以下就是一个例子:

post_url = "http://address/to/your/target/action"

date = Time.now.strftime('%Y-%m-%d')
release_params ={
  "issue[tracker_id]" => "9",
  "issue[subject]" => "[xx上线任务] #{date}",
  "issue[description]" => "#{description}",
  "issue[project_id]" => "46",
  "issue[assigned_to_id]" => "642",
  "issue[status_id]" => "1",
  "issue[priority_id]" => "13",
  "issue[custom_field_values][30]" => "642",
  "issue[custom_field_values][37]" => "xxxx"
}
@agent.post(post_url, release_params)

3. 参考文献

RSpec 测试——2. RSpec Expectations

RSpec::Expectations 允许你表达一个样例中对象的预期结果。

1. 内建匹配器

1.1 相等

expect(actual).to eq(expected)  # passes if actual == expected
expect(actual).to eql(expected) # passes if actual.eql?(expected)
expect(actual).not_to eql(not_expected) # passes if not(actual.eql?(expected))

注意:新的 expect 语法不再支持 == 匹配器。

1.2 身份标识

expect(actual).to be(expected)    # passes if actual.equal?(expected)
expect(actual).to equal(expected) # passes if actual.equal?(expected)

1.3 比较

expect(actual).to be >  expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be <  expected
expect(actual).to be_within(delta).of(expected)

1.4 正则表达式

expect(actual).to match(/expression/)

1.5 类型

expect(actual).to be_an_instance_of(expected) # passes if actual.class == expected
expect(actual).to be_a(expected)              # passes if actual.is_a?(expected)
expect(actual).to be_an(expected)             # an alias for be_a
expect(actual).to be_a_kind_of(expected)      # another alias

1.6 真假

expect(actual).to be_truthy   # passes if actual is truthy (not nil or false)
expect(actual).to be true     # passes if actual == true
expect(actual).to be_falsy    # passes if actual is falsy (nil or false)
expect(actual).to be false    # passes if actual == false
expect(actual).to be_nil      # passes if actual is nil
expect(actual).to_not be_nil  # passes if actual is not nil

1.7 预期错误

expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")

1.8 预期抛出

expect { ... }.to throw_symbol
expect { ... }.to throw_symbol(:symbol)
expect { ... }.to throw_symbol(:symbol, 'value')

1.9 断言匹配

expect(actual).to be_xxx         # passes if actual.xxx?
expect(actual).to have_xxx(:arg) # passes if actual.has_xxx?(:arg)

1.10 范围(Ruby >= 1.9)

expect(1..10).to cover(3)

1.11 集合

expect(actual).to include(expected)
expect(actual).to start_with(expected)
expect(actual).to end_with(expected)

expect(actual).to contain_exactly(individual, items)
# ...which is the same as:
expect(actual).to match_array(expected_array)

2 should 和 should_not 内建匹配器

2.1 相等

actual.should     eq(expected)  # passes if actual == expected
actual.should     == expected   # passes if actual == expected
actual.should_not eql(expected) # passes if actual.eql?(expected)

注意:我们推荐 eq 匹配器而不是 == 以避免 Ruby 的警告。

2.2 身份标识

actual.should     be(expected)    # passes if actual.equal?(expected)
actual.should_not equal(expected) # passes if actual.equal?(expected)

2.3 比较

actual.should be >  expected
actual.should be >= expected
actual.should be <= expected
actual.should be <  expected
actual.should be_within(delta).of(expected)

2.4 正则表达式

actual.should match(/expression/)
actual.should =~ /expression/

2.5 类型

actual.should     be_an_instance_of(expected)
actual.should_not be_a_kind_of(expected)

2.6 真假

actual.should be_true  # passes if actual is truthy (not nil or false)
actual.should be_false # passes if actual is falsy (nil or false)
actual.should be_nil   # passes if actual is nil

2.7 断言

actual.should     be_xxx         # passes if actual.xxx?
actual.should_not have_xxx(:arg) # passes if actual.has_xxx?(:arg)

2.8 范围(Ruby >= 1.9)

(1..10).should cover(3)

2.9 集合

actual.should include(expected)
actual.should start_with(expected)
actual.should end_with(expected)

RSpec 测试——1. RSpec Core

1. 基本结构

RSpec.describe Order do
  it "sums the prices of its line items" do
    #something...
  end
end

2. 紧凑结构

RSpec.describe Order do
  context "with no items" do
    it "behaves one way" do
      # ...
    end
  end

  context "with one item" do
    it "behaves another way" do
      # ...
    end
  end
end

3. 别名

对于顶层的 example group 来说应该用 describe 和 context,组群内可以使用 it、specify、example。
it、specify、example 是测试的基本单元,它们内部不能使用 before 语句块。before 语句块只能在 describe 和 context 内使用。

4. 共享的样例和上下文

RSpec.shared_examples "collections" do |collection_class|
  it "is empty when first created" do
    expect(collection_class.new).to be_empty
  end
end

RSpec.describe Array do
  include_examples "collections", Array
end

RSpec.describe Hash do
  include_examples "collections", Hash
end

在共享样例中几乎可以声明一切,before、after 以及 around 等钩子方法,let 声明以及紧凑结构等。
类似的用法还有 shared_context 以及 include_context。

5. 元数据

rspec-core 存储了一个包含所有样例和组群的元数据 hash。可以通过如下方法从样例中获取:

it "does something" do
  expect(example.metadata[:description]).to eq("does something")
end

或者:

RSpec.shared_examples "collections" do
  it "is empty when first created" do
    expect(described_class.new).to be_empty
  end
end

RSpec.describe Array do
  include_examples "collections"
end

RSpec.describe Hash do
  include_examples "collections"
end

6. RSpec 命令

获取帮助:rspec --help
以文档格式输出:rspec <spec_file> --format doc
以网页格式输出:rspec <spec_file> --format html
rspec 命令的参数储存到 .rspec 文件中来自动调用如:echo "--format doc" >> .rspec

Rails使用RSpec的配置

1. Gemfile 引入 RSpec

group :development, :test do
  gem 'sqlite3', '1.3.8'
  gem 'rspec-rails', '2.13.1'
end

group :test do
  gem 'selenium-webdriver', '2.35.1'
  gem 'capybara', '2.1.0'
  gem 'factory_girl_rails', '4.2.1'
end

2. Rails 初始化跳过默认的 Test::Unit

rails new <app> --skip-test-unit

3. Rails 设置使用 RSpec进行测试

rails generate rspec:install

结果是:

创建 .rspec 目录
创建 spec 目录
创建 spec/spec_helper.rb 文件

4. 把 CapybaraDSL 加入 RSpec

spec_helper.rb

RSpec.configure do |config|
  ...
  config.include Capybara::DSL
end

Ruby Rugged 使用简介

1. 初始化仓库

1.1 使用路径打开存在的仓库

repo = Rugged::Repository.new('path/to/my/repository')

1.2 创建一个新的仓库

Rugged::Repository.init_at('.', :bare)

1.3 根据子目录搜寻仓库

Rugged::Repository.discover("/Users/me/projects/repo/lib/subdir/")

2. 分支操作

2.1 创建分支

repo.create_branch('branch-name')

2.2 切换到某一个分支

repo.checkout('branch-name')

2.3 查找分支

根据某个关键字查找分支:

branch_names = []
last_commits = []

repo.branches.each do |branch|
  keywords.each do |keyword|
    if branch.name.include?(keyword)
      branch_names << branch.name
      last_commits << repo.branches[branch.name].target
    end
  end
end

branch_names 用来保存符合条件的分支名字,last_commits 用来保存这些分支的最后一次提交。

2.4 获取分支

branch = repo.branches[branch_name]

3. 增加对文件的追踪

3.1 增加对单个文件的追踪

repo.index.add('./b.txt') # path or object
commit_tree = repo.index.write_tree repo
repo.index.write

相当于 git add b.txt 的操作。注意:index.write 操作会同步暂存区和工作目录。在增加或者删除对文件的追踪后必须要执行,否则执行 git status 看不到预期的效果。疑问:commit_tree = repo.index.write_tree repo 的作用是获取提交时所需要的 tree 参数?

3.2 增加对多个文件的追踪

repo.index.add_all
repo.index.write

相当于 git add -A 操作。

4. 创建 commit

author = { email: "wuzhiyu@note4code.com",
           name: "wuzhiyu",
           time: Time.now }
options = {}
options[:author] = author
options[:commiter] = author
options[:message] = "commit message here"
options[:parents] = repo.empty? ? [] : [ repo.head.target ].compact
options[:tree] = commit_tree
options[:update_ref] = 'HEAD'
Rugged::Commit.create(repo, options)

5. 分支的合并

5.1 commit 合并分析

repo.merge_analysis(last_commits[index])

返回一个包含如下 symbol 组合的数组:

symbol description
:normal A “normal” merge is possible, both HEAD and the given commit have diverged from their common ancestor. The divergent commits must be merged.
:up_to_date The given commit is reachable from HEAD, meaning HEAD is up-to-date and no merge needs to be performed.
:fastforward The given commit is a fast-forward from HEAD and no merge needs to be performed. HEAD can simply be set to the given commit.
:unborn The HEAD of the current repository is “unborn” and does not point to a valid commit. No merge can be performed, but the caller may wish to simply set HEAD to the given commit.

5.2 commit 的合并

#git fetch:
remote = Rugged::Remote.lookup(repo, "origin")
remote.connect(:fetch) do |r|
  r.download
  r.update_tips!
end

#git merge:
merge_index = repo.merge_commits(
  Rugged::Branches.lookup(repo, "master").tip,
  Rugged::Branches.lookup(repo, "origin/master").tip
)
raise "Conflict detected!" if merge_index.conflicts?
merge_commit = Rugged::Commit.create(repo, {
  parents: [
    Rugged::Branches.lookup(repo, "master").tip,
    Rugged::Branches.lookup(repo, "origin/master").tip
  ],
  tree: merge_index.write_tree(repo),
  message: 'Merged `origin/master` into `master`',
  author:    { name: "User", email: "example@test.com" },
  committer: { name: "User", email: "example@test.com" },
  update_ref: 'master'
})

上文中使用了 merge_index.conflicts? 来测试是否有冲突。但是这种冲突检测方法仅限于检测目录的冲突,也就是说只要两个分支修改了同一个文件就会报冲突,但实际上修改的内容可能并不冲突。而使用合并命令则可以针对行来检测是否冲突。

6.参考

Ruby和Rails的安装与环境配置

1.RVM

RVM 是(Ruby Version Manager)的缩写,用来管理 Ruby 和 Rails 的开发环境。

1.1 RVM(Linux 下)可以通过如下命令来安装:

gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
\curl -sSL https://get.rvm.io | bash -s stable
source ~/.bashrc
source ~/.bash_profile

1.2 获取最新的 RVM:

rvm get stable

1.3 满足 RVM 所需的软件包依赖:

rvm requirements

按照提示输入管理员密码进行依赖的安装。

1.4 RVM 的使用

改用淘宝作为下载源:

sed -i 's!cache.ruby-lang.org/pub/ruby!ruby.taobao.org/mirrors/ruby!' $rvm_path/config/db
gem sources --remove https://rubygems.org/
gem sources -a https://ruby.taobao.org/

查看已安装的 Ruby:

rvm list

查看所有 Ruby 版本信息:

rvm list known

安装 Ruby:

rvm install 2.0.0 --with-openssl-dir=$HOME/.rvm/usr

卸载 Ruby:

rvm remove 2.0.0

2. gemset

gemset可以理解为是一个独立的虚拟gem环境,每一个gemset都是相互独立的。

2.1 建立 gemset:

rvm use 2.0.0
rvm gemset create rails4

或者如:

rvm use 2.0.0@rails4 --create --default

建立一个 gemset 并将其设为默认。
同样可以在项目的根目录下建立 .rvmrc 文件,并添加:

rvm use <ruby版本号>@<gemset名>

2.2 查看所有 gemset:

rvm gemset list

2.3 清空一个 gemset 中的所有 gem:

rvm gemset empty 2.0.0@rails4

2.4 删除一个 gemset:

rvm gemset delete rails4

2.5 升级到一个指定版本

gem update --system 2.1.9

2.6 创建 gem 配置文件

~/.gemrc 中配置不生成 ri 和 rdoc 文档

install: --no-rdoc --no-ri
update: --no-rdoc --no-ri

2.7 可能出现的问题

如果提示:

RVM is not a function, selecting rubies with ‘rvm use …’ will not work.

则执行:

source ~/.rvm/scripts/rvm 

如果你使用了 zsh 这个神器,那么你应该在 ~/.zshrc 文件中添加这么一行:

[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm"

3. Rails 的安装

gem install rails --version 4.0.2 --no-ri --no-rdoc