zenflow 0.8.0

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 (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.zenflow +10 -0
  7. data/CHANGELOG.md +0 -0
  8. data/Gemfile +3 -0
  9. data/Gemfile.lock +95 -0
  10. data/Guardfile +6 -0
  11. data/LICENSE.md +22 -0
  12. data/README.markdown +92 -0
  13. data/VERSION.yml +5 -0
  14. data/bin/zenflow +17 -0
  15. data/lib/zenflow.rb +35 -0
  16. data/lib/zenflow/cli.rb +130 -0
  17. data/lib/zenflow/commands/deploy.rb +33 -0
  18. data/lib/zenflow/commands/feature.rb +10 -0
  19. data/lib/zenflow/commands/hotfix.rb +15 -0
  20. data/lib/zenflow/commands/release.rb +16 -0
  21. data/lib/zenflow/commands/reviews.rb +12 -0
  22. data/lib/zenflow/helpers/ask.rb +66 -0
  23. data/lib/zenflow/helpers/branch.rb +80 -0
  24. data/lib/zenflow/helpers/branch_command.rb +95 -0
  25. data/lib/zenflow/helpers/branch_commands/abort.rb +21 -0
  26. data/lib/zenflow/helpers/branch_commands/branches.rb +21 -0
  27. data/lib/zenflow/helpers/branch_commands/compare.rb +20 -0
  28. data/lib/zenflow/helpers/branch_commands/deploy.rb +29 -0
  29. data/lib/zenflow/helpers/branch_commands/diff.rb +19 -0
  30. data/lib/zenflow/helpers/branch_commands/finish.rb +68 -0
  31. data/lib/zenflow/helpers/branch_commands/review.rb +58 -0
  32. data/lib/zenflow/helpers/branch_commands/start.rb +39 -0
  33. data/lib/zenflow/helpers/branch_commands/update.rb +22 -0
  34. data/lib/zenflow/helpers/changelog.rb +100 -0
  35. data/lib/zenflow/helpers/config.rb +36 -0
  36. data/lib/zenflow/helpers/github.rb +40 -0
  37. data/lib/zenflow/helpers/help.rb +37 -0
  38. data/lib/zenflow/helpers/log.rb +21 -0
  39. data/lib/zenflow/helpers/pull_request.rb +68 -0
  40. data/lib/zenflow/helpers/repo.rb +13 -0
  41. data/lib/zenflow/helpers/shell.rb +89 -0
  42. data/lib/zenflow/helpers/version.rb +87 -0
  43. data/lib/zenflow/version.rb +3 -0
  44. data/spec/fixtures/VERSION.yml +5 -0
  45. data/spec/fixtures/cassettes/create_bad_pull_request.yml +57 -0
  46. data/spec/fixtures/cassettes/create_pull_request.yml +68 -0
  47. data/spec/fixtures/cassettes/existing_pull_request.yml +145 -0
  48. data/spec/fixtures/cassettes/pull_request_by_ref.yml +145 -0
  49. data/spec/fixtures/cassettes/pull_request_find.yml +70 -0
  50. data/spec/fixtures/cassettes/pull_request_for_non-existent_ref.yml +145 -0
  51. data/spec/fixtures/cassettes/pull_request_list.yml +74 -0
  52. data/spec/fixtures/cassettes/unexisting_pull_request.yml +145 -0
  53. data/spec/spec_helper.rb +36 -0
  54. data/spec/support/shared_examples_for_version_number.rb +19 -0
  55. data/spec/zenflow/commands/deploy_spec.rb +48 -0
  56. data/spec/zenflow/commands/feature_spec.rb +11 -0
  57. data/spec/zenflow/commands/hotfix_spec.rb +14 -0
  58. data/spec/zenflow/commands/release_spec.rb +15 -0
  59. data/spec/zenflow/commands/reviews_spec.rb +15 -0
  60. data/spec/zenflow/helpers/ask_spec.rb +115 -0
  61. data/spec/zenflow/helpers/branch_command_spec.rb +310 -0
  62. data/spec/zenflow/helpers/branch_spec.rb +300 -0
  63. data/spec/zenflow/helpers/changelog_spec.rb +188 -0
  64. data/spec/zenflow/helpers/cli_spec.rb +277 -0
  65. data/spec/zenflow/helpers/github_spec.rb +45 -0
  66. data/spec/zenflow/helpers/help_spec.rb +36 -0
  67. data/spec/zenflow/helpers/log_spec.rb +31 -0
  68. data/spec/zenflow/helpers/pull_request_spec.rb +108 -0
  69. data/spec/zenflow/helpers/shell_spec.rb +135 -0
  70. data/spec/zenflow/helpers/version_spec.rb +111 -0
  71. data/zenflow.gemspec +33 -0
  72. metadata +273 -0
@@ -0,0 +1,68 @@
1
+ module Zenflow
2
+ module BranchCommands
3
+ module Finish
4
+
5
+ def self.included(thor)
6
+ thor.class_eval do
7
+
8
+ desc "finish", "Finish the branch and close the code review"
9
+ option :offline, type: :boolean, desc: "Runs in offline mode"
10
+ def finish
11
+ branch_name
12
+ confirm(:confirm_staging, "Has this been tested in a staging environment first?",
13
+ "Sorry, deploy to a staging environment first")
14
+ confirm(:confirm_review, "Has this been code reviewed yet?",
15
+ "Please have someone look at this first")
16
+ update_branch_from_destination
17
+ update_version_and_changelog(version, changelog)
18
+ merge_branch_into_destination
19
+ create_tag
20
+ delete_branches
21
+ end
22
+
23
+ no_commands do
24
+ def confirm(confirmation, question, failure_response)
25
+ return unless Zenflow::Config[confirmation]
26
+ if Zenflow::Ask(question, options: ["Y", "n"], default: "Y") == "n"
27
+ Zenflow::Log(failure_response, color: :red)
28
+ exit(1)
29
+ end
30
+ end
31
+
32
+ def update_version_and_changelog(version, changelog)
33
+ if version
34
+ Zenflow::Version.update(version)
35
+ end
36
+ if changelog
37
+ @change = Zenflow::Changelog.update(rotate: (changelog == :rotate), name: branch_name)
38
+ end
39
+ end
40
+
41
+ def create_tag
42
+ return unless tag
43
+ Zenflow::Branch.tag(Zenflow::Version.current.to_s, @change)
44
+ Zenflow::Branch.push(:tags) if !options[:offline]
45
+ end
46
+
47
+ def update_branch_from_destination
48
+ destination = (branch(:destination) || branch(:source))
49
+ Zenflow::Branch.update(destination) if !options[:offline]
50
+ Zenflow::Branch.checkout("#{flow}/#{branch_name}")
51
+ Zenflow::Branch.merge(destination)
52
+ end
53
+
54
+ def merge_branch_into_destination
55
+ [branch(:source), branch(:destination)].compact.each do |finish|
56
+ Zenflow::Branch.checkout(finish)
57
+ Zenflow::Branch.merge("#{flow}/#{branch_name}")
58
+ Zenflow::Branch.push(finish) if !options[:offline]
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ module Zenflow
2
+ module BranchCommands
3
+ module Review
4
+
5
+ def self.included(thor)
6
+ thor.class_eval do
7
+
8
+ desc "review", "Start a code review."
9
+ def review
10
+ branch_name
11
+ create_pull_request
12
+ end
13
+
14
+ no_commands do
15
+ def create_pull_request
16
+ already_created?(Zenflow::PullRequest.find_by_ref("#{flow}/#{branch_name}"))
17
+
18
+ pull = Zenflow::PullRequest.create(
19
+ base: branch(:source),
20
+ head: "#{flow}/#{branch_name}",
21
+ title: "#{flow}: #{branch_name}",
22
+ body: Zenflow::Ask("Describe this #{flow}:", required: true)
23
+ )
24
+
25
+ return handle_invalid_pull_request(pull) unless pull.valid?
26
+
27
+ Zenflow::Log("Pull request was created!")
28
+ Zenflow::Log(pull["html_url"], indent: true, color: false)
29
+ Zenflow::Shell["open #{pull['html_url']}"]
30
+ end
31
+
32
+ def already_created?(pull)
33
+ return unless pull
34
+ Zenflow::Log("A pull request for #{flow}/#{branch_name} already exists", color: :red)
35
+ Zenflow::Log(pull[:html_url], indent: true, color: false)
36
+ exit(1)
37
+ end
38
+
39
+ def handle_invalid_pull_request(pull)
40
+ Zenflow::Log("There was a problem creating the pull request:", color: :red)
41
+ if pull["errors"]
42
+ pull["errors"].each do |error|
43
+ Zenflow::Log("* #{error['message'].gsub(/^base\s*/,'')}", indent: true, color: :red)
44
+ end
45
+ elsif pull["message"]
46
+ Zenflow::Log("* #{pull['message']}", indent: true, color: :red)
47
+ else
48
+ Zenflow::Log(" * unexpected failure, both 'errors' and 'message' were empty in the response")
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ module Zenflow
2
+ module BranchCommands
3
+ module Start
4
+
5
+ def self.included(thor)
6
+ thor.class_eval do
7
+
8
+ desc "start [NAME]", "Start a branch"
9
+ option :offline, type: :boolean, desc: "Runs in offline mode"
10
+ def start(name=nil)
11
+ @branch_name = Zenflow::Ask("Name of the #{flow}:",
12
+ required: true,
13
+ validate: /^[-0-9a-z]+$/,
14
+ error_message: "Names can only contain dashes, 0-9, and a-z",
15
+ response: name).downcase
16
+
17
+ create_new_branch(options[:offline])
18
+ end
19
+
20
+ no_commands do
21
+ def create_new_branch(offline=false)
22
+ if !offline
23
+ Zenflow::Branch.update(branch(:source))
24
+ Zenflow::Branch.create("#{flow}/#{branch_name}", branch(:source))
25
+ Zenflow::Branch.push("#{flow}/#{branch_name}")
26
+ Zenflow::Branch.track("#{flow}/#{branch_name}")
27
+ else
28
+ Zenflow::Branch.checkout(branch(:source))
29
+ Zenflow::Branch.create("#{flow}/#{branch_name}", branch(:source))
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ module Zenflow
2
+ module BranchCommands
3
+ module Update
4
+
5
+ def self.included(thor)
6
+ thor.class_eval do
7
+
8
+ desc "update", "Update the branch to the latest code"
9
+ option :offline, type: :boolean, desc: "Runs in offline mode"
10
+ def update
11
+ branch_name
12
+ Zenflow::Branch.update(branch(:source)) if !options[:offline]
13
+ Zenflow::Branch.checkout("#{flow}/#{branch_name}")
14
+ Zenflow::Branch.merge(branch(:source))
15
+ end
16
+
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,100 @@
1
+ module Zenflow
2
+ module Changelog
3
+ class << self
4
+
5
+ def exist?
6
+ File.exist?("CHANGELOG.md")
7
+ end
8
+
9
+ def update(options={})
10
+ return unless exist?
11
+ change = prompt_for_change(options)
12
+ if change
13
+ prepend_change_to_changelog(change, options)
14
+ else
15
+ rotate(:commit => true) if options[:rotate]
16
+ end
17
+ change
18
+ end
19
+
20
+ def prompt_for_change(options={})
21
+ required = ' (optional)' if options[:required] == false
22
+ Zenflow::Ask("Add one line to the changelog#{required}:", :required => !(options[:required] == false))
23
+ end
24
+
25
+ def prepend_change_to_changelog(change, options={})
26
+ return unless exist?
27
+ new_changes = Zenflow::Shell.shell_escape_for_single_quoting(change)
28
+ File.open("CHANGELOG.md", "w") do |f|
29
+ f.write prepended_changelog(new_changes)
30
+ end
31
+ rotate(:name => options[:name]) if options[:rotate]
32
+ Zenflow::Shell["git add . && git commit -a -m 'Adding line to CHANGELOG: #{new_changes}'"]
33
+ end
34
+
35
+ def prepended_changelog(new_changes)
36
+ existing_changes, changelog = get_changes
37
+
38
+ <<-EOS
39
+ #{new_changes}
40
+ #{existing_changes}
41
+ --------------------------------------------------------------------------------
42
+ #{changelog}
43
+ EOS
44
+ end
45
+
46
+ def rotate(options={})
47
+ return unless changelog = rotated_changelog(options)
48
+ Zenflow::Log("Managing changelog for version #{Zenflow::Version.current} / #{Time.now.strftime('%Y-%m-%d')} #{"/ " + options[:name] + " " if options[:name]}")
49
+
50
+ File.open("CHANGELOG.md", "w") do |f|
51
+ f.write changelog
52
+ end
53
+ Zenflow::Shell["git add CHANGELOG.md && git commit -a -m 'Rotating CHANGELOG.'"] if options[:commit]
54
+ end
55
+
56
+ def rotated_changelog(options={})
57
+ changes, changelog = get_changes
58
+ return if changes.nil?
59
+ <<-EOS
60
+ #{changelog}
61
+
62
+ #{row_name(options[:name])}
63
+ #{changes}
64
+ EOS
65
+ end
66
+
67
+ def get_changes
68
+ return unless exist?
69
+ changelog = File.read("CHANGELOG.md").strip
70
+ changes = changelog.split("--------------------------------------------------------------------------------")[0]
71
+ return if changes.strip.empty?
72
+ changelog = changelog.sub(changes, "")
73
+ return changes.strip, changelog
74
+ end
75
+
76
+ def row_name(name=nil)
77
+ formatted_name = "/ #{name} " if name
78
+ "---- #{Zenflow::Version.current} / #{Time.now.strftime('%Y-%m-%d')} #{formatted_name}".ljust(80, "-")
79
+ end
80
+
81
+ def create
82
+ File.open("CHANGELOG.md", "w") do |f|
83
+ f.write changelog_template
84
+ end
85
+ end
86
+
87
+ def changelog_template
88
+ <<-EOS
89
+ --------------------------------------------------------------------------------
90
+ ^ ADD NEW CHANGES ABOVE ^
91
+ --------------------------------------------------------------------------------
92
+
93
+ CHANGELOG
94
+ =========
95
+
96
+ EOS
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,36 @@
1
+ module Zenflow
2
+ class Config
3
+ class << self
4
+ attr_accessor :config
5
+
6
+ CONFIG_FILE = "#{Dir.pwd}/.zenflow"
7
+
8
+ def load!
9
+ @config = {}
10
+ @config = YAML.load_file(CONFIG_FILE) if configured?
11
+ end
12
+
13
+ def save!
14
+ File.open(CONFIG_FILE, "w") do |out|
15
+ YAML.dump(@config, out)
16
+ end
17
+ end
18
+
19
+ def [](key)
20
+ load!
21
+ @config[key.to_s]
22
+ end
23
+
24
+ def []=(key, value)
25
+ load!
26
+ @config[key.to_s] = value
27
+ save!
28
+ end
29
+
30
+ def configured?
31
+ File.exist?(CONFIG_FILE)
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ module Zenflow
2
+
3
+ module Github
4
+ def self.user
5
+ Zenflow::Shell.run('git config --get github.user', silent: true).chomp
6
+ end
7
+
8
+ def self.zenflow_token
9
+ zenflow_token = Zenflow::Shell.run('git config --get zenflow.token', silent: true).chomp
10
+ zenflow_token = nil if zenflow_token.to_s.strip == ''
11
+ zenflow_token
12
+ end
13
+
14
+ def self.authorize
15
+ Zenflow::Log("Authorizing with GitHub... Enter your GitHub password.")
16
+ oauth_response = JSON.parse(Zenflow::Shell.run(%{curl -u "#{Zenflow::Github.user}" https://api.github.com/authorizations -d '{"scopes":["repo"], "note":"Zenflow"}' --silent}, silent: true))
17
+ if oauth_response['token']
18
+ Zenflow::Shell.run("git config --global zenflow.token #{oauth_response['token']}", silent: true)
19
+ Zenflow::Log("Authorized!")
20
+ else
21
+ Zenflow::Log("Something went wrong. Error from GitHub was: #{oauth_response['message']}")
22
+ return
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.set_user
28
+ username = Zenflow::Ask("What is your Github username?")
29
+ Zenflow::Shell.run("git config --global github.user #{username}", silent: true)
30
+ end
31
+
32
+ class GithubRequest
33
+ include HTTParty
34
+ base_uri "https://api.github.com/repos/#{Zenflow::Repo.slug}"
35
+ format :json
36
+ headers "Authorization" => "token #{Zenflow::Github.zenflow_token}"
37
+ headers "User-Agent" => "Zencoder/Zenflow-#{VERSION}"
38
+ end
39
+
40
+ end
@@ -0,0 +1,37 @@
1
+ module Zenflow
2
+
3
+ def self.Help(options={})
4
+ Zenflow::Help.new(options)
5
+ end
6
+
7
+ class Help
8
+ attr_accessor :options
9
+
10
+ def initialize(options={})
11
+ @options = options
12
+ end
13
+
14
+ def title(text)
15
+ "- #{text} ".ljust(40, "-").cyan
16
+ end
17
+
18
+ def banner
19
+ help = []
20
+ help << "#{title("Summary")}\n#{options[:summary]}" if options[:summary]
21
+ help << "#{title("Usage")}\n#{options[:usage]}" if options[:usage]
22
+ help << "#{title("Available Commands")}\n#{options[:commands]}" if options[:commands]
23
+ help << "#{title("Options")}"
24
+ help.join("\n\n")
25
+ end
26
+
27
+ def unknown_command
28
+ if options[:command].nil?
29
+ Zenflow::Log("Missing command", :color => :red)
30
+ else
31
+ Zenflow::Log("Unknown command #{options[:command].inspect}", :color => :red)
32
+ end
33
+ exit(1)
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,21 @@
1
+ module Zenflow
2
+
3
+ LOG_PATH = File.join(Dir.pwd, ".zenflow-log")
4
+
5
+ def self.Log(message, options={})
6
+ output = ""
7
+ output << " " if options[:indent]
8
+ output << "-----> " if !(options[:arrows] === false)
9
+ output << message
10
+ LogToFile(output)
11
+ output = output.send(options[:color] || :cyan) unless options[:color] == false
12
+ puts output
13
+ end
14
+
15
+ def self.LogToFile(message)
16
+ File.open(LOG_PATH, "a") do |f|
17
+ f.write(message+"\n")
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,68 @@
1
+ module Zenflow
2
+ class PullRequest
3
+
4
+ class << self
5
+ def list
6
+ response = Zenflow::GithubRequest.get("/pulls").parsed_response
7
+ response.map{ |pull| new(pull) }
8
+ end
9
+
10
+ def find(number)
11
+ new(Zenflow::GithubRequest.get("/pulls/#{number}").parsed_response["pull"])
12
+ end
13
+
14
+ def find_by_ref(ref, options={})
15
+ Zenflow::Log("Looking up pull request for #{ref}") unless options[:silent]
16
+ if !list.nil?
17
+ pull = list.detect do |p|
18
+ p["head"]["ref"] == ref
19
+ end
20
+ if pull
21
+ new(pull)
22
+ end
23
+ end
24
+ end
25
+
26
+ def find_by_ref!(ref)
27
+ if pull = find_by_ref(ref)
28
+ new(pull)
29
+ else
30
+ Zenflow::Log("No open pull request was found for #{ref}", color: :red)
31
+ exit(1)
32
+ end
33
+ end
34
+
35
+ def exist?(ref)
36
+ !!find_by_ref(ref)
37
+ end
38
+
39
+ def create(options={})
40
+ response = Zenflow::GithubRequest.post("/pulls",
41
+ body: {
42
+ "base" => options[:base],
43
+ "head" => options[:head],
44
+ "title" => options[:title],
45
+ "body" => options[:body]
46
+ }.to_json
47
+ )
48
+ new(response.parsed_response)
49
+ end
50
+ end
51
+
52
+
53
+ attr_reader :pull
54
+
55
+ def initialize(pull)
56
+ @pull = pull || {}
57
+ end
58
+
59
+ def valid?
60
+ !pull["errors"] && pull['html_url']
61
+ end
62
+
63
+ def [](key)
64
+ pull[key.to_s]
65
+ end
66
+
67
+ end
68
+ end