daq_flow 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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