intello-shipit-cli 0.6.0.rc1

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.
@@ -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