ballantine 0.1.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a2188ad3272546c25226bdbb1209075baadee05fb7f011cafe13874b1af45777
4
+ data.tar.gz: 0fe5889c616c93c7f62ce8353671543750604fc4c6def9958abdfe3a0f6e5c42
5
+ SHA512:
6
+ metadata.gz: ab52789a4ae2fb510d48f34cd7cf8c2920610e5f341fdbbd94f687fee7ace7ff1f1ff7cbce32fe2e4ce78a783762aa29cf1c9053be5f042ef5b12f84f9101b28
7
+ data.tar.gz: bda07819f4146ac05b3ab1c39f4debe647f9c72a3a1dc50cfac433bf3381ecc6b96e96c92559775c08ecbf4f4e14b39a235a5a23d8636d2e956b6eddee33ad60
data/bin/ballantine ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'ballantine'
5
+ Ballantine::CLI.start
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ class Author
4
+ attr_accessor :name, :commits
5
+
6
+ class << self
7
+ # @param [String] name
8
+ # @return [Author] author
9
+ def find_or_create_by(name)
10
+ @@_collections = {} unless defined?(@@_collections)
11
+ return @@_collections[name] unless @@_collections[name].nil?
12
+ @@_collections[name] = new(name)
13
+ end
14
+
15
+ # @return [Array<Author>] authors
16
+ def all
17
+ return [] unless defined?(@@_collections)
18
+ @@_collections.sort.map(&:last) # sort and take values
19
+ end
20
+ end
21
+
22
+ # @param [String] name
23
+ def initialize(name)
24
+ @name = name
25
+ @commits = {}
26
+ end
27
+
28
+ # @return [NilClass] nil
29
+ def print_commits
30
+ puts "\n" + "@#{name}".green
31
+ @commits.each do |repo, lists|
32
+ count, word = retrieve_count_and_word(lists)
33
+ puts " > #{repo.blue}: #{count} new #{word}\n"
34
+ puts lists
35
+ end
36
+ nil
37
+ end
38
+
39
+ # returns an array to use slack attachments field
40
+ # reference: https://api.slack.com/messaging/composing/layouts#building-attachments
41
+ # @return [Hash] result
42
+ def serialize_commits
43
+ message = @commits.map do |repo, lists|
44
+ count, word = retrieve_count_and_word(lists)
45
+ "*#{repo}*: #{count} new #{word}\n#{lists.join("\n")}"
46
+ end.join("\n")
47
+
48
+ results = {
49
+ 'text' => "- <@#{name}>\n#{message}",
50
+ 'color' => '#00B86A', # green
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ # @param [Array<String>] lists
57
+ # @param [Array(Integer, String)] count, word
58
+ def retrieve_count_and_word(lists)
59
+ count = lists.size
60
+ word = count == 1 ? 'commit' : 'commits'
61
+ [count, word]
62
+ end
63
+ end
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Ballantine
4
+ require 'thor'
5
+ require 'json'
6
+ require_relative './author'
7
+ require_relative './string'
8
+
9
+ class CLI < Thor
10
+ # reference: https://github.com/desktop/desktop/blob/a7bca44088b105a04714dc4628f4af50f6f179c3/app/src/lib/remote-parsing.ts#L27-L44
11
+ GITHUB_REGEXES = [
12
+ '^https?://(.+)/(.+)/(.+)\.git/?$', # protocol: https -> https://github.com/oohyun15/ballantine.git | https://github.com/oohyun15/ballantine.git/
13
+ '^https?://(.+)/(.+)/(.+)/?$', # protocol: https -> https://github.com/oohyun15/ballantine | https://github.com/oohyun15/ballantine/
14
+ '^git@(.+):(.+)/(.+)\.git$', # protocol: ssh -> git@github.com:oohyun15/ballantine.git
15
+ '^git@(.+):(.+)/(.+)/?$', # protocol: ssh -> git@github.com:oohyun15/ballantine | git@github.com:oohyun15/ballantine/
16
+ '^git:(.+)/(.+)/(.+)\.git$', # protocol: ssh -> git:github.com/oohyun15/ballantine.git
17
+ '^git:(.+)/(.+)/(.+)/?$', # protocol: ssh -> git:github.com/oohyun15/ballantine | git:github.com/oohyun15/ballantine/
18
+ '^ssh://git@(.+)/(.+)/(.+)\.git$', # protocol: ssh -> ssh://git@github.com/oohyun15/ballantine.git
19
+ ].freeze
20
+
21
+ FILE_GITMODULES = '.gitmodules'
22
+ FILE_BALLANTINE_CONFIG = '.ballantine.json'
23
+
24
+ TYPE_TERMINAL = 'terminal'
25
+ TYPE_SLACK = 'slack'
26
+
27
+ DEFAULT_LJUST = 80
28
+
29
+ AVAILABLE_CONFIG = ['slack_webhook'].freeze
30
+
31
+ attr_reader :app_name, :main_path, :sub_path, :slack_webhook, :send_type
32
+
33
+ package_name 'Ballantine'
34
+
35
+ desc 'init', 'Initialize ballantine configuration'
36
+ def init
37
+ puts "🥃 Init ballantine configuration"
38
+ slack_webhook = ask("Q. Set slack webhook\n> ")
39
+
40
+ config = {
41
+ slack_webhook: slack_webhook
42
+ }
43
+ File.write('./' + FILE_BALLANTINE_CONFIG, JSON.dump(config))
44
+ end
45
+
46
+ desc 'config', 'Describe ballantine configuration'
47
+ def config
48
+ load_config
49
+ AVAILABLE_CONFIG.each do |key|
50
+ puts "#{key}: #{instance_variable_get('@' + key)}"
51
+ end
52
+ end
53
+
54
+ desc 'diff [TARGET] [SOURCE]', 'Diff commits between TARGET and SOURCE'
55
+ option TYPE_SLACK, type: :boolean, aliases: '-s', default: false, desc: 'send to slack using slack webhook URL.'
56
+ def diff(from, to = `git rev-parse --abbrev-ref HEAD`.chomp)
57
+ load_config
58
+ preprocess(from, to, **options)
59
+
60
+ # check argument is tag
61
+ from = check_tag(from)
62
+ to = check_tag(to)
63
+
64
+ # check commits are newest
65
+ system 'git pull -f &> /dev/null'
66
+
67
+ # set instance variables
68
+ @send_type = options[TYPE_SLACK] ? TYPE_SLACK : TYPE_TERMINAL
69
+ @app_name = File.basename(`git config --get remote.origin.url`.chomp, '.git')
70
+ @main_path = Dir.pwd
71
+ @sub_path = if Dir[FILE_GITMODULES].any?
72
+ file = File.open(FILE_GITMODULES)
73
+ lines = file.readlines.map(&:chomp)
74
+ file.close
75
+ lines.grep(/path =/).map{ |line| line[/(?<=path \=).*/, 0].strip }.sort
76
+ else
77
+ []
78
+ end
79
+
80
+ # find github url, branch
81
+ main_url = github_url(`git config --get remote.origin.url`.chomp)
82
+ current_branch = `git rev-parse --abbrev-ref HEAD`.chomp
83
+
84
+ # get commit hash
85
+ main_from, sub_from = commit_hash(from)
86
+ main_to, sub_to = commit_hash(to)
87
+ system "git checkout #{current_branch} -f &> /dev/null"
88
+
89
+ # check commits
90
+ check_commits(main_from, main_to, main_url)
91
+ @sub_path.each_with_index do |path, idx|
92
+ next if sub_from[idx] == sub_to[idx]
93
+ Dir.chdir(path)
94
+ sub_url = github_url(`git config --get remote.origin.url`.chomp)
95
+ check_commits(sub_from[idx], sub_to[idx], sub_url)
96
+ Dir.chdir(@main_path)
97
+ end
98
+
99
+ send_commits(from, to, main_url)
100
+
101
+ exit 0
102
+ end
103
+
104
+ desc 'version', 'Display version information about ballntine'
105
+ def version
106
+ puts "ballantine version #{Ballantine::VERSION}"
107
+ end
108
+
109
+ private
110
+
111
+ def self.exit_on_failure?
112
+ exit 1
113
+ end
114
+
115
+ def load_config
116
+ return if Dir[FILE_BALLANTINE_CONFIG].empty?
117
+
118
+ JSON.parse(File.read('./' + FILE_BALLANTINE_CONFIG)).each do |key, value|
119
+ next unless AVAILABLE_CONFIG.include?(key)
120
+ instance_variable_set('@' + key, value)
121
+ end
122
+ end
123
+
124
+ # @param [String] from
125
+ # @param [String] to
126
+ # @param [Hash] options
127
+ # @return [NilClass] nil
128
+ def preprocess(from, to, **options)
129
+ if Dir['.git'].empty?
130
+ raise SystemCallError, "ERROR: There is no \".git\" in #{Dir.pwd}."
131
+ end
132
+
133
+ if (uncommitted = `git diff HEAD --name-only`.split("\n")).any?
134
+ raise SystemCallError, "ERROR: Uncommitted file exists. stash or commit uncommitted files.\n#{uncommitted.join("\n")}"
135
+ end
136
+
137
+ if from == to
138
+ raise ArgumentError, "ERROR: target(#{from}) and source(#{to}) can't be equal."
139
+ end
140
+
141
+ if options[TYPE_SLACK] && !@slack_webhook
142
+ raise ArgumentError, "ERROR: Can't find any slack webhook. Set slack webhook using `ballantine init`."
143
+ end
144
+
145
+ nil
146
+ end
147
+
148
+ # @param [String] name
149
+ # @return [String] hash
150
+ def check_tag(name)
151
+ list = `git tag -l`.split("\n")
152
+ return name unless list.grep(name).any?
153
+
154
+ system "git fetch origin tag #{name} -f &> /dev/null"
155
+ `git rev-list -n 1 #{name}`.chomp[0...7]
156
+ end
157
+
158
+ # @param [String] from
159
+ # @param [String] to
160
+ # @param [String] url
161
+ # @return [NilClass] nil
162
+ def check_commits(from, to, url)
163
+ repo = File.basename(`git config --get remote.origin.url`.chomp, '.git')
164
+ names = `git --no-pager log --pretty=format:"%an" #{from}..#{to}`.split("\n").uniq.sort
165
+ authors = names.map{ |name| Author.find_or_create_by(name) }
166
+ authors.each do |author|
167
+ format = commit_format(url, ljust: DEFAULT_LJUST - 10)
168
+ commits = `git --no-pager log --reverse --no-merges --author="#{author.name}" --format="#{format}" --abbrev=7 #{from}..#{to}`.gsub('"', '\"').gsub(/[\u0080-\u00ff]/, '')
169
+ next if commits.empty?
170
+ author.commits[repo] = commits.split("\n")
171
+ end
172
+ nil
173
+ end
174
+
175
+ # @param [String] url
176
+ # @return [String] github_url
177
+ def github_url(url)
178
+ owner, repository = GITHUB_REGEXES.each do |regex|
179
+ if (str = url.match(regex))
180
+ break [str[2], str[3]]
181
+ end
182
+ end
183
+
184
+ "https://github.com/#{owner}/#{repository}"
185
+ end
186
+
187
+ # @param [String] hash
188
+ # @param [Array<String>] sub_path
189
+ # @return [Array(String, Array<String>)] main, sub's hash
190
+ def commit_hash(hash)
191
+ system "git checkout #{hash} -f &> /dev/null"
192
+ system 'git pull &> /dev/null'
193
+ main_hash = `git --no-pager log -1 --format='%h'`.chomp
194
+ sub_hash = if @sub_path.any?
195
+ `git ls-tree HEAD #{@sub_path.join(' ')}`.split("\n").map{ |line| line.split(' ')[2] }
196
+ else
197
+ []
198
+ end
199
+
200
+ [main_hash, sub_hash]
201
+ end
202
+
203
+ # @param [String] url
204
+ # @param [String] format
205
+ # @param [Integer] ljust
206
+ def commit_format(url, ljust: DEFAULT_LJUST)
207
+ case @send_type
208
+ when TYPE_TERMINAL
209
+ " - "+ "%h".yellow + " %<(#{ljust})%s " + "#{url}/commit/%H".gray
210
+ when TYPE_SLACK
211
+ "\\\`<#{url}/commit/%H|%h>\\\` %s - %an"
212
+ end
213
+ end
214
+
215
+ # @param [String] from
216
+ # @param [String] to
217
+ # @param [String] url
218
+ # @return [NilClass] nil
219
+ def send_commits(from, to, url)
220
+ authors = Author.all
221
+ if authors.empty?
222
+ raise ArgumentError, "ERROR: There is no commits between \"#{from}\" and \"#{to}\""
223
+ end
224
+ number = authors.size
225
+ last_commit = `git --no-pager log --reverse --format="#{commit_format(url, ljust: DEFAULT_LJUST - 22)}" --abbrev=7 #{from}..#{to} -1`.strip
226
+
227
+ case @send_type
228
+ when TYPE_TERMINAL
229
+ puts "Check commits before #{@app_name.red} deployment. (#{from.cyan} <- #{to.cyan})".ljust(DEFAULT_LJUST + 34) + " #{url}/compare/#{from}...#{to}".gray
230
+ puts "Author".yellow + ": #{number}"
231
+ puts "Last commit".blue + ": #{last_commit}"
232
+ authors.map(&:print_commits)
233
+ when TYPE_SLACK
234
+ # set message for each author
235
+ messages = authors.map(&:serialize_commits)
236
+ actor = `git config user.name`
237
+
238
+ # send message to slack
239
+ require 'net/http'
240
+ require 'uri'
241
+ uri = URI.parse(@slack_webhook)
242
+ request = Net::HTTP::Post.new(uri)
243
+ request.content_type = 'application/json'
244
+ request.body = JSON.dump({
245
+ 'text' => ":check: *#{@app_name}* deployment request by <@#{actor}> (\`<#{url}/tree/#{from}|#{from}>\` <- \`<#{url}/tree/#{to}|#{to}>\` <#{url}/compare/#{from}...#{to}|compare>)\n:technologist: Author: #{number}\nLast commit: #{last_commit}",
246
+ 'attachments' => messages
247
+ })
248
+ req_options = { use_ssl: uri.scheme == 'https' }
249
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
250
+ http.request(request)
251
+ end
252
+ puts response.message
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # @override
4
+ class String
5
+ NC = "\e[0m"
6
+ GRAY = "\e[1;30m"
7
+ RED = "\e[1;31m"
8
+ GREEN = "\e[1;32m"
9
+ YELLOW = "\e[1;33m"
10
+ BLUE = "\e[1;34m"
11
+ CYAN = "\e[1;36m"
12
+
13
+ def gray; "#{GRAY}#{self}#{NC}" end
14
+ def red; "#{RED}#{self}#{NC}" end
15
+ def green; "#{GREEN}#{self}#{NC}" end
16
+ def yellow; "#{YELLOW}#{self}#{NC}" end
17
+ def blue; "#{BLUE}#{self}#{NC}" end
18
+ def cyan; "#{CYAN}#{self}#{NC}" end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ballantine
4
+ VERSION = "0.1.2"
5
+ end
data/lib/ballantine.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require_relative "ballantine/cli"
3
+ require_relative "ballantine/version"
4
+
5
+ module Ballantine
6
+ class Error < StandardError; end
7
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ballantine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - oohyun15
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-08-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.28
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.28
41
+ description: Ballantine helps you describe your commits easier and prettier from cli
42
+ & slack.
43
+ email:
44
+ - sakiss4774@gmail.com
45
+ executables:
46
+ - ballantine
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - bin/ballantine
51
+ - lib/ballantine.rb
52
+ - lib/ballantine/author.rb
53
+ - lib/ballantine/cli.rb
54
+ - lib/ballantine/string.rb
55
+ - lib/ballantine/version.rb
56
+ homepage: https://github.com/oohyun15/ballantine
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.7.6
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.3.7
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Describe your commits.
79
+ test_files: []