intello-shipit-cli 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ require "uri"
2
+
3
+ module Shipit
4
+ module Cli
5
+ # This class maintains all system-wide configuration for *shipit*. It properly
6
+ # applies the correct source of configuration values based on the different contexts and
7
+ # also makes sure that compulsory data are not missing.
8
+ #
9
+ class Configuration
10
+ # List of all the configuration attributes stored for use within the gem
11
+ ATTRIBUTES = [:endpoint, :private_token, :protected_branches]
12
+
13
+ attr_accessor :endpoint, :private_token, :protected_branches
14
+
15
+ # Apply a configuration hash to a configuration instance
16
+ #
17
+ # @example Override one of the configuration attributes
18
+ # config = Shipit::Cli::Configuration.new
19
+ # config.apply(private_token: 'supersecret')
20
+ # config.private_token #=> "supersecret"
21
+ #
22
+ # @param attributes [Hash] list of key/values to apply to the configuration
23
+ # @return [Object] the configuration object
24
+ #
25
+ def apply(attributes = {})
26
+ prepared_attributes = prepare_attributes attributes
27
+ prepared_attributes.each_pair do |attribute, value|
28
+ send("#{attribute}=", value)
29
+ end
30
+ self
31
+ end
32
+
33
+ # The configuration instance formatted as a stringified hash
34
+ #
35
+ # @example Override one of the configuration attributes
36
+ # config = Shipit::Cli::Configuration.new
37
+ # config.to_hash
38
+ # #=> { "endpoint" => "https://gitlab.example.com/api/v3", "private_token" => "supersecret" }
39
+ #
40
+ # @return [Hash] the configuration object as a Hash
41
+ #
42
+ def to_hash
43
+ config_hash = ATTRIBUTES.inject({}) do |hash, attr|
44
+ hash["#{attr}"] = instance_variable_get("@#{attr}")
45
+ hash
46
+ end
47
+ Shipit::Cli::Sanitizer.symbolize config_hash
48
+ end
49
+
50
+ # Write a configuration summary to STDOUT, useful for output in the CLI
51
+ #
52
+ def to_stdout
53
+ to_hash.each_pair do |attribute, value|
54
+ puts format("%-20s %-50s", "#{attribute}:", value)
55
+ end
56
+ nil
57
+ end
58
+
59
+ def motd_list
60
+ File.readlines(File.expand_path('../../motd', __FILE__)).map(&:chomp)
61
+ end
62
+
63
+ private
64
+
65
+ # Symbolize keys and remove nil or duplicate attributes
66
+ # The attributes usually passed to our configuration class by the CLI
67
+ # are usually full of duplicates and unconsistant keys, we make sure
68
+ # to clean up that input, before doing any configuration work.
69
+ #
70
+ # @param attributes [Hash] list of key/values
71
+ # @return [Hash] a clean list of key/values
72
+ #
73
+ def prepare_attributes(attributes)
74
+ # Convert string keys to symbols
75
+ symboled_attributes = Shipit::Cli::Sanitizer.symbolize attributes
76
+ # Clean up user_attributes from unwanted, nil and duplicate options
77
+ symboled_attributes.select { |key, _| ATTRIBUTES.include? key }.delete_if { |_, v| v.nil? }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,58 @@
1
+ require "yaml"
2
+
3
+ module Shipit
4
+ module Cli
5
+ class ConfigurationFile
6
+ ATTR_READER = [:endpoint, :private_token, :protected_branches]
7
+ attr_reader(*ATTR_READER)
8
+
9
+ def initialize(path, configuration = nil)
10
+ @path = path
11
+ @file = load_file
12
+ @configuration = configuration || load_configuration
13
+ end
14
+
15
+ def exist?
16
+ File.exist?(@path)
17
+ end
18
+
19
+ def to_hash
20
+ config_hash = ATTR_READER.inject({}) do |hash, attr|
21
+ hash["#{attr}"] = instance_variable_get("@#{attr}")
22
+ hash
23
+ end
24
+ Shipit::Cli::Sanitizer.symbolize config_hash
25
+ end
26
+
27
+ def persist
28
+ File.open(@path, "w") do |f|
29
+ f.write @configuration.to_yaml
30
+ end
31
+ reload!
32
+ end
33
+
34
+ def reload!
35
+ @file = load_file
36
+ @configuration = load_configuration
37
+ end
38
+
39
+ private
40
+
41
+ def load_file
42
+ if exist?
43
+ Shipit::Cli::Sanitizer.symbolize YAML.load_file(@path)
44
+ else
45
+ {}
46
+ end
47
+ end
48
+
49
+ def load_configuration
50
+ if @file
51
+ ATTR_READER.each do |attr|
52
+ instance_variable_set "@#{attr}", @file[attr]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ module Shipit
2
+ module Cli
3
+ class Git
4
+ GIT = ENV["SHIPIT_GIT"] || "git"
5
+ ORIGIN = ENV["SHIPIT_ORIGIN"] || "origin"
6
+ VERBOSE = ENV["SHIPIT_GIT_VERBOSE"] ? "--verbose" : "--quiet"
7
+
8
+ COMMANDS = {
9
+ new: {
10
+ desc: "create new branch `branch`",
11
+ commands: [
12
+ '"#{GIT} push #{origin} #{current_branch}:refs/heads/#{branch} --no-verify #{VERBOSE}"',
13
+ '"#{GIT} fetch #{origin} #{VERBOSE}"',
14
+ '"#{GIT} branch --track #{branch} #{origin}/#{branch} #{VERBOSE}"',
15
+ '"#{GIT} checkout #{branch} #{VERBOSE}"',
16
+ '"#{GIT} commit --allow-empty -m \"#{message}\" #{VERBOSE}"',
17
+ '"#{GIT} push --no-verify #{VERBOSE}"'
18
+ ]
19
+ }
20
+ }
21
+
22
+ # Ex: Shipit::Cli::Git.run(command: :new, target_branch: "master", branch: "my-new-branch", message: "empty commit")
23
+ def self.run(opt)
24
+ if COMMANDS.keys.include?(opt[:command].to_sym)
25
+ current_branch = opt[:target_branch]
26
+ branch = opt[:branch]
27
+ origin = ORIGIN
28
+ message = opt.fetch(:message, "").gsub("`", "'")
29
+ dry_run = opt.fetch(:dry_run, false)
30
+ grepped_protected_branches = Array(Shipit::Cli.config.protected_branches)
31
+ .push("master")
32
+ .push("develop")
33
+ .push(get_current_branch)
34
+ .uniq.map{ |b| "| grep -v #{b} " }.join
35
+ COMMANDS[opt[:command].to_sym][:commands].map do |x|
36
+ command = exec_cmd(eval(x), dry_run)
37
+ exit_now!("There was an error running the last Git command. See the trace for more info.") if dry_run == false && command == false
38
+ end
39
+ else
40
+ false
41
+ end
42
+ end
43
+
44
+ def self.get_current_branch
45
+ (`#{GIT} branch 2> /dev/null | grep '^\*'`).gsub(/[\*\s]/, "")
46
+ end
47
+
48
+ def self.exec_cmd(str, dry_run = false)
49
+ return true unless str
50
+ if dry_run
51
+ puts " - #{str}"
52
+ else
53
+ system("#{str}")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ module Shipit
2
+ module Cli
3
+ class Sanitizer
4
+ def self.symbolize(obj)
5
+ return obj.inject({}) do |memo, (k, v)|
6
+ memo.tap { |m| m[k.to_sym] = symbolize(v) }
7
+ end if obj.is_a? Hash
8
+
9
+ return obj.inject([]) do |memo, v|
10
+ memo << symbolize(v)
11
+ memo
12
+ end if obj.is_a? Array
13
+
14
+ obj
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,48 @@
1
+ require "json"
2
+ require "uri"
3
+ require "net/http"
4
+ require "net/https"
5
+
6
+ module Shipit
7
+ module Cli
8
+ class Server
9
+ def initialize(url, options = {})
10
+ @uri = URI(url)
11
+ @scheme = @uri.scheme || "https"
12
+ @method = options[:method] || :get
13
+ @body = options[:body]
14
+ @private_token = options[:private_token] || Shipit::Cli.config.private_token
15
+ @authorization_header = "Token #{@private_token}"
16
+ @http = set_http(@uri, @scheme)
17
+ end
18
+
19
+ def request
20
+ case @method
21
+ when :post
22
+ @request = Net::HTTP::Post.new(@uri.request_uri, initheader = { "Content-Type" => "application/json" })
23
+ @request.body = @body.to_json
24
+ else
25
+ @request = Net::HTTP::Get.new(@uri.request_uri)
26
+ end
27
+
28
+ @request["authorization"] = @authorization_header
29
+ @request
30
+ end
31
+
32
+ def response
33
+ @response ||= @http.request(request)
34
+ end
35
+
36
+ private
37
+
38
+ def set_http(uri, scheme)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ if scheme == "https"
41
+ http.use_ssl = true
42
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
43
+ end
44
+ http
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ module Shipit
2
+ module Cli
3
+ VERSION = "0.6.0.rc1".freeze
4
+ end
5
+ end
@@ -0,0 +1,138 @@
1
+ require "active_support/all"
2
+
3
+ module Shipit
4
+ module Cli
5
+ class Work
6
+ attr_reader :client, :issue_id, :global_options
7
+
8
+ # @param issue_id [Integer] The issue GitLab IID
9
+ # @param target_branch [String] "master", "develop"...
10
+ # @param global_options [Hash] A hash of options
11
+ # - dry_run : If true will not perform the commands
12
+ # - client : an instance of the GitLab client
13
+ def initialize(issue_id, global_options)
14
+ @issue_id = issue_id
15
+ @global_options = global_options
16
+ @client = global_options.fetch(:client)
17
+ end
18
+
19
+ # 1. Get issue : https://docs.gitlab.com/ee/api/issues.html#single-issue
20
+ # 2. Validate issue properties
21
+ # 3. Create branch : https://docs.gitlab.com/ee/api/branches.html#create-repository-branch
22
+ # 4. First commit : https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions
23
+ # 5. New merge request : https://docs.gitlab.com/ee/api/merge_requests.html#create-mr
24
+ def perform(target_branch)
25
+ # 1. Load issue
26
+ issue = fetch_remote_issue(issue_id).to_hash
27
+ # 2. Validate issue
28
+ validate_issue(issue)
29
+ # 3. Add additional issue attributes
30
+ enhanced_issue = enhance_issue(issue, target_branch)
31
+ # 4. Create remote branch & checkout
32
+ create_source_branch(enhanced_issue)
33
+ # 5. Create merge request
34
+ create_merge_request(enhanced_issue)
35
+ # 6. Motivation!
36
+ puts ""
37
+ Shipit::Cli.ascii
38
+ rescue => e
39
+ exit_now! e
40
+ end
41
+
42
+ # @return [Boolean]
43
+ def dry_run?
44
+ global_options["dry_run"] == true
45
+ end
46
+
47
+ private
48
+
49
+ # @param id [Integer] The issue IID to fetch from GitLab
50
+ def fetch_remote_issue(id)
51
+ client.issue(project_path, id)
52
+ rescue Gitlab::Error::NotFound => e
53
+ exit_now! "Issue #{id} not found for project #{project_name} on the remote GitLab server."
54
+ end
55
+
56
+ def create_source_branch(issue)
57
+ commands = Shipit::Cli::Git.run(command: :new,
58
+ branch: issue["source_branch"],
59
+ message: issue["first_commit_message"],
60
+ target_branch: issue["target_branch"],
61
+ dry_run: global_options["dry_run"])
62
+ raise "There was an error creating your working branch.\nPROTIP: Review the previous trace and fix the errors before retrying." if !dry_run? && commands.any? { |c| c == false }
63
+ end
64
+
65
+ def create_merge_request(issue)
66
+ return if dry_run?
67
+ opts = issue.slice("source_branch", "target_branch", "assignee_id", "labels", "milestone_id", "remove_source_branch")
68
+ client.create_merge_request(project_path,
69
+ issue["merge_request_title"],
70
+ opts)
71
+ rescue Gitlab::Error => e
72
+ exit_now! "There was a problem creating your merge request on the GitLab server. Please do it manually.\nError : #{e.message}"
73
+ end
74
+
75
+ # @param issue [Hash] A hash of properties
76
+ # @return true if issue if valid
77
+ def validate_issue(issue)
78
+ errors = []
79
+ errors << "Assignee missing" if issue["assignees"].empty? # returns an array of assignees (Array of hash)
80
+ errors << "Milestone missing" if issue["milestone"].to_s.empty? # returns a hash with the milestone properties
81
+ errors << "Labels missing" if issue["labels"].empty? # returns an array of strings
82
+ exit_now!("Invalid remote GitLab issue. Please fix the following warnings : #{errors.join(", ")}") if errors.any?
83
+ end
84
+
85
+ def enhance_issue(issue, target_branch)
86
+ # 1. Build properties
87
+ issue_id = issue["iid"]
88
+ labels = issue["labels"].join(", ")
89
+ assignee = issue["assignees"].first
90
+ sane_title = issue["title"].gsub(/[^0-9A-Za-z ,.]/, '').strip
91
+ assignee_initials = assignee["name"].split.map(&:first).join.downcase.gsub(/[^a-z]/, '').strip
92
+ assignee_id = assignee["id"]
93
+ issue_title = sane_title.parameterize[0..20]
94
+ first_commit_message = "[#{labels}] Fixes ##{issue_id} - #{sane_title}"
95
+ mr_title = "[WIP] [#{labels}] ##{issue_id} - #{sane_title}"
96
+
97
+ # 2. Add to issue properties
98
+ issue["source_branch"] = [issue["labels"].first, assignee_initials, issue_id, issue_title].compact.join("-")
99
+ issue["first_commit_message"] = first_commit_message
100
+ issue["target_branch"] = target_branch
101
+ issue["merge_request_title"] = mr_title
102
+ issue["assignee_id"] = assignee_id
103
+ issue["milestone_id"] = issue["milestone"]["id"]
104
+ issue["remove_source_branch"] = true
105
+ issue["labels"] = labels
106
+ issue
107
+ end
108
+
109
+ # Branch names are constructed like so :
110
+ # {label}-{assignee initials}-{issue id}-{issue title}
111
+ #
112
+ # @return [String]
113
+ def define_source_branch_name
114
+ [first_label, assignee_initials, @id, issue_title].compact.join("-")
115
+ end
116
+
117
+ # @return [String]
118
+ def project_git_url
119
+ @project_git_url ||= `git config --local remote.origin.url`.chomp
120
+ end
121
+
122
+ # @return [String]
123
+ def project_name
124
+ @project_name ||= project_git_url.split("/").last.gsub(".git", "")
125
+ end
126
+
127
+ # @return [String]
128
+ def project_namespace
129
+ @project_namespace ||= project_git_url.split(":").last.split("/").first
130
+ end
131
+
132
+ # @return [String]
133
+ def project_path
134
+ @project_path ||= [project_namespace, project_name].join("/")
135
+ end
136
+ end
137
+ end
138
+ end
data/lib/shipit/cli.rb ADDED
@@ -0,0 +1,45 @@
1
+ require "shipit/cli/configuration"
2
+ require "shipit/cli/configuration_file"
3
+ require "shipit/cli/sanitizer"
4
+ require "shipit/cli/git"
5
+ require "shipit/cli/server"
6
+ require "shipit/cli/version"
7
+ require "shipit/cli/work"
8
+
9
+ module Shipit
10
+ module Cli
11
+ class << self
12
+ # Keep track of the configuration values set after a configuration
13
+ # has been applied
14
+ #
15
+ # @example Return a configuration value
16
+ # Shipit::Cli.config.foo #=> "bar"
17
+ #
18
+ # @return [Object] the configuration object
19
+ #
20
+ def config
21
+ @config ||= Shipit::Cli::Configuration.new
22
+ end
23
+
24
+ def configure(attributes = {})
25
+ config.apply attributes
26
+ end
27
+
28
+ def ascii
29
+ label = "*" + @config.motd_list.sample[0..37].upcase.center(38) + "*"
30
+
31
+ puts "****************************************"
32
+ puts label
33
+ puts "* *"
34
+ puts "* | | | *"
35
+ puts "* )_) )_) )_) *"
36
+ puts "* )___))___))___)\\ *"
37
+ puts "* )____)____)_____)\\ *"
38
+ puts "* _____|____|____|____\\__ *"
39
+ puts "*--------\\ /---------*"
40
+ puts "* ^^^^^^^^^^^^^^^^^^^^^^ *"
41
+ puts "****************************************"
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/shipit/motd ADDED
@@ -0,0 +1,15 @@
1
+ F*CK IT, SHIP IT
2
+ FAILURE IS ALWAYS AN OPTION
3
+ I can haz Ship It?
4
+ I dare you to ship. I dare you!
5
+ It's hip to ship
6
+ Keep it simple, ship it fast
7
+ NEVER GIVE UP, NEVER SURRENDER
8
+ REAL ARTISTS SHIP
9
+ Ship it baby!
10
+ TO ERR IS HUMAN BUT TO ARR IS PIRATE
11
+ IT'S GOING TO BE ALL RIGHT
12
+ IT'S GONNA BLOW THEM ALL AWAY
13
+ Smurferation, Smurferation, Smurf
14
+ All boundaries are conventions
15
+ Ship it or die trying
data/script/bootstrap ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # Usage: script/bootstrap
3
+ set -e
4
+
5
+ echo "* Install the project gems"
6
+ gem install bundler --conservative
7
+ bundle check || bundle install
8
+
9
+ echo "* Bootstrap finished."
data/script/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "shipit/cli"
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
data/script/package ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: script/package
3
+ # Updates the gemspec and builds a new gem in the pkg directory.
4
+
5
+ mkdir -p pkg
6
+ bundle exec gem build *.gemspec
7
+ mv *.gem pkg
data/script/release ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: script/release
3
+ # Build the package, tag a commit, push it to origin, and then release the
4
+ # package to our private gem repository.
5
+
6
+ # Let's source required ENV variables
7
+ source .rbenv-vars
8
+ set -e
9
+
10
+ # Get latest tag
11
+
12
+ # http://git-scm.com/docs/git-fetch
13
+ # `-t`: Fetch all tags from the remote
14
+ echo "Fetching origin with all remote tags..."
15
+ git fetch -t origin
16
+
17
+ latest_tag=$(git describe --always origin/master --abbrev=0 --match="v*")
18
+ latest_release=${latest_tag:1}
19
+ echo "Did you update the VERSION in lib/shipit-cli/version.rb ?"
20
+ read -p "It should be > $latest_release [y/N]: " response
21
+
22
+ if [[ $response =~ ^(y|Y)$ ]]; then
23
+ # Build the gem
24
+ echo "Preparing the gem package in pkg/*"
25
+ # Output format: intello-shipit-cli 1.0.2 built to pkg/intello-shipit-cli-1.0.2.gem.
26
+ version="$(script/package | grep Version: | awk '{print $2}')"
27
+ # Did we get a version?
28
+ if [ -z "$version" ]; then echo "You have to give me a semantic tag, like 1.0.0"; exit 1; fi
29
+ echo "Tagging your code to v$version"
30
+ echo ""
31
+
32
+ echo "Make sure you are GPG ready."
33
+ echo "You should have run this at one point:"
34
+ echo "$ git config --global user.signingkey [gpg-key-id]"
35
+ git tag -s "v$version" -m "Release v$version"
36
+ git push origin
37
+ git push origin "v$version"
38
+ echo ""
39
+ echo "Publishing to remote gem repository."
40
+ gem push pkg/intello-shipit-cli-${version}.gem
41
+ else
42
+ echo "You canceled the operation, nothing was done."
43
+ fi
data/script/test ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ echo "===> Running specs..."
6
+ bundle exec rspec
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "shipit/cli/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "intello-shipit-cli"
8
+ spec.version = Shipit::Cli::VERSION
9
+ spec.authors = ["Benjamin Thouret"]
10
+ spec.email = ["benjamin.thouret@intello.com"]
11
+
12
+ spec.summary = %w{Designed to simplify working with Gitlab,
13
+ its main purpose is to kickstart working on an issue.}
14
+ spec.homepage = "https://gitlab.com/intello/shipit-cli"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "gli", "~> 2.13"
23
+ spec.add_dependency "gitlab", "~> 4.3.0"
24
+
25
+ spec.add_development_dependency "bundler", "~> 2.0.1"
26
+ spec.add_development_dependency "guard"
27
+ spec.add_development_dependency "guard-ctags-bundler"
28
+ spec.add_development_dependency "guard-rspec"
29
+ spec.add_development_dependency "guard-rubocop"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec"
32
+ spec.add_development_dependency "rubocop"
33
+ spec.add_development_dependency "simplecov"
34
+ end