daq_flow 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +62 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +165 -0
  8. data/Rakefile +10 -0
  9. data/bin/daq_flow +23 -0
  10. data/flash_flow.gemspec +28 -0
  11. data/flash_flow.yml.erb.example +42 -0
  12. data/lib/flash_flow.rb +7 -0
  13. data/lib/flash_flow/branch_merger.rb +55 -0
  14. data/lib/flash_flow/cmd_runner.rb +54 -0
  15. data/lib/flash_flow/config.rb +84 -0
  16. data/lib/flash_flow/data.rb +6 -0
  17. data/lib/flash_flow/data/base.rb +89 -0
  18. data/lib/flash_flow/data/bitbucket.rb +152 -0
  19. data/lib/flash_flow/data/branch.rb +124 -0
  20. data/lib/flash_flow/data/collection.rb +211 -0
  21. data/lib/flash_flow/data/github.rb +140 -0
  22. data/lib/flash_flow/data/store.rb +44 -0
  23. data/lib/flash_flow/git.rb +267 -0
  24. data/lib/flash_flow/install.rb +19 -0
  25. data/lib/flash_flow/lock.rb +23 -0
  26. data/lib/flash_flow/merge.rb +6 -0
  27. data/lib/flash_flow/merge/acceptance.rb +154 -0
  28. data/lib/flash_flow/merge/base.rb +116 -0
  29. data/lib/flash_flow/merge_order.rb +27 -0
  30. data/lib/flash_flow/notifier.rb +23 -0
  31. data/lib/flash_flow/options.rb +34 -0
  32. data/lib/flash_flow/resolve.rb +143 -0
  33. data/lib/flash_flow/shadow_repo.rb +44 -0
  34. data/lib/flash_flow/time_helper.rb +32 -0
  35. data/lib/flash_flow/version.rb +4 -0
  36. data/log/.keep +0 -0
  37. data/test/lib/data/test_base.rb +10 -0
  38. data/test/lib/data/test_branch.rb +206 -0
  39. data/test/lib/data/test_collection.rb +308 -0
  40. data/test/lib/data/test_store.rb +70 -0
  41. data/test/lib/lock/test_github.rb +74 -0
  42. data/test/lib/merge/test_acceptance.rb +230 -0
  43. data/test/lib/test_branch_merger.rb +78 -0
  44. data/test/lib/test_config.rb +63 -0
  45. data/test/lib/test_git.rb +73 -0
  46. data/test/lib/test_merge_order.rb +71 -0
  47. data/test/lib/test_notifier.rb +33 -0
  48. data/test/lib/test_resolve.rb +69 -0
  49. data/test/minitest_helper.rb +41 -0
  50. data/update_gem.sh +5 -0
  51. metadata +192 -0
@@ -0,0 +1,19 @@
1
+ require 'fileutils'
2
+
3
+ module FlashFlow
4
+ class Install
5
+ def self.install
6
+ FileUtils.mkdir 'config' unless Dir.exists?('config')
7
+ dest_file = 'config/flash_flow.yml.erb'
8
+
9
+ FileUtils.cp example_file, dest_file
10
+
11
+ puts "Flash flow config file is in #{dest_file}"
12
+ end
13
+
14
+ def self.example_file
15
+ "#{File.dirname(__FILE__)}/../../flash_flow.yml.erb.example"
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module FlashFlow
2
+ module Lock
3
+ class Error < RuntimeError; end
4
+
5
+ class Base
6
+ def initialize(config=nil)
7
+ lock_class_name = config && config['class'] && config['class']['name']
8
+ return unless lock_class_name
9
+
10
+ lock_class = Object.const_get(lock_class_name)
11
+ @lock = lock_class.new(config['class'])
12
+ end
13
+
14
+ def with_lock(&block)
15
+ if @lock
16
+ @lock.with_lock(&block)
17
+ else
18
+ yield
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module FlashFlow
2
+ module Merge
3
+ end
4
+ end
5
+
6
+ require 'flash_flow/merge/acceptance'
@@ -0,0 +1,154 @@
1
+ require 'flash_flow/merge/base'
2
+ require 'flash_flow/time_helper'
3
+
4
+ module FlashFlow
5
+ module Merge
6
+ class Acceptance < Base
7
+
8
+ def initialize(opts={})
9
+ super(opts)
10
+
11
+ @data = Data::Base.new(Config.configuration.branches, Config.configuration.branch_info_file, @git, logger: logger)
12
+
13
+ @do_not_merge = opts[:do_not_merge]
14
+ @force = opts[:force]
15
+ @rerere_forget = opts[:rerere_forget]
16
+ @stories = [opts[:stories]].flatten.compact
17
+ end
18
+
19
+ def run
20
+ # check_version
21
+ check_git_version
22
+ check_repo
23
+ puts "Building #{@local_git.merge_branch}... Log can be found in #{FlashFlow::Config.configuration.log_file}"
24
+ logger.info "\n\n### Beginning #{@local_git.merge_branch} merge ###\n\n"
25
+
26
+ begin
27
+ open_pull_request
28
+
29
+ @lock.with_lock do
30
+ @git.in_original_merge_branch do
31
+ @git.initialize_rerere(@local_git.working_dir)
32
+ end
33
+
34
+ @git.reset_temp_merge_branch
35
+ @git.in_temp_merge_branch do
36
+ merge_branches(@data.mergeable) do |branch, merger|
37
+ # Do not merge the master branch or the merge branch
38
+ next if [@git.merge_branch, @git.master_branch].include?(branch.ref)
39
+ process_result(branch, merger)
40
+ end
41
+ commit_branch_info
42
+ commit_rerere
43
+ end
44
+
45
+ @git.copy_temp_to_branch(@git.merge_branch, commit_message)
46
+ @git.delete_temp_merge_branch
47
+ @git.push(@git.merge_branch)
48
+ end
49
+
50
+ raise OutOfSyncWithRemote.new("#{@git.merge_branch} is out of sync with the remote.") unless @git.last_success?
51
+ print_errors
52
+ logger.info "### Finished #{@local_git.merge_branch} merge ###"
53
+ rescue Lock::Error, OutOfSyncWithRemote => e
54
+ puts 'Failure!'
55
+ puts e.message
56
+ ensure
57
+ @local_git.run("checkout #{@local_git.working_branch}")
58
+ end
59
+ end
60
+
61
+ def commit_branch_info
62
+ @stories.each do |story_id|
63
+ @data.add_story(@git.working_branch, story_id)
64
+ end
65
+ @data.save!
66
+ end
67
+
68
+ def commit_rerere
69
+ current_branches = @data.to_a.select { |branch| !@git.master_branch_contains?(branch.sha) && (Time.now - branch.updated_at < TimeHelper.two_weeks) }
70
+ current_rereres = current_branches.map { |branch| branch.resolutions.to_h.values }.flatten
71
+
72
+ @git.commit_rerere(current_rereres)
73
+ end
74
+
75
+ def process_result(branch, merger)
76
+ case merger.result
77
+ when :deleted
78
+ @data.mark_deleted(branch)
79
+ @notifier.deleted_branch(branch) unless is_working_branch(branch)
80
+
81
+ when :success
82
+ branch.sha = merger.sha
83
+ @data.mark_success(branch)
84
+ @data.set_resolutions(branch, merger.resolutions)
85
+
86
+ when :conflict
87
+ if is_working_branch(branch)
88
+ @data.mark_failure(branch, merger.conflict_sha)
89
+ else
90
+ @data.mark_failure(branch, nil)
91
+ @notifier.merge_conflict(branch)
92
+ end
93
+ end
94
+ end
95
+
96
+ def is_working_branch(branch)
97
+ branch.ref == @git.working_branch
98
+ end
99
+
100
+ def open_pull_request
101
+ return false if [@local_git.master_branch, @local_git.merge_branch].include?(@local_git.working_branch)
102
+
103
+ @local_git.push(@local_git.working_branch, @force)
104
+ raise OutOfSyncWithRemote.new("Your branch is out of sync with the remote. If you want to force push, run 'flash_flow -f'") unless @local_git.last_success?
105
+
106
+ if @do_not_merge
107
+ @data.remove_from_merge(@local_git.working_branch)
108
+ else
109
+ @data.add_to_merge(@local_git.working_branch)
110
+ end
111
+ end
112
+
113
+ def print_errors
114
+ puts format_errors
115
+ end
116
+
117
+ def format_errors
118
+ errors = []
119
+ branch_not_merged = nil
120
+ @data.failures.each do |branch|
121
+ if branch.ref == @local_git.working_branch
122
+ branch_not_merged = "ERROR: Your branch did not merge to #{@local_git.merge_branch}. Run 'flash_flow --resolve', fix the merge conflict(s) and then re-run this script\n"
123
+ else
124
+ errors << "WARNING: Unable to merge branch #{@local_git.remote}/#{branch.ref} to #{@local_git.merge_branch} due to conflicts."
125
+ end
126
+ end
127
+ errors << branch_not_merged if branch_not_merged
128
+
129
+ if errors.empty?
130
+ "Success!"
131
+ else
132
+ errors.join("\n")
133
+ end
134
+ end
135
+
136
+ def commit_message
137
+ message =<<-EOS
138
+ Flash Flow run from branch: #{@local_git.working_branch}
139
+
140
+ Merged branches:
141
+ #{@data.successes.empty? ? 'None' : @data.successes.sort_by(&:merge_order).map(&:ref).join("\n")}
142
+
143
+ Failed branches:
144
+ #{@data.failures.empty? ? 'None' : @data.failures.map(&:ref).join("\n")}
145
+
146
+ Removed branches:
147
+ #{@data.removals.empty? ? 'None' : @data.removals.map(&:ref).join("\n")}
148
+ EOS
149
+ message.gsub(/'/, '')
150
+ end
151
+
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,116 @@
1
+ require 'logger'
2
+
3
+ require 'flash_flow/git'
4
+ require 'flash_flow/data'
5
+ require 'flash_flow/lock'
6
+ require 'flash_flow/notifier'
7
+ require 'flash_flow/branch_merger'
8
+ require 'flash_flow/merge_order'
9
+ require 'flash_flow/shadow_repo'
10
+
11
+ module FlashFlow
12
+ module Merge
13
+ class Base
14
+
15
+ class VersionError < RuntimeError; end
16
+ class OutOfSyncWithRemote < RuntimeError; end
17
+ class UnmergeableBranch < RuntimeError; end
18
+ class NothingToMergeError < RuntimeError; end
19
+
20
+ def initialize(opts={})
21
+ @local_git = Git.new(Config.configuration.git, logger)
22
+ @git = ShadowGit.new(Config.configuration.git, logger)
23
+ @lock = Lock::Base.new(Config.configuration.lock)
24
+ @notifier = Notifier::Base.new(Config.configuration.notifier)
25
+ end
26
+
27
+ def logger
28
+ @logger ||= FlashFlow::Config.configuration.logger
29
+ end
30
+
31
+ def check_repo
32
+ if @local_git.staged_and_working_dir_files.any?
33
+ raise RuntimeError.new('You have changes in your working directory. Please stash and try again')
34
+ end
35
+ end
36
+
37
+ def check_version
38
+ data_version = @data.version
39
+ return if data_version.nil?
40
+
41
+ written_version = data_version.split(".").map(&:to_i)
42
+ running_version = FlashFlow::VERSION.split(".").map(&:to_i)
43
+
44
+ unless written_version[0] < running_version[0] ||
45
+ (written_version[0] == running_version[0] && written_version[1] <= running_version[1]) # Ignore the point release number
46
+ raise RuntimeError.new("Your version of flash flow (#{FlashFlow::VERSION}) is behind the version that was last used (#{data_version}) by a member of your team. Please upgrade to at least #{written_version[0]}.#{written_version[1]}.0 and try again.")
47
+ end
48
+ end
49
+
50
+ def check_git_version
51
+ git_version = @local_git.version
52
+ return if git_version.nil?
53
+
54
+ running_version = git_version.split(".").map(&:to_i)
55
+ expected_version = FlashFlow::GIT_VERSION.split(".").map(&:to_i)
56
+
57
+ if running_version[0] < expected_version[0] ||
58
+ (running_version[0] == expected_version[0] && running_version[1] < expected_version[1]) # Ignore the point release number
59
+ puts "Warning: Your version of git (#{git_version}) is behind the version that is tested (#{FlashFlow::GIT_VERSION}). We recommend to upgrade to at least #{expected_version[0]}.#{expected_version[1]}.0"
60
+ end
61
+ end
62
+
63
+ def merge_branches(branches)
64
+ ordered_branches = MergeOrder.new(@git, branches).get_order
65
+ ordered_branches.each_with_index do |branch, index|
66
+ branch.merge_order = index + 1
67
+
68
+ merger = git_merge(branch)
69
+
70
+ yield(branch, merger)
71
+ end
72
+ end
73
+
74
+ def git_merge(branch)
75
+ merger = BranchMerger.new(@git, branch)
76
+ forget_rerere = is_working_branch(branch) && @rerere_forget
77
+
78
+ merger.do_merge(forget_rerere)
79
+
80
+ merger
81
+ end
82
+
83
+ def is_working_branch(branch)
84
+ branch.ref == @git.working_branch
85
+ end
86
+
87
+ def pending_release
88
+ @data.pending_release
89
+ end
90
+
91
+ def ready_to_merge_release
92
+ @data.ready_to_merge_release
93
+ end
94
+
95
+ def release_ahead_of_master?
96
+ @git.ahead_of_master?("#{@git.remote}/#{@git.release_branch}")
97
+ end
98
+
99
+ def write_data(commit_msg)
100
+ @git.in_temp_merge_branch do
101
+ @git.run("reset --hard #{@git.remote}/#{@git.merge_branch}")
102
+ end
103
+ @git.in_merge_branch do
104
+ @git.run("reset --hard #{@git.remote}/#{@git.merge_branch}")
105
+ end
106
+
107
+ @data.save!
108
+
109
+ @git.copy_temp_to_branch(@git.merge_branch, commit_msg)
110
+ @git.delete_temp_merge_branch
111
+ @git.push(@git.merge_branch, false)
112
+ end
113
+
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ module FlashFlow
2
+ class MergeOrder
3
+
4
+ def initialize(git, branches)
5
+ @git = git
6
+ @branches = branches
7
+ end
8
+
9
+ def get_order
10
+ new_branches, old_branches = @branches.partition { |branch| branch.merge_order.nil? }
11
+ branches = old_branches.sort_by(&:merge_order) + new_branches
12
+
13
+ unchanged, changed = branches.partition { |branch| current_sha(branch) == branch.sha }
14
+ my_branch_index = changed.find_index { |branch| branch.ref == @git.working_branch }
15
+ my_branch_changed = my_branch_index ? changed.delete_at(my_branch_index) : nil
16
+
17
+ [unchanged, changed, my_branch_changed].flatten.compact
18
+ end
19
+
20
+ private
21
+
22
+ def current_sha(branch)
23
+ @git.get_sha("#{@git.remote}/#{branch.ref}")
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ require 'flash_flow/data'
2
+
3
+ module FlashFlow
4
+ module Notifier
5
+ class Base
6
+ def initialize(config=nil)
7
+ notifier_class_name = config && config['class'] && config['class']['name']
8
+ return unless notifier_class_name
9
+
10
+ @notifier_class = Object.const_get(notifier_class_name)
11
+ @notifier = @notifier_class.new(config['class'])
12
+ end
13
+
14
+ def merge_conflict(branch)
15
+ @notifier.merge_conflict(branch) if @notifier.respond_to?(:merge_conflict)
16
+ end
17
+
18
+ def deleted_branch(branch)
19
+ @notifier.deleted_branch(branch) if @notifier.respond_to?(:deleted_branch)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ require 'optparse'
2
+
3
+ module FlashFlow
4
+ class Options
5
+ def self.parse
6
+ options = {}
7
+ opt_parser = OptionParser.new do |opts|
8
+ opts.banner = 'Usage: flash_flow [options]'
9
+ opts.separator ''
10
+
11
+ opts.on('--install', 'Copy flash_flow.yml.erb to your repo and exit') { |v| options[:install] = true }
12
+ opts.on('-v', '--version', 'Print the current version of flash flow and exit') { |v| options[:version] = true }
13
+ opts.on('-n', '--no-merge', 'Run flash flow, but do not merge this branch') { |v| options[:do_not_merge] = true }
14
+ opts.on('--rerere-forget', 'Delete the saved patch for this branch and let the merge fail if there is a conflict') { |v| options[:rerere_forget] = true }
15
+ opts.on('-f', '--force-push', 'Force push your branch') { |v| options[:force] = v }
16
+ opts.on('-c', '--config-file FILE_PATH', 'The path to your config file. Defaults to config/flash_flow.yml.erb') { |v| options[:config_file] = v }
17
+ opts.on('--resolve', 'Launch a bash shell to save your conflict resolutions') { |v| options[:resolve] = true }
18
+ opts.on('--resolve-manual', 'Print instructions to use git to resolve conflicts') { |v| options[:resolve_manual] = true }
19
+
20
+ opts.on_tail('-h', '--help', 'Show this message') do
21
+ puts opts
22
+ exit
23
+ end
24
+ end
25
+
26
+ opt_parser.parse!
27
+
28
+ options[:stories] ||= []
29
+ options[:config_file] ||= './config/flash_flow.yml.erb'
30
+
31
+ options
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,143 @@
1
+ require 'logger'
2
+
3
+ require 'flash_flow/git'
4
+ require 'flash_flow/data'
5
+
6
+ module FlashFlow
7
+ class Resolve
8
+
9
+ class NothingToResolve < StandardError; end
10
+
11
+ def initialize(git_config, branch_info_file, opts={})
12
+ @logger = opts[:logger]
13
+ @branch_info_file = branch_info_file
14
+ @cmd_runner = CmdRunner.new(logger: @logger)
15
+ @git = ShadowGit.new(git_config, @logger)
16
+ end
17
+
18
+ def manual_instructions
19
+ check_for_conflict
20
+ puts manual_not_merged_instructions
21
+ end
22
+
23
+ def start
24
+ check_for_conflict
25
+
26
+ in_working_branch do
27
+ merge_conflicted
28
+
29
+ if unresolved_conflicts.empty?
30
+ puts "You have already resolved all conflicts."
31
+ else
32
+ launch_bash
33
+
34
+ rerere
35
+
36
+ unless unresolved_conflicts.empty?
37
+ puts "There are still unresolved conflicts in these files:\n#{unresolved_conflicts.join("\n")}\n\n"
38
+ end
39
+ end
40
+
41
+ git_reset
42
+ end
43
+ end
44
+
45
+ def unresolved_conflicts
46
+ @git.unresolved_conflicts
47
+ end
48
+
49
+ def merge_conflicted
50
+ @git.run("checkout #{branch.conflict_sha}")
51
+ @git.run("merge #{@git.remote}/#{working_branch}")
52
+ end
53
+
54
+ def git_reset
55
+ @git.run("reset --hard HEAD")
56
+ end
57
+
58
+ def rerere
59
+ @git.run("rerere")
60
+ end
61
+
62
+ def bash_message
63
+ puts "\nNote: You are in a special flash_flow directory (#{Dir.pwd}). The files still open in your editor will not reflect the merge conflicts, open them from this shell to get the conflicted versions.\n\nPlease fix the following conflicts and then 'exit':\n#{unresolved_conflicts.join("\n")}\n\n"
64
+ end
65
+
66
+ def launch_bash
67
+ bash_message
68
+
69
+ with_init_file do |file|
70
+ system("bash --init-file #{file} -i")
71
+ end
72
+ end
73
+
74
+ def with_init_file
75
+ filename = '.flash_flow_init'
76
+ File.open(filename, 'w') do |f|
77
+ f.puts(init_file_contents)
78
+ end
79
+
80
+ yield filename
81
+
82
+ File.delete(filename)
83
+ end
84
+
85
+ def manual_not_merged_instructions
86
+ <<-EOS
87
+
88
+ Run the following commands to fix the merge conflict and then re-run flash_flow:
89
+ pushd #{flash_flow_directory}
90
+ git checkout #{branch.conflict_sha}
91
+ git merge #{working_branch}
92
+ # Resolve the conflicts
93
+ git add <conflicted files>
94
+ git commit --no-edit
95
+ popd
96
+
97
+ EOS
98
+ end
99
+
100
+ private
101
+
102
+ def data
103
+ @data ||= Data::Base.new({}, @branch_info_file, @git, logger: @logger)
104
+ end
105
+
106
+ def branch
107
+ @branch ||= data.saved_branches.detect { |branch| branch.ref == working_branch }
108
+ end
109
+
110
+ def working_branch
111
+ @git.working_branch
112
+ end
113
+
114
+ def in_working_branch
115
+ @git.in_dir do
116
+ @git.in_branch(working_branch) do
117
+ yield
118
+ end
119
+ end
120
+ end
121
+
122
+ def flash_flow_directory
123
+ @git.flash_flow_dir
124
+ end
125
+
126
+ def init_file_contents
127
+ <<-EOS
128
+ # Commented this one out because it was causing lots of spurious "saving session..." type messages
129
+ # [[ -s /etc/profile ]] && source /etc/profile
130
+ [[ -s ~/.bash_profile ]] && source ~/.bash_profile
131
+ [[ -s ~/.bash_login ]] && source ~/.bash_login
132
+ [[ -s ~/.profile ]] && source ~/.profile
133
+ [[ -s ~/.bashrc ]] && source ~/.bashrc
134
+
135
+ PS1='flash_flow resolve: (type "exit" after your conflicts are resolved)$ '
136
+ EOS
137
+ end
138
+
139
+ def check_for_conflict
140
+ raise NothingToResolve.new("The current branch (#{working_branch}) does not appear to be in conflict.") unless branch.conflict_sha
141
+ end
142
+ end
143
+ end