ninny 0.1.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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+
5
+ module Ninny
6
+ module Commands
7
+ class OutputDatedBranch < Ninny::Command
8
+ attr_reader :branch_type
9
+ def initialize(options)
10
+ @branch_type = options[:branch_type] || Git::STAGING_PREFIX
11
+ @options = options
12
+ end
13
+
14
+ def execute(input: $stdin, output: $stdout)
15
+ output.puts Ninny.git.latest_branch_for(branch_type)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+
5
+ module Ninny
6
+ module Commands
7
+ class PullRequestMerge < Ninny::Command
8
+ attr_accessor :pull_request_id, :options, :pull_request
9
+ attr_reader :branch_type
10
+
11
+ def initialize(pull_request_id, options)
12
+ @branch_type = options[:branch_type] || Ninny::Git::STAGING_PREFIX
13
+ self.pull_request_id = pull_request_id
14
+ self.options = options
15
+ end
16
+
17
+ def execute(input: $stdin, output: $stdout)
18
+ if (!pull_request_id)
19
+ current = Ninny.repo.current_pull_request
20
+ self.pull_request_id = current.number if current
21
+ end
22
+ self.pull_request_id ||= select_pull_request
23
+
24
+ check_out_branch
25
+ merge_pull_request
26
+ comment_about_merge
27
+ end
28
+
29
+ private def select_pull_request
30
+ choices = Ninny.repo.open_pull_requests.map { |pr| { name: pr.title, value: pr.number } }
31
+ prompt.select("Which #{Ninny.repo.pull_request_label}?", choices)
32
+ end
33
+
34
+ # Public: Check out the branch
35
+ def check_out_branch
36
+ branch_to_merge_into.checkout
37
+ rescue Ninny::Git::NoBranchOfType
38
+ prompt.say "No #{branch_type} branch available. Creating one now."
39
+ CreateDatedBranch.new(branch: branch_type).execute
40
+ end
41
+
42
+ # Public: Merge the pull request's branch into the checked-out branch
43
+ def merge_pull_request
44
+ Ninny.git.merge(pull_request.branch)
45
+ end
46
+
47
+ # Public: Comment that the pull request was merged into the branch
48
+ def comment_about_merge
49
+ pull_request.write_comment(comment_body)
50
+ end
51
+
52
+ # Public: The content of the comment to post when merged
53
+ #
54
+ # Returns a String
55
+ def comment_body
56
+ "Merged into #{branch_to_merge_into}."
57
+ end
58
+
59
+ # Public: Find the pull request
60
+ #
61
+ # Returns a Ninny::Repository::PullRequest
62
+ def pull_request
63
+ @pull_request ||= Ninny.repo.pull_request(pull_request_id)
64
+ end
65
+
66
+ # Public: Find the branch
67
+ #
68
+ # Returns a String
69
+ def branch_to_merge_into
70
+ @branch_to_merge_into ||= Ninny.git.latest_branch_for(branch_type)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+
5
+ module Ninny
6
+ module Commands
7
+ class Setup < Ninny::Command
8
+ attr_reader :config
9
+ def initialize(options)
10
+ @options = options
11
+ @config = Ninny.user_config
12
+ end
13
+
14
+ def execute(input: $stdin, output: $stdout)
15
+ try_reading_user_config
16
+
17
+ prompt_for_gitlab_private_token
18
+
19
+ config.write(force: true)
20
+ # Command logic goes here ...
21
+ output.puts "User config #{@result}"
22
+ end
23
+
24
+ def try_reading_user_config
25
+ begin
26
+ config.read
27
+ @result = 'updated'
28
+ rescue MissingUserConfig
29
+ @result = 'created'
30
+ end
31
+ end
32
+
33
+ def prompt_for_gitlab_private_token
34
+ new_token_text = config.gitlab_private_token ? ' new' : ''
35
+ if prompt.yes?("Do you have a#{new_token_text} gitlab private token?")
36
+ private_token = prompt.ask("Enter private token", required: true)
37
+ config.set(:gitlab_private_token, value: private_token)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+ require_relative 'pull_request_merge'
5
+
6
+ module Ninny
7
+ module Commands
8
+ class StageUp < PullRequestMerge
9
+ def initialize(pull_request_id, options)
10
+ super
11
+ @branch_type = Ninny::Git::STAGING_PREFIX
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+
5
+ module Ninny
6
+ module Commands
7
+ class StagingBranch < OutputDatedBranch
8
+ def initialize(options)
9
+ super
10
+ @branch_type = Git::STAGING_PREFIX
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/ninny/git.rb ADDED
@@ -0,0 +1,148 @@
1
+ module Ninny
2
+ class Git
3
+ extend Forwardable
4
+ NO_BRANCH = "(no branch)"
5
+ DEFAULT_DIRTY_MESSAGE = "Your Git index is not clean. Commit, stash, or otherwise clean up the index before continuing."
6
+ DIRTY_CONFIRM_MESSAGE = "Your Git index is not clean. Do you want to continue?"
7
+
8
+ # branch prefixes
9
+ DEPLOYABLE_PREFIX = "deployable"
10
+ STAGING_PREFIX = "staging"
11
+ QAREADY_PREFIX = "qaready"
12
+
13
+ def_delegators :git, :branch
14
+
15
+ attr_reader :git
16
+
17
+ def initialize
18
+ @git = ::Git.open(Dir.pwd)
19
+ end
20
+
21
+ def command(*args)
22
+ git.lib.send(:command, *args)
23
+ end
24
+
25
+ def current_branch
26
+ git.branch(current_branch_name)
27
+ end
28
+
29
+ def current_branch_name
30
+ name = git.current_branch
31
+ if name == NO_BRANCH
32
+ raise NotOnBranch, "Not currently checked out to a particular branch"
33
+ else
34
+ name
35
+ end
36
+ end
37
+
38
+ def merge(branch_name)
39
+ if_clean do
40
+ git.fetch
41
+ current_branch.merge("origin/#{branch_name}")
42
+ raise MergeFailed unless clean?
43
+ push
44
+ end
45
+ end
46
+
47
+ # Public: Push the current branch to GitHub
48
+ def push
49
+ if_clean do
50
+ git.push('origin', current_branch_name)
51
+ end
52
+ end
53
+
54
+ # Public: Pull the latest changes for the checked-out branch
55
+ def pull
56
+ if_clean do
57
+ command('pull')
58
+ end
59
+ end
60
+
61
+ # Public: Create a new branch from the given source
62
+ #
63
+ # new_branch_name - The name of the branch to create
64
+ # source_branch_name - The name of the branch to branch from
65
+ def new_branch(new_branch_name, source_branch_name)
66
+ git.fetch
67
+ command('branch', ['--no-track', new_branch_name, "origin/#{source_branch_name}"])
68
+ new_branch = branch(new_branch_name)
69
+ new_branch.checkout
70
+ command('push', ['-u', 'origin', new_branch_name])
71
+ end
72
+
73
+ # Public: Delete the given branch
74
+ #
75
+ # branch_name - The name of the branch to delete
76
+ def delete_branch(branch_name)
77
+ branch = branch_name.is_a?(::Git::Branch) ? branch_name : git.branch(branch_name)
78
+ git.push('origin', ":#{branch}")
79
+ branch.delete
80
+ end
81
+
82
+ # Public: The list of branches on GitHub
83
+ #
84
+ # Returns an Array of Strings containing the branch names
85
+ def remote_branches
86
+ git.fetch
87
+ git.branches.remote.map{ |branch| git.branch(branch.name) }.sort_by(&:name)
88
+ end
89
+
90
+ # Public: List of branches starting with the given string
91
+ #
92
+ # prefix - String to match branch names against
93
+ #
94
+ # Returns an Array of Branches containing the branch name
95
+ def branches_for(prefix)
96
+ remote_branches.select do |branch|
97
+ branch.name =~ /^#{prefix}/
98
+ end
99
+ end
100
+
101
+ # Public: Most recent branch starting with the given string
102
+ #
103
+ # prefix - String to match branch names against
104
+ #
105
+ # Returns an Array of Branches containing the branch name
106
+ def latest_branch_for(prefix)
107
+ branches_for(prefix).last || raise(NoBranchOfType, "No #{prefix} branch")
108
+ end
109
+
110
+
111
+ # Public: Whether the Git index is clean (has no uncommited changes)
112
+ #
113
+ # Returns a Boolean
114
+ def clean?
115
+ command('status', '--short').empty?
116
+ end
117
+
118
+ # Public: Perform the block if the Git index is clean
119
+ def if_clean(message=DEFAULT_DIRTY_MESSAGE)
120
+ if clean? || prompt.yes?(DIRTY_CONFIRM_MESSAGE)
121
+ yield
122
+ else
123
+ alert_dirty_index message
124
+ exit 1
125
+ end
126
+ end
127
+
128
+ # Public: Display the message and show the git status
129
+ def alert_dirty_index(message)
130
+ prompt.say " "
131
+ prompt.say message
132
+ prompt.say " "
133
+ prompt.say command('status')
134
+ raise DirtyIndex
135
+ end
136
+
137
+ def prompt(**options)
138
+ require 'tty-prompt'
139
+ TTY::Prompt.new(options)
140
+ end
141
+
142
+
143
+ # Exceptions
144
+ NotOnBranch = Class.new(StandardError)
145
+ NoBranchOfType = Class.new(StandardError)
146
+ DirtyIndex = Class.new(StandardError)
147
+ end
148
+ end
@@ -0,0 +1,48 @@
1
+ module Ninny
2
+ class ProjectConfig
3
+ attr_reader :config
4
+
5
+ def initialize
6
+ @config = TTY::Config.new
7
+ @config.filename = '.ninny'
8
+ @config.extname = '.yml'
9
+ @config.prepend_path Dir.pwd
10
+ @config.read
11
+ end
12
+
13
+ def write(*args)
14
+ config.write(*args)
15
+ end
16
+
17
+ def set(*args)
18
+ config.set(*args)
19
+ end
20
+
21
+ def repo_type
22
+ config.fetch(:repo_type)
23
+ end
24
+
25
+ def deploy_branch
26
+ config.fetch(:deploy_branch)
27
+ end
28
+
29
+ def gitlab_project_id
30
+ config.fetch(:gitlab_project_id)
31
+ end
32
+
33
+ def gitlab_endpoint
34
+ config.fetch(:gitlab_endpoint, default: "https://gitlab.com/api/v4")
35
+ end
36
+
37
+ def repo
38
+ return unless repo_type
39
+
40
+ repo_class = { gitlab: Repository::Gitlab }[repo_type.to_sym]
41
+ repo_class && repo_class.new
42
+ end
43
+
44
+ def self.config
45
+ new
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ module Ninny
2
+ module Repository
3
+ class Gitlab
4
+ attr_reader :gitlab
5
+ attr_reader :project_id
6
+ def initialize
7
+ @gitlab = ::Gitlab.client(endpoint: Ninny.project_config.gitlab_endpoint,
8
+ private_token: Ninny.user_config.gitlab_private_token)
9
+ @project_id = Ninny.project_config.gitlab_project_id
10
+ end
11
+
12
+ def current_pull_request
13
+ to_pr(gitlab.merge_requests(project_id, { source_branch: Ninny.git.current_branch.name,
14
+ target_branch: Ninny.project_config.deploy_branch })
15
+ .last)
16
+ end
17
+
18
+ def pull_request_label
19
+ 'Merge Request'
20
+ end
21
+
22
+ def open_pull_requests
23
+ gitlab.merge_requests(project_id, { state: 'opened' }).map{ |mr| to_pr(mr) }
24
+ end
25
+
26
+ def pull_request(id)
27
+ to_pr(gitlab.merge_request(project_id, id))
28
+ end
29
+
30
+ def create_merge_request_note(id, body)
31
+ gitlab.create_merge_request_note(project_id, id, body)
32
+ end
33
+
34
+ private def to_pr(request)
35
+ request && PullRequest.new(number: request.iid,
36
+ title: request.title,
37
+ branch: request.source_branch,
38
+ description: request.description,
39
+ comment_lambda: ->(body) { Ninny.repo.create_merge_request_note(request.iid, body) })
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ module Ninny
2
+ module Repository
3
+ class PullRequest
4
+ attr_accessor :number, :title, :description, :branch, :comment_lambda
5
+ def initialize(opts={})
6
+ self.number = opts[:number]
7
+ self.title = opts[:title]
8
+ self.description = opts[:description]
9
+ self.branch = opts[:branch]
10
+ self.comment_lambda = opts[:comment_lambda]
11
+ end
12
+
13
+ def write_comment(body)
14
+ self.comment_lambda.call(body)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,47 @@
1
+ module Ninny
2
+ class UserConfig
3
+ attr_reader :config
4
+
5
+ def initialize
6
+ @config = TTY::Config.new
7
+ @config.filename = '.ninny'
8
+ @config.extname = '.yml'
9
+ @config.prepend_path Dir.home
10
+ @read = false
11
+ end
12
+
13
+ def write(*args)
14
+ config.write(*args)
15
+ end
16
+
17
+ def set(*args)
18
+ config.set(*args)
19
+ end
20
+
21
+ def gitlab_private_token
22
+ with_read do
23
+ config.fetch(:gitlab_private_token)
24
+ end
25
+ end
26
+
27
+ def read
28
+ config.read unless @read
29
+ end
30
+
31
+ def with_read
32
+ begin
33
+ read
34
+ rescue TTY::Config::ReadError
35
+ raise MissingUserConfig.new("User config not found, run `ninny setup`")
36
+ end
37
+ @read = true
38
+ yield
39
+ end
40
+
41
+ def self.config
42
+ new
43
+ end
44
+ end
45
+
46
+ MissingUserConfig = Class.new(StandardError)
47
+ end
@@ -0,0 +1,3 @@
1
+ module Ninny
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ninny.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+ require 'ninny/git'
3
+ require 'ninny/command'
4
+ require 'ninny/commands/create_dated_branch'
5
+ require 'ninny/commands/output_dated_branch'
6
+ require 'ninny/commands/new_staging'
7
+ require 'ninny/commands/staging_branch'
8
+ require 'ninny/commands/pull_request_merge'
9
+ require 'ninny/commands/setup'
10
+ require 'ninny/commands/stage_up'
11
+ require 'ninny/repository/gitlab'
12
+ require 'ninny/repository/pull_request'
13
+ require 'ninny/project_config'
14
+ require 'ninny/user_config'
15
+
16
+
17
+ require 'git'
18
+ require 'gitlab'
19
+ require 'tty-config'
20
+
21
+ module Ninny
22
+ class Error < StandardError; end
23
+ def self.project_config
24
+ @config ||= ProjectConfig.config
25
+ end
26
+
27
+ def self.user_config
28
+ @user_config ||= UserConfig.config
29
+ end
30
+
31
+ def self.repo
32
+ @repo ||= project_config.repo
33
+ end
34
+
35
+ def self.git
36
+ @git ||= Git.new
37
+ end
38
+ end
data/ninny.gemspec ADDED
@@ -0,0 +1,70 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "ninny/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ninny"
8
+ spec.license = "MIT"
9
+ spec.version = Ninny::VERSION
10
+ spec.authors = ["Carl Allen"]
11
+ spec.email = ["carl.allen@dispatchwithus.com"]
12
+
13
+ spec.summary = "ninny (n): an foolish person, see: git"
14
+ spec.description = "Ninny is a command line workflow for git"
15
+ # spec.homepage = "TODO: Put your gem's website or public repo URL here."
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
21
+
22
+ # spec.metadata["homepage_uri"] = spec.homepage
23
+ # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
24
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
25
+ # else
26
+ # raise "RubyGems 2.0 or newer is required to protect against " \
27
+ # "public gem pushes."
28
+ # end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_dependency "tty-box", "~> 0.3.0"
40
+ spec.add_dependency "tty-color", "~> 0.4"
41
+ spec.add_dependency "tty-command", "~> 0.8.0"
42
+ spec.add_dependency "tty-config", "~> 0.3.0"
43
+ spec.add_dependency "tty-prompt", "~> 0.18.0"
44
+
45
+ # spec.add_dependency "tty-cursor", "~> 0.6"
46
+ # spec.add_dependency "tty-editor", "~> 0.5.0"
47
+ # spec.add_dependency "tty-file", "~> 0.7.0"
48
+ # spec.add_dependency "tty-font", "~> 0.2.0"
49
+ # spec.add_dependency "tty-markdown", "~> 0.5.0"
50
+ # spec.add_dependency "tty-pager", "~> 0.12.0"
51
+ # spec.add_dependency "tty-pie", "~> 0.1.0"
52
+ # spec.add_dependency "tty-platform", "~> 0.2.0"
53
+ # spec.add_dependency "tty-progressbar", "~> 0.16.0"
54
+ # spec.add_dependency "tty-screen", "~> 0.6"
55
+ # spec.add_dependency "tty-spinner", "~> 0.9.0"
56
+ # spec.add_dependency "tty-table", "~> 0.10.0"
57
+ # spec.add_dependency "tty-tree", "~> 0.2.0"
58
+ # spec.add_dependency "tty-which", "~> 0.4"
59
+
60
+ spec.add_dependency "pastel", "~> 0.7.2"
61
+ spec.add_dependency "thor", "~> 0.20.0"
62
+
63
+ spec.add_dependency "git", "~> 1.5.0"
64
+ spec.add_dependency "gitlab", "~> 4.11.0"
65
+
66
+ spec.add_development_dependency "bundler", "~> 1.17"
67
+ spec.add_development_dependency "rake", "~> 10.0"
68
+ spec.add_development_dependency "rspec", "~> 3.0"
69
+ spec.add_development_dependency "byebug"
70
+ end