zenflow 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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