├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── doc ├── images │ ├── SmartMergeConf.jpg │ ├── SmartMergeFlow.jpg │ ├── WhatIsSmartMerge.jpg │ ├── branch_model.jpg │ └── conflict.jpg └── 分支集成加速器SmartMerge.md ├── lib ├── generators │ └── smart_merge │ │ ├── install_generator.rb │ │ └── templates │ │ ├── create_smart_merge_settings.rb │ │ └── smart_merge_setting.rb ├── smart_merge.rb └── smart_merge │ ├── base_service.rb │ ├── check_service.rb │ ├── create_service.rb │ ├── destroy_service.rb │ ├── merge_service.rb │ ├── trigger_service.rb │ ├── update_service.rb │ └── version.rb ├── smart_merge.gemspec └── spec ├── smart_merge_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.3 5 | before_install: gem install bundler -v 1.14.6 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in smart_merge.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartMerge 2 | 3 | Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/smart_merge`. To experiment with that code, run `bin/console` for an interactive prompt. 4 | 5 | TODO: Delete this and the text above, and describe your gem 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'smart_merge', git: 'https://github.com/gitlab-extra/smart_merge' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | And then execute: 20 | 21 | $ bundle exec rails generate smart_merge:install 22 | $ bundle exec rake db:migrate 23 | 24 | ## Usage 25 | 26 | Assume your project's repository has multiple branches of master,test1,test2,test3, you can run the command to create a smart_merge_setting: 27 | 28 | $ params = {"target_branch"=>"LM/light", "base_branch"=>"master", "source_branches"=>["test3", "test1", "test2"], "auto_merge"=>true} 29 | $ sm = SmartMerge::CreateService.new(project: your_project, user: you, params: params).execute 30 | 31 | And if have conflicts between the base_branch and source_branches, you can see the conflicts by: 32 | 33 | $ sm.conflicts 34 | $ => [{:branches=>["master", "test2"], :files=>["1.txt"]}, {:branches=>["test2", "test3"], :files=>["1.txt"]}] 35 | 36 | And if no conflicts, the project will create target_branch(LM/light) from master and merge the source_branches(["test3", "test1", "test2"]). 37 | 38 | And after the base_branch or the source_branches update, you can remerge by: 39 | 40 | $ SmartMerge::TriggerService.new(project: your_project, user: you, params: { branch_name: updated_branch_name }).execute 41 | 42 | And if you want to remerge after push code,you can add the following code to the app/workers/post_receive.rb: 43 | 44 | $ LightMerge.auto_merge_by_ref(post_received.project, @user, ref) 45 | 46 | ## Contributing 47 | 48 | Bug reports and pull requests are welcome on GitHub at https://github.com/gitlab-extra/smart_merge. 49 | 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "smart_merge" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /doc/images/SmartMergeConf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlab-extra/smart_merge/0d7c2fa3f86d8bd6521d85053bcc57036d908a35/doc/images/SmartMergeConf.jpg -------------------------------------------------------------------------------- /doc/images/SmartMergeFlow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlab-extra/smart_merge/0d7c2fa3f86d8bd6521d85053bcc57036d908a35/doc/images/SmartMergeFlow.jpg -------------------------------------------------------------------------------- /doc/images/WhatIsSmartMerge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlab-extra/smart_merge/0d7c2fa3f86d8bd6521d85053bcc57036d908a35/doc/images/WhatIsSmartMerge.jpg -------------------------------------------------------------------------------- /doc/images/branch_model.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlab-extra/smart_merge/0d7c2fa3f86d8bd6521d85053bcc57036d908a35/doc/images/branch_model.jpg -------------------------------------------------------------------------------- /doc/images/conflict.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlab-extra/smart_merge/0d7c2fa3f86d8bd6521d85053bcc57036d908a35/doc/images/conflict.jpg -------------------------------------------------------------------------------- /doc/分支集成加速器SmartMerge.md: -------------------------------------------------------------------------------- 1 | ## 综述 2 | 3 | 本文分析了代码平台提供的集成加速器(Smart Merge,简称SM)产生的背景及其特点,并具体说明了SM在多特性分支上线流程中发挥的作用。SM 给不使用特性开关的项目的集成与上线,提供了一种高效便捷的解决方案。我们期待SM 能为同行的代码集成提供有效的参考,让研发的集成尽可能不那么繁琐,不那么低效。 4 | 5 | ## 软件研发状况和面临的问题 6 | 7 | + 项目通常有多个功能一起开发,开发的初期为了减少彼此干扰,会为每个功能创建特性分支。 8 | + 受研发不可控因素的影响,对于未来的某个时间点,多个特性功能存在是否能集成的问题。 9 | + 各特性分支进入到稳定阶段,必然需要merge在一起,然后一起被编译、打包和测试。怎么让这个过程更加自动化呢? 10 | + 多个特性分支,难免会修改公用文件,如果不及时关注公共文件的变化,以后各特性分支之间可能出现难merge的情况。 11 | 12 | ## 典型分支模型 13 | 14 | + 采用特性分支开发模式。 15 | + 主推的分支模型: 16 | * master 分支为最核心的集成分支,用于上线。 17 | * master 分支的任何一个 commit,符合质量要求。 18 | * 统一从 master 拉取新分支 。 19 | 20 | ![](images/branch_model.jpg) 21 | 22 | ## 单个特性分支怎么合入到 master 分支? 23 | 24 | 为了保证集成分支的质量,在 gitlab 上集成分支通常都被保护起来(protected),不允许直接 push 到被保护的分支。不过,我们可以通过发起 Merge Request 的方式把特性分支合入到集成分支 。借助 Merge Request,我们可以完成 sonar 静态检查、代码 review 等质量管理的活动。 25 | 26 | ## 多个特性分支会给集成带来哪些问题? 27 | 28 | + 不同分支可能会修改相同文件,集成时很可能出现代码冲突。 29 | + A、B两个分支先后合入到集成分支,B合入后导致A分支对应的功能发生故障。 30 | + A 合入到集成分支后可能需要一套测试环境;B 合入到集成分支后也可能再需要一套测试环境。多特性分支分别合入集成分支所需的测试环境也多。 31 | 32 | ## 靠什么快速发现多特性分支集成的问题? 33 | 34 | ![](images/WhatIsSmartMerge.jpg) 35 | 36 | ## Smart Merge 功能及特点 37 | 38 | + 合并冲突自动告警 39 | + 多分支自动merge 40 | + 自由选择需集成的特性分支 41 | + 高效定位可集成的特性分支的最大集合 42 | + 设置非常简单 43 | + 支持 CI 44 | + 与 Merge Request有机结合 45 | 46 | ## Smart Merge 和 Merge Request 的关系 47 | 48 | + Merge Request 是正式的 merge 请求,在开发的某个时机,用来把一个分支合入到另一个分支。 49 | + Smart Merge 服务于开发的过程,在不影响任何一个分支的基础上,把>=两个分支做集成,发现冲突及时通知当事人;一旦某个分支发生变化, Smart Merge 立即重新merge、检查并通知。 50 | + 当它俩在一起工作时,可以大大提升团队集成的效率与质量。 51 | 52 | ## 多特性分支上线流程(推荐) 53 | 54 | ![](images/SmartMergeFlow.jpg) 55 | 说明: 56 | 1. F01、F02和F03 三个功能,每个功能对应一个特性分支,并行开发。 57 | 2. 三个特性分支的开发人员通过自测后,各自发起了合入到 master 的 merge request。此时,团队可以做code review,sonar静态扫描等检查活动。 58 | 3. 于此同时,负责集成的人员借助 Smart Merge,构建、打包并测试后发现F01和 F03的功能集成后可以一起上线,而集成F02 后发现有问题。 59 | 4. 最后,F01master 及 F03master 的两个 Merge Request 被接纳,而 F02master的 Merge Request 被拒绝。 60 | 61 | 上面步骤3在确定哪些特性分支可以一起上线的过程中,就可以借助Smart Merge。代码平台上Smart Merge设置如下图: 62 | ![](images/SmartMergeConf.jpg) 63 | 64 | 如果自动集成时代码发生冲突,则web上会提示冲突,也会Email通知给相关人员。如F01和F02分支修改了同文件的同一行,Smart Merge自动merge后web提示信息如下图: 65 | ![](images/conflict.jpg) 66 | 67 | ## 结束语 68 | 69 | 对于不使用特性开关的项目(上线后所有功能都会启用),我们必须保证上线后各个功能正确且有效,这对集成的效率和质量提出较高的要求。 70 | 在对多个特性分支做集成的时候,如果不借助Smart Merge类似的工具,负责集成的人员需要做许多繁琐又重复的活动;反之,只需简单的调整需参加集成的特性分支的集合,就能靠Smart Merge自动完成这些特性分支代码的集成、构建、部署,甚至自动化测试,从而筛选出用于上线的特性分支。因此,我们称 Smart Merge 是分支集成的加速器。 71 | -------------------------------------------------------------------------------- /lib/generators/smart_merge/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | module SmartMerge 4 | module Generators 5 | class InstallGenerator < Rails::Generators::Base 6 | source_root File.expand_path('templates', __dir__) 7 | 8 | def copy_initializer_file 9 | copy_file "smart_merge_setting.rb", "app/models/smart_merge_setting.rb" 10 | copy_file "create_smart_merge_settings.rb", "db/migrate/#{Time.now.strftime('%Y%m%d%H%M%S')}_create_smart_merge_settings.rb" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/smart_merge/templates/create_smart_merge_settings.rb: -------------------------------------------------------------------------------- 1 | class CreateSmartMergeSettings < ActiveRecord::Migration 2 | def change 3 | create_table :smart_merge_settings do |t| 4 | t.references :project 5 | t.string :target_branch 6 | t.text :base_branch 7 | t.text :source_branches 8 | t.text :conflicts 9 | t.integer :status, default: 2 10 | t.boolean :auto_merge, default: true 11 | t.integer :creator 12 | 13 | t.timestamps null: false 14 | end 15 | add_index :light_merges, :project_id 16 | add_index :light_merges, :target_branch 17 | add_index :light_merges, :status 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/smart_merge/templates/smart_merge_setting.rb: -------------------------------------------------------------------------------- 1 | class SmartMergeSetting < ActiveRecord::Base 2 | belongs_to :project 3 | 4 | serialize :base_branch 5 | serialize :source_branches 6 | serialize :conflicts 7 | 8 | validates :base_branch, presence: true 9 | validate :validate_source_branches 10 | 11 | before_create :validate_target_branches 12 | after_create :create_target_branch 13 | 14 | STATUS_LIST = {'unchecked' => 0, 'success' => 1, 'failed' => 2} 15 | 16 | def self.auto_merge_by_ref(project, user, ref) 17 | branch_name = ref.match(/refs\/heads\/(.+)/)[1] 18 | SmartMerge::TriggerService.new(project: project, user: user, params: { branch_name: branch_name }).execute 19 | end 20 | 21 | def validate_target_branches 22 | if self.project.repository.branch_names.include?(self.target_branch) 23 | message = "Target branch was existed!" 24 | end 25 | 26 | light_merges = SmartMergeSetting.where(project_id: self.project_id, target_branch: self.target_branch) 27 | if light_merges.present? 28 | message = "Target branch was used by other light merge" 29 | errors.add :target_branch_conflict, message if message.present? 30 | return false 31 | end 32 | end 33 | 34 | def validate_source_branches 35 | if source_branches.blank? 36 | errors.add :source_branches_blank, "source branches can not be blank" 37 | return false 38 | end 39 | delete_branches = source_branches.reject{ |b| project.repository.branch_names.include?(b[:name]) } 40 | if delete_branches.present? 41 | errors.add :source_branch_not_exist, "#{delete_branches.join(" ")} was not exist" 42 | return false 43 | end 44 | end 45 | 46 | def source_branches_ordered 47 | self[:source_branches].sort_by{|sb| sb[:name]} 48 | end 49 | 50 | def check_if_can_be_merged 51 | require_merge_branches.inject([]) do |arr, branch| 52 | unless project.repository.can_be_merged?(branch[:source_sha], target_branch) 53 | branch[:status] = "FAILURE" 54 | update_source_branch(branch) 55 | arr << branch 56 | end 57 | arr 58 | end 59 | end 60 | 61 | def user 62 | User.find(creator) 63 | end 64 | 65 | def recent_update_source_branch 66 | source_branches.sort_by{|sb| sb[:update_at]}.last 67 | end 68 | 69 | def recent_update_by 70 | recent_update_source_branch.try(:[], :author) 71 | end 72 | 73 | def display_status 74 | case 75 | when merging? then "Merging" 76 | when merged? then "Merged" 77 | when conflict? then "Conflict" 78 | end 79 | end 80 | 81 | def merging? 82 | source_branches.select{ |branch| ["PENDING", "MERGING"].include?(branch[:status]) }.present? 83 | end 84 | 85 | def merged? 86 | source_branches.select{ |branch| branch[:status] != "MERGED" }.blank? 87 | end 88 | 89 | def conflict? 90 | source_branches.select{ |branch| ["CONFLICT", "UNMERGE"].include?(branch[:status]) }.present? 91 | end 92 | 93 | def find_branch(name) 94 | light_merge.project.repository.find_branch(name) 95 | end 96 | 97 | def can_merge? 98 | source_branches.select do |branch| 99 | light_merge.find_branch(branch[:name]).target != branch[:source_sha] 100 | end.present? 101 | end 102 | 103 | def can_branch_merge?(name) 104 | light_merge.find_branch(name).target != branch[:source_sha] 105 | end 106 | 107 | def to_pending(branch) 108 | source_branch = source_branches.select{ |sb| sb[:name] == branch }[0] 109 | if source_branch 110 | source_branch[:status] = "PENDING" 111 | update_source_branch(source_branch) 112 | end 113 | end 114 | 115 | def source_branch_forward(name) 116 | if name.present? 117 | source_branch = find_source_branch(name) 118 | update_source_branch(source_branch.merge(branch_info(name)).merge(status: "PENDING")) if source_branch 119 | else 120 | sbs = source_branches.map do |sb| 121 | sb.merge(branch_info(sb[:name]).merge(status: "PENDING")) 122 | end 123 | update(source_branches: sbs) 124 | end 125 | end 126 | 127 | def merge_commit_message(branch) 128 | "Merge branch '#{branch}' into '#{target_branch}'" 129 | end 130 | 131 | def recent_merged_message 132 | branch = source_branches.select{ |sb| sb[:status] == "SUCCESS" }.sort_by{|sb| sb[:update_at]}.last 133 | return "UNMERGED!" unless branch 134 | "#{branch[:name]} Was Merged Into #{target_branch} At #{branch[:update_at]}!" 135 | end 136 | 137 | def failure_branches 138 | source_branches.select{ |sb| sb[:status] == "FAILURE" } 139 | end 140 | 141 | def update_source_branch(branch) 142 | branches = source_branches.delete_if{ |sb| sb[:name] == branch[:name] } 143 | branches.push(branch) 144 | update(source_branches: branches) 145 | end 146 | 147 | def branch_info(name) 148 | recent_commit = project.repository.commits(name).first 149 | { source_sha: recent_commit.id, author: recent_commit.author_name, update_at: recent_commit.committed_date.strftime("%Y-%m-%d %H:%M:%S") } 150 | end 151 | 152 | def find_source_branch(name) 153 | source_branches.select{ |sb| sb[:name] == name }[0] 154 | end 155 | 156 | def updated_source_branches 157 | source_branches.select do |source_branch| 158 | target_sha = project.repository.find_branch(source_branch[:name]).target 159 | target_sha != source_branch[:source_sha] 160 | end 161 | end 162 | 163 | def tmp_ref 164 | "refs/light_merge/#{target_branch}" 165 | end 166 | 167 | def in_locked_state 168 | begin 169 | Timeout.timeout(300) do 170 | loop do 171 | break if reload.status != STATUS_LIST["unchecked"] 172 | sleep 5 173 | end 174 | end 175 | update(status: STATUS_LIST["unchecked"]) 176 | result = yield 177 | update(status: STATUS_LIST["success"]) if status == STATUS_LIST["unchecked"] 178 | result 179 | rescue 180 | update(status: STATUS_LIST["failed"]) 181 | false 182 | end 183 | end 184 | 185 | def create_target_branch 186 | CreateBranchService.new(self.project, User.find(self.creator)).execute(self.target_branch, self.base_branch[:source_sha]) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/smart_merge.rb: -------------------------------------------------------------------------------- 1 | require "smart_merge/version" 2 | 3 | require_relative "smart_merge/base_service" 4 | require_relative "smart_merge/check_service" 5 | require_relative "smart_merge/create_service" 6 | require_relative "smart_merge/destroy_service" 7 | require_relative "smart_merge/merge_service" 8 | require_relative "smart_merge/trigger_service" 9 | require_relative "smart_merge/update_service" 10 | 11 | module SmartMerge 12 | # Your code goes here... 13 | end 14 | -------------------------------------------------------------------------------- /lib/smart_merge/base_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class BaseService 3 | attr_accessor :project, :user, :params, :smart_merge 4 | 5 | def initialize(project: nil, user: nil, smart_merge: nil, params: {}) 6 | @project, @user, @smart_merge, @params = project, user, smart_merge, params 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/smart_merge/check_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class CheckService < SmartMerge::BaseService 3 | def execute 4 | conflicts = [] 5 | branches = [smart_merge.base_branch].concat(smart_merge.source_branches_ordered) 6 | branches.each_with_index do |branch, index| 7 | next_index = index + 1 8 | branch_sha = project.repository.commit(branch[:name]).id 9 | branches.slice(next_index..-1).each do |other_branch| 10 | other_branch_sha = project.repository.commit(other_branch[:name]).id 11 | merge_index = project.repository.rugged.merge_commits(branch_sha, other_branch_sha) 12 | if merge_index.conflicts? 13 | files = merge_index.conflicts.map{ |conflic| conflic[:ancestor] && conflic[:ancestor][:path] }.compact 14 | conflict_branches = [ branch[:name], other_branch[:name] ] 15 | conflicts << { branches: conflict_branches, files: files } 16 | end 17 | end 18 | end 19 | smart_merge.update(conflicts: conflicts) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/smart_merge/create_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class CreateService < SmartMerge::BaseService 3 | def execute 4 | smart_merge = SmartMergeSetting.new(project_id: @project.id, target_branch: params[:target_branch], creator: @user.id) 5 | 6 | smart_merge.source_branches = params[:source_branches].map do |branch| 7 | recent_commit = @project.repository.commits(branch).first 8 | { name: branch, status: "PENDING", source_sha: recent_commit.id, author: recent_commit.author_name, update_at: recent_commit.committed_date.strftime("%Y-%m-%d %H:%M:%S") } 9 | end 10 | 11 | source_sha = @project.repository.find_branch(params[:base_branch]).target 12 | smart_merge.base_branch = { name: params[:base_branch], source_sha: source_sha } 13 | 14 | smart_merge.save 15 | smart_merge 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/smart_merge/destroy_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class DestroyService < SmartMerge::BaseService 3 | def execute 4 | unless params[:remain_target] 5 | DeleteBranchService.new(project, user).execute(smart_merge.target_branch) 6 | end 7 | smart_merge.destroy 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/smart_merge/merge_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class MergeService < SmartMerge::BaseService 3 | def execute 4 | return to_failure unless check 5 | return if !smart_merge.auto_merge && params[:trigger] == "push" 6 | smart_merge.in_locked_state do 7 | begin 8 | to_pending 9 | to_merge 10 | to_success 11 | rescue 12 | to_failure 13 | end 14 | end 15 | end 16 | 17 | private 18 | def check 19 | SmartMerge::CheckService.new(project: project, user: user, smart_merge: smart_merge).execute 20 | return true if smart_merge.conflicts.blank? 21 | conflict_branches = smart_merge.conflicts.map{ |conflic| conflic[:branches] }.flatten 22 | source_branches = smart_merge.source_branches.map do |source_branch| 23 | source_branch[:status] = conflict_branches.include?(source_branch[:name]) ? "CONFLICT" : "UNMERGE" 24 | source_branch 25 | end 26 | smart_merge.update(source_branches: source_branches) 27 | false 28 | end 29 | 30 | def to_pending 31 | smart_merge.update(status: SmartMergeSetting::STATUS_LIST["unchecked"]) 32 | source_branches = smart_merge.source_branches.map do |source_branch| 33 | source_branch[:status] == "PENDING" 34 | source_branch 35 | end 36 | smart_merge.update(source_branches: source_branches) 37 | 38 | delete_tmp_ref 39 | rugged.references.create(smart_merge.tmp_ref, smart_merge.base_branch[:source_sha]) 40 | end 41 | 42 | def to_merge 43 | smart_merge.source_branches_ordered.each do |branch| 44 | branch[:status] = "MERGING" 45 | smart_merge.update_source_branch(branch) 46 | unless commit(branch) 47 | to_unmerge 48 | break 49 | end 50 | end 51 | end 52 | 53 | def to_unmerge 54 | source_branches = smart_merge.source_branches.map do |source_branch| 55 | source_branch[:status] == "UNMERGE" 56 | source_branch 57 | end 58 | smart_merge.update(source_branches: source_branches) 59 | end 60 | 61 | def to_success 62 | if smart_merge.conflict? 63 | to_failure 64 | else 65 | if !project.repository.branch_exists?(smart_merge.target_branch) 66 | CreateBranchService.new(project, user).execute(smart_merge.target_branch, smart_merge.base_branch[:source_sha]) 67 | end 68 | ref = Gitlab::Git::BRANCH_REF_PREFIX + smart_merge.target_branch 69 | GitOperationService.new(user, project.repository).update_ref_in_hooks(ref, tmp_branch_sha, smart_merge.base_branch[:source_sha]) 70 | delete_tmp_ref 71 | end 72 | end 73 | 74 | def commit(branch) 75 | begin 76 | tmp_branch_sha = project.repository.commit(smart_merge.tmp_ref).id 77 | if tmp_branch_sha == branch[:source_sha] || project.repository.is_ancestor?(branch[:source_sha], tmp_branch_sha) 78 | branch[:status] = "MERGED" 79 | else 80 | if Rugged::Commit.create(rugged, merge_options(branch)) 81 | branch[:status] = "MERGED" 82 | else 83 | branch[:status] = "UNMERGE" 84 | end 85 | end 86 | rescue => e 87 | branch[:status] = "UNMERGE" 88 | Rails.logger.error(e.message) 89 | end 90 | 91 | smart_merge.update_source_branch(branch) 92 | return branch[:status] == "MERGED" 93 | end 94 | 95 | def merge_options(branch) 96 | committer = project.repository.user_to_committer(user) 97 | merge_index = rugged.merge_commits(tmp_branch_sha, branch[:source_sha]) 98 | { 99 | parents: [tmp_branch_sha, branch[:source_sha]], 100 | message: params[:commit_message] || smart_merge.merge_commit_message(branch[:name]), 101 | tree: merge_index.write_tree(rugged), 102 | update_ref: smart_merge.tmp_ref, 103 | author: committer, 104 | committer: committer 105 | } 106 | end 107 | 108 | def rugged 109 | project.repository.rugged 110 | end 111 | 112 | def tmp_branch_sha 113 | project.repository.commit(smart_merge.tmp_ref).id 114 | end 115 | 116 | def delete_tmp_ref 117 | if project.repository.commit(smart_merge.tmp_ref) 118 | rugged.references.delete(smart_merge.tmp_ref) 119 | end 120 | end 121 | 122 | def to_failure 123 | smart_merge.update(status: SmartMergeSetting::STATUS_LIST["failed"]) 124 | target_branch = project.repository.find_branch(smart_merge.target_branch) 125 | if target_branch && target_branch.target != smart_merge.base_branch[:source_sha] 126 | ref = Gitlab::Git::BRANCH_REF_PREFIX + smart_merge.target_branch 127 | GitOperationService.new(user, project.repository).send(:update_ref, ref, smart_merge.base_branch[:source_sha], nil) 128 | elsif target_branch.nil? 129 | CreateBranchService.new(project, user).execute(smart_merge.target_branch, smart_merge.base_branch[:source_sha]) 130 | end 131 | delete_tmp_ref 132 | Notify.conflict_smart_merge_email(user.id, smart_merge.id).deliver_now 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/smart_merge/trigger_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class TriggerService < SmartMerge::BaseService 3 | def execute 4 | if smart_merge 5 | SmartMerge::MergeService.new(project: project, user: user, smart_merge: smart_merge).execute 6 | elsif params[:branch_name] 7 | SmartMergeSetting.where(project_id: project.id).each do |smart_merge| 8 | source_branch = smart_merge.find_source_branch(params[:branch_name]) 9 | if source_branch.present? 10 | source_branch.merge!(smart_merge.branch_info(source_branch[:name])) 11 | smart_merge.update_source_branch(source_branch) 12 | end 13 | if smart_merge.base_branch[:name] == params[:branch_name] 14 | smart_merge.base_branch[:source_sha] = project.repository.find_branch(params[:branch_name]).target 15 | smart_merge.save 16 | end 17 | if source_branch || smart_merge.base_branch[:name] == params[:branch_name] 18 | SmartMerge::MergeService.new(project: project, user: user, smart_merge: smart_merge, params: { trigger: "push" }).execute 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/smart_merge/update_service.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | class UpdateService < SmartMerge::BaseService 3 | def execute 4 | if params[:base_branch] != smart_merge.base_branch[:name] 5 | source_sha = project.repository.find_branch(params[:base_branch]).target 6 | smart_merge.base_branch = { name: params[:base_branch], source_sha: source_sha } 7 | end 8 | 9 | add, del = get_change_source_branches 10 | smart_merge.source_branches = smart_merge.source_branches.delete_if{ |sb| del.include?(sb[:name]) } if del.present? 11 | add.each do |branch| 12 | smart_merge.source_branches << { name: branch, status: "PENDING" }.merge(smart_merge.branch_info(branch)) 13 | end if add.present? 14 | 15 | if to_auto_merge? 16 | smart_merge.base_branch[:source_sha] = project.repository.find_branch(params[:base_branch]).target 17 | smart_merge.source_branches = smart_merge.source_branches.map do |branch| 18 | { name: branch[:name], status: "PENDING" }.merge(smart_merge.branch_info(branch[:name])) 19 | end 20 | end 21 | 22 | smart_merge.auto_merge = params[:auto_merge] 23 | smart_merge.save 24 | end 25 | 26 | private 27 | def get_change_source_branches 28 | old_branches = smart_merge.source_branches.map{ |sb| sb[:name] } 29 | new_branches = params[:source_branches] 30 | add = new_branches - old_branches 31 | del = old_branches - new_branches 32 | [add, del] 33 | end 34 | 35 | def to_auto_merge? 36 | !smart_merge.auto_merge && params[:auto_merge] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/smart_merge/version.rb: -------------------------------------------------------------------------------- 1 | module SmartMerge 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /smart_merge.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'smart_merge/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "smart_merge" 8 | spec.version = SmartMerge::VERSION 9 | spec.authors = ["htle2101"] 10 | spec.email = ["htle2101@gmail.com"] 11 | 12 | spec.summary = %q{smart merge for gitlab.} 13 | spec.description = %q{smart merge for gitlab.} 14 | spec.homepage = "https://github.com/gitlab-extra/smart_merge" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata['allowed_push_host'] = "https://github.com/gitlab-extra/smart_merge" 20 | else 21 | raise "RubyGems 2.0 or newer is required to protect against " \ 22 | "public gem pushes." 23 | end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 26 | f.match(%r{^(test|spec|features)/}) 27 | end 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | 32 | spec.add_development_dependency "bundler", "~> 1.14" 33 | spec.add_development_dependency "rake", "~> 10.0" 34 | spec.add_development_dependency "rspec", "~> 3.0" 35 | end 36 | -------------------------------------------------------------------------------- /spec/smart_merge_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe SmartMerge do 4 | it "has a version number" do 5 | expect(SmartMerge::VERSION).not_to be nil 6 | end 7 | 8 | it "does something useful" do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "smart_merge" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | config.expect_with :rspec do |c| 9 | c.syntax = :expect 10 | end 11 | end 12 | --------------------------------------------------------------------------------