skunk 0.4.2 → 0.5.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.
@@ -7,24 +7,56 @@ require "skunk"
7
7
  require "skunk/rubycritic/analysed_module"
8
8
  require "skunk/cli/options"
9
9
  require "skunk/cli/command_factory"
10
+ require "skunk/cli/commands/status_sharer"
10
11
 
11
12
  module Skunk
12
13
  module Cli
13
14
  # Knows how to execute command line commands
15
+ # :reek:InstanceVariableAssumption
14
16
  class Application < RubyCritic::Cli::Application
17
+ COVERAGE_FILE = "coverage/.resultset.json"
18
+
15
19
  def initialize(argv)
16
20
  @options = Skunk::Cli::Options.new(argv)
17
21
  end
18
22
 
23
+ # :reek:UncommunicativeVariableName
19
24
  def execute
20
- parsed_options = @options.parse.to_h
21
- reporter = Skunk::Cli::CommandFactory.create(parsed_options).execute
25
+ warn_coverage_info unless File.exist?(COVERAGE_FILE)
26
+
27
+ # :reek:NilCheck
28
+ @parsed_options = @options.parse.to_h
29
+ command = Skunk::Cli::CommandFactory.create(@parsed_options)
30
+ reporter = command.execute
31
+
22
32
  print(reporter.status_message)
33
+ if command.sharing?
34
+ share_status_message = command.share(reporter)
35
+ print(share_status_message)
36
+ end
37
+
23
38
  reporter.status
24
- rescue OptionParser::InvalidOption => error
25
- warn "Error: #{error}"
39
+ rescue OptionParser::InvalidOption => e
40
+ warn "Error: #{e}"
26
41
  STATUS_ERROR
27
42
  end
43
+
44
+ private
45
+
46
+ def warn_coverage_info
47
+ warn "warning: Couldn't find coverage info at #{COVERAGE_FILE}."
48
+ warn "warning: Having no coverage metrics will make your SkunkScore worse."
49
+ end
50
+
51
+ # :reek:NilCheck
52
+ def print(message)
53
+ filename = @parsed_options[:output_filename]
54
+ if filename.nil?
55
+ $stdout.puts(message)
56
+ else
57
+ File.open(filename, "a") { |file| file << message }
58
+ end
59
+ end
28
60
  end
29
61
  end
30
62
  end
@@ -14,6 +14,8 @@ module Skunk
14
14
  @options = options
15
15
  @status_reporter = Skunk::Command::StatusReporter.new(@options)
16
16
  end
17
+
18
+ def share(_); end
17
19
  end
18
20
  end
19
21
  end
@@ -3,43 +3,63 @@
3
3
  require "rubycritic/commands/compare"
4
4
  require "skunk/rubycritic/analysed_modules_collection"
5
5
  require "skunk/cli/commands/output"
6
+ require "skunk/cli/commands/shareable"
7
+ require "skunk/cli/commands/compare_score"
6
8
 
7
9
  # nodoc #
8
10
  module Skunk
9
- module Command
10
- # Knows how to compare two branches and their stink score average
11
- class Compare < RubyCritic::Command::Compare
12
- # switch branch and analyse files but don't generate a report
13
- def analyse_branch(branch)
14
- ::RubyCritic::SourceControlSystem::Git.switch_branch(::RubyCritic::Config.send(branch))
15
- critic = critique(branch)
16
- ::RubyCritic::Config.send(:"#{branch}_score=", critic.stink_score_average)
17
- ::RubyCritic::Config.root = branch_directory(branch)
18
- end
11
+ module Cli
12
+ module Command
13
+ # Knows how to compare two branches and their skunk score average
14
+ class Compare < RubyCritic::Command::Compare
15
+ include Skunk::Cli::Command::Shareable
19
16
 
20
- # generate report only for modified files but don't report it
21
- def analyse_modified_files
22
- modified_files = ::RubyCritic::Config
23
- .feature_branch_collection
24
- .where(::RubyCritic::SourceControlSystem::Git.modified_files)
25
- ::RubyCritic::AnalysedModulesCollection.new(modified_files.map(&:path),
26
- modified_files)
27
- ::RubyCritic::Config.root = "#{::RubyCritic::Config.root}/compare"
28
- end
17
+ def initialize(options)
18
+ super
19
+ @options = options
20
+ @status_reporter = Skunk::Command::StatusReporter.new(options)
21
+ end
29
22
 
30
- # create a txt file with the branch score details
31
- def build_details
32
- details = "Base branch (#{::RubyCritic::Config.base_branch}) "\
33
- "average stink score: #{::RubyCritic::Config.base_branch_score} \n"\
34
- "Feature branch (#{::RubyCritic::Config.feature_branch}) "\
35
- "average stink score: #{::RubyCritic::Config.feature_branch_score} \n"
36
- Skunk::Command::Output.create_directory(::RubyCritic::Config.compare_root_directory)
37
- File.open(build_details_path, "w") { |file| file.write(details) }
38
- puts details
39
- end
23
+ def execute
24
+ compare_branches
25
+ status_reporter
26
+ end
27
+
28
+ # switch branch and analyse files but don't generate a report
29
+ def analyse_branch(branch)
30
+ ::RubyCritic::SourceControlSystem::Git.switch_branch(::RubyCritic::Config.send(branch))
31
+ critic = critique(branch)
32
+ ::RubyCritic::Config.send(:"#{branch}_score=", critic.skunk_score_average)
33
+ ::RubyCritic::Config.root = branch_directory(branch)
34
+ end
35
+
36
+ # generate report only for modified files but don't report it
37
+ def analyse_modified_files
38
+ modified_files = ::RubyCritic::Config
39
+ .feature_branch_collection
40
+ .where(::RubyCritic::SourceControlSystem::Git.modified_files)
41
+ ::RubyCritic::AnalysedModulesCollection.new(modified_files.map(&:path),
42
+ modified_files)
43
+ ::RubyCritic::Config.root = "#{::RubyCritic::Config.root}/compare"
44
+ end
45
+
46
+ # create a txt file with the branch score details
47
+ def build_details
48
+ details = CompareScore.new(
49
+ ::RubyCritic::Config.base_branch,
50
+ ::RubyCritic::Config.feature_branch,
51
+ ::RubyCritic::Config.base_branch_score.to_f.round(2),
52
+ ::RubyCritic::Config.feature_branch_score.to_f.round(2)
53
+ ).message
54
+
55
+ Skunk::Command::Output.create_directory(::RubyCritic::Config.compare_root_directory)
56
+ File.open(build_details_path, "w") { |file| file.write(details) }
57
+ puts details
58
+ end
40
59
 
41
- def build_details_path
42
- "#{::RubyCritic::Config.compare_root_directory}/build_details.txt"
60
+ def build_details_path
61
+ "#{::RubyCritic::Config.compare_root_directory}/build_details.txt"
62
+ end
43
63
  end
44
64
  end
45
65
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc #
4
+ module Skunk
5
+ module Cli
6
+ module Command
7
+ # Knows how to describe score evolution between two branches
8
+ class CompareScore
9
+ def initialize(base_branch, feature_branch, base_branch_score, feature_branch_score)
10
+ @base_branch = base_branch
11
+ @feature_branch = feature_branch
12
+ @base_branch_score = base_branch_score
13
+ @feature_branch_score = feature_branch_score
14
+ end
15
+
16
+ def message
17
+ "Base branch (#{@base_branch}) "\
18
+ "average skunk score: #{@base_branch_score} \n"\
19
+ "Feature branch (#{@feature_branch}) "\
20
+ "average skunk score: #{@feature_branch_score} \n"\
21
+ "#{score_evolution_message}"
22
+ end
23
+
24
+ def score_evolution_message
25
+ "Skunk score average is #{score_evolution} #{score_evolution_appreciation} \n"
26
+ end
27
+
28
+ def score_evolution_appreciation
29
+ @feature_branch_score > @base_branch_score ? "worse" : "better"
30
+ end
31
+
32
+ def score_evolution
33
+ return "Infinitely" if @base_branch_score.zero?
34
+
35
+ precentage = (100 * (@base_branch_score - @feature_branch_score) / @base_branch_score)
36
+ "#{precentage.round(0).abs}%"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -6,6 +6,7 @@ require "rubycritic/revision_comparator"
6
6
  require "rubycritic/reporter"
7
7
 
8
8
  require "skunk/cli/commands/base"
9
+ require "skunk/cli/commands/shareable"
9
10
  require "skunk/cli/commands/status_reporter"
10
11
 
11
12
  module Skunk
@@ -14,18 +15,30 @@ module Skunk
14
15
  # Default command runs a critique using RubyCritic and uses
15
16
  # Skunk::Command::StatusReporter to report status
16
17
  class Default < RubyCritic::Command::Default
18
+ include Skunk::Cli::Command::Shareable
19
+
17
20
  def initialize(options)
18
21
  super
19
- @status_reporter = Skunk::Command::StatusReporter.new(@options)
22
+ @options = options
23
+ @status_reporter = Skunk::Command::StatusReporter.new(options)
20
24
  end
21
25
 
26
+ # It generates a report and it returns an instance of
27
+ # Skunk::Command::StatusReporter
28
+ #
29
+ # @return [Skunk::Command::StatusReporter]
22
30
  def execute
23
31
  RubyCritic::Config.formats = []
24
32
 
25
33
  report(critique)
34
+
26
35
  status_reporter
27
36
  end
28
37
 
38
+ # It connects the Skunk::Command::StatusReporter with the collection
39
+ # of analysed modules.
40
+ #
41
+ # @param [RubyCritic::AnalysedModulesCollection] A collection of analysed modules
29
42
  def report(analysed_modules)
30
43
  status_reporter.analysed_modules = analysed_modules
31
44
  status_reporter.score = analysed_modules.score
@@ -6,7 +6,21 @@ require "rubycritic/commands/help"
6
6
  module Skunk
7
7
  module Cli
8
8
  module Command
9
- class Help < RubyCritic::Command::Help
9
+ # Knows how to guide user into using `skunk` properly
10
+ class Help < Skunk::Cli::Command::Base
11
+ # Outputs a help message
12
+ def execute
13
+ puts options[:help_text]
14
+ status_reporter
15
+ end
16
+
17
+ def sharing?
18
+ false
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :options, :status_reporter
10
24
  end
11
25
  end
12
26
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skunk
4
+ module Cli
5
+ module Command
6
+ # This is a module that will be used for sharing reports to a server
7
+ module Shareable
8
+ # It shares the report using SHARE_URL or https://skunk.fastruby.io. It
9
+ # will post all results in JSON format and return a status message.
10
+ #
11
+ # @param [Skunk::Command::StatusReporter] A status reporter with analysed modules
12
+ # :reek:FeatureEnvy
13
+ def share(reporter)
14
+ sharer = Skunk::Command::StatusSharer.new(@options)
15
+ sharer.status_reporter = reporter
16
+ sharer.share
17
+ end
18
+
19
+ # @return [Boolean] If the environment is set to share to an external
20
+ # service
21
+ def sharing?
22
+ ENV["SHARE"] == "true"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -10,21 +10,23 @@ module Skunk
10
10
  class StatusReporter < RubyCritic::Command::StatusReporter
11
11
  attr_accessor :analysed_modules
12
12
 
13
- HEADINGS = %w[file stink_score churn_times_cost churn cost coverage].freeze
13
+ HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze
14
14
  HEADINGS_WITHOUT_FILE = HEADINGS - %w[file]
15
15
  HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding
16
16
 
17
17
  TEMPLATE = ERB.new(<<-TEMPL
18
18
  <%= _ttable %>\n
19
- StinkScore Total: <%= total_stink_score %>
19
+ SkunkScore Total: <%= total_skunk_score %>
20
20
  Modules Analysed: <%= analysed_modules_count %>
21
- StinkScore Average: <%= stink_score_average %>
22
- <% if worst %>Worst StinkScore: <%= worst.stink_score %> (<%= worst.pathname %>)<% end %>
21
+ SkunkScore Average: <%= skunk_score_average %>
22
+ <% if worst %>Worst SkunkScore: <%= worst.skunk_score %> (<%= worst.pathname %>)<% end %>
23
+
24
+ Generated with Skunk v<%= Skunk::VERSION %>
23
25
  TEMPL
24
26
  )
25
27
 
26
28
  # Returns a status message with a table of all analysed_modules and
27
- # a stink score average
29
+ # a skunk score average
28
30
  def update_status_message
29
31
  opts = table_options.merge(headings: HEADINGS, rows: table)
30
32
 
@@ -41,7 +43,8 @@ TEMPL
41
43
 
42
44
  def non_test_modules
43
45
  @non_test_modules ||= analysed_modules.reject do |a_module|
44
- a_module.pathname.to_s.start_with?("test", "spec")
46
+ module_path = a_module.pathname.dirname.to_s
47
+ module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
45
48
  end
46
49
  end
47
50
 
@@ -50,21 +53,21 @@ TEMPL
50
53
  end
51
54
 
52
55
  def sorted_modules
53
- @sorted_modules ||= non_test_modules.sort_by(&:stink_score).reverse!
56
+ @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
54
57
  end
55
58
 
56
- def total_stink_score
57
- @total_stink_score ||= non_test_modules.map(&:stink_score).inject(0.0, :+)
59
+ def total_skunk_score
60
+ @total_skunk_score ||= non_test_modules.sum(&:skunk_score)
58
61
  end
59
62
 
60
63
  def total_churn_times_cost
61
- non_test_modules.map(&:churn_times_cost).sum
64
+ non_test_modules.sum(&:churn_times_cost)
62
65
  end
63
66
 
64
- def stink_score_average
67
+ def skunk_score_average
65
68
  return 0 if analysed_modules_count.zero?
66
69
 
67
- (total_stink_score.to_d / analysed_modules_count).to_f
70
+ (total_skunk_score.to_d / analysed_modules_count).to_f.round(2)
68
71
  end
69
72
 
70
73
  def table_options
@@ -72,7 +75,7 @@ TEMPL
72
75
  width = max.pathname.to_s.length + HEADINGS_WITHOUT_FILE_WIDTH
73
76
  {
74
77
  style: {
75
- width: width
78
+ width: width # rubocop:disable Style/HashSyntax
76
79
  }
77
80
  }
78
81
  end
@@ -81,7 +84,7 @@ TEMPL
81
84
  sorted_modules.map do |a_mod|
82
85
  [
83
86
  a_mod.pathname,
84
- a_mod.stink_score,
87
+ a_mod.skunk_score,
85
88
  a_mod.churn_times_cost,
86
89
  a_mod.churn,
87
90
  a_mod.cost.round(2),
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "net/https"
5
+ require "json"
6
+
7
+ require "skunk/cli/commands/status_reporter"
8
+
9
+ module Skunk
10
+ module Command
11
+ # Knows how to share status to an API
12
+ class StatusSharer < Skunk::Command::StatusReporter
13
+ attr_reader :status_message
14
+
15
+ DEFAULT_URL = "https://skunk.fastruby.io"
16
+ def status_reporter=(status_reporter)
17
+ self.analysed_modules = status_reporter.analysed_modules
18
+ end
19
+
20
+ def share
21
+ return "" if not_sharing?
22
+
23
+ response = post_payload
24
+
25
+ @status_message =
26
+ if Net::HTTPOK === response
27
+ data = JSON.parse response.body
28
+ "Shared at: #{File.join(base_url, data['id'])}"
29
+ else
30
+ "Error sharing report: #{response}"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # :reek:UtilityFunction
37
+ def base_url
38
+ ENV["SHARE_URL"] || DEFAULT_URL
39
+ end
40
+
41
+ # rubocop:disable Style/HashSyntax
42
+ def json_summary
43
+ result = {
44
+ total_skunk_score: total_skunk_score,
45
+ analysed_modules_count: analysed_modules_count,
46
+ skunk_score_average: skunk_score_average,
47
+ skunk_version: Skunk::VERSION
48
+ }
49
+
50
+ if worst
51
+ result[:worst_skunk_score] = {
52
+ file: worst.pathname.to_s,
53
+ skunk_score: worst.skunk_score
54
+ }
55
+ end
56
+
57
+ result
58
+ end
59
+ # rubocop:enable Style/HashSyntax
60
+
61
+ def json_results
62
+ sorted_modules.map(&:to_hash)
63
+ end
64
+
65
+ # :reek:UtilityFunction
66
+ def not_sharing?
67
+ ENV["SHARE"] != "true" && ENV["SHARE_URL"].to_s == ""
68
+ end
69
+
70
+ def payload
71
+ JSON.generate(
72
+ "entries" => json_results,
73
+ "summary" => json_summary,
74
+ "options" => {
75
+ "compare" => "false"
76
+ }
77
+ )
78
+ end
79
+
80
+ # :reek:TooManyStatements
81
+ def post_payload
82
+ req = Net::HTTP::Post.new(url)
83
+ req.body = payload
84
+
85
+ http = Net::HTTP.new(url.hostname, url.port)
86
+ if url.scheme == "https"
87
+ http.use_ssl = true
88
+ http.ssl_version = :TLSv1_2
89
+ end
90
+
91
+ http.start do |connection|
92
+ connection.request req
93
+ end
94
+ end
95
+
96
+ def url
97
+ URI(File.join(base_url, "reports"))
98
+ end
99
+ end
100
+ end
101
+ end
@@ -4,12 +4,18 @@ require "rubycritic/commands/version"
4
4
 
5
5
  # nodoc #
6
6
  module Skunk
7
- module Command
8
- # Shows skunk version
9
- class Version < RubyCritic::Command::Version
10
- def execute
11
- print Skunk::VERSION
12
- status_reporter
7
+ module Cli
8
+ module Command
9
+ # Shows skunk version
10
+ class Version < RubyCritic::Command::Version
11
+ def execute
12
+ print Skunk::VERSION
13
+ status_reporter
14
+ end
15
+
16
+ def sharing?
17
+ false
18
+ end
13
19
  end
14
20
  end
15
21
  end
@@ -9,6 +9,9 @@ module Skunk
9
9
  # Extends RubyCritic::Cli::Options::Argv to parse a subset of the
10
10
  # parameters accepted by RubyCritic
11
11
  class Argv < RubyCritic::Cli::Options::Argv
12
+ # :reek:Attribute
13
+ attr_accessor :output_filename
14
+
12
15
  def parse # rubocop:disable Metrics/MethodLength
13
16
  parser.new do |opts|
14
17
  opts.banner = "Usage: skunk [options] [paths]\n"
@@ -19,6 +22,10 @@ module Skunk
19
22
  self.mode = :compare_branches
20
23
  end
21
24
 
25
+ opts.on("-o", "--out FILE", "Output report to file") do |filename|
26
+ self.output_filename = filename
27
+ end
28
+
22
29
  opts.on_tail("-v", "--version", "Show gem's version") do
23
30
  self.mode = :version
24
31
  end
@@ -28,6 +35,10 @@ module Skunk
28
35
  end
29
36
  end.parse!(@argv)
30
37
  end
38
+
39
+ def to_h
40
+ super.merge(output_filename: output_filename) # rubocop:disable Style/HashSyntax
41
+ end
31
42
  end
32
43
  end
33
44
  end
@@ -3,34 +3,34 @@
3
3
  require "rubycritic/core/analysed_module"
4
4
 
5
5
  module RubyCritic
6
- # Monkey-patches RubyCritic::AnalysedModule to add a stink_score method
6
+ # Monkey-patches RubyCritic::AnalysedModule to add a skunk_score method
7
7
  class AnalysedModule
8
8
  PERFECT_COVERAGE = 100
9
9
 
10
- # Returns a numeric value that represents the stink_score of a module:
10
+ # Returns a numeric value that represents the skunk_score of a module:
11
11
  #
12
- # If module is perfectly covered, stink score is the same as the
12
+ # If module is perfectly covered, skunk score is the same as the
13
13
  # `churn_times_cost`
14
14
  #
15
- # If module has no coverage, stink score is a penalized value of
15
+ # If module has no coverage, skunk score is a penalized value of
16
16
  # `churn_times_cost`
17
17
  #
18
- # For now the stink_score is calculated by multiplying `churn_times_cost`
18
+ # For now the skunk_score is calculated by multiplying `churn_times_cost`
19
19
  # times the lack of coverage.
20
20
  #
21
21
  # For example:
22
22
  #
23
23
  # When `churn_times_cost` is 100 and module is perfectly covered:
24
- # stink_score => 100
24
+ # skunk_score => 100
25
25
  #
26
26
  # When `churn_times_cost` is 100 and module is not covered at all:
27
- # stink_score => 100 * 100 = 10_000
27
+ # skunk_score => 100 * 100 = 10_000
28
28
  #
29
29
  # When `churn_times_cost` is 100 and module is covered at 75%:
30
- # stink_score => 100 * 25 (percentage uncovered) = 2_500
30
+ # skunk_score => 100 * 25 (percentage uncovered) = 2_500
31
31
  #
32
32
  # @return [Float]
33
- def stink_score
33
+ def skunk_score
34
34
  return cost.round(2) if coverage == PERFECT_COVERAGE
35
35
 
36
36
  (cost * (PERFECT_COVERAGE - coverage.to_i)).round(2)
@@ -43,5 +43,27 @@ module RubyCritic
43
43
  safe_churn = churn.positive? ? churn : 1
44
44
  @churn_times_cost ||= (safe_churn * cost).round(2)
45
45
  end
46
+
47
+ # Returns a hash with these attributes:
48
+ # - file
49
+ # - skunk_score
50
+ # - churn_times_cost
51
+ # - churn
52
+ # - cost
53
+ # - coverage
54
+ #
55
+ # @return [Hash]
56
+ # rubocop:disable Style/HashSyntax
57
+ def to_hash
58
+ {
59
+ file: pathname.to_s,
60
+ skunk_score: skunk_score,
61
+ churn_times_cost: churn_times_cost,
62
+ churn: churn,
63
+ cost: cost.round(2),
64
+ coverage: coverage.round(2)
65
+ }
66
+ end
67
+ # rubocop:enable Style/HashSyntax
46
68
  end
47
69
  end
@@ -5,10 +5,10 @@ require "rubycritic/core/analysed_modules_collection"
5
5
  module RubyCritic
6
6
  # nodoc #
7
7
  class AnalysedModulesCollection
8
- def stink_score_average
8
+ def skunk_score_average
9
9
  num_modules = @modules.size
10
10
  if num_modules.positive?
11
- map(&:stink_score).reduce(:+) / num_modules.to_f
11
+ sum(&:skunk_score) / num_modules.to_f
12
12
  else
13
13
  0.0
14
14
  end
data/lib/skunk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skunk
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.2"
5
5
  end
data/lib/skunk.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "skunk/version"
4
4
 
5
- # Knows how to calculate the `StinkScore` for each file analyzed by `RubyCritic`
5
+ # Knows how to calculate the `SkunkScore` for each file analyzed by `RubyCritic`
6
6
  # and `SimpleCov`
7
7
  module Skunk
8
8
  end
data/logo.png ADDED
Binary file
@@ -0,0 +1,11 @@
1
+ **IMPORTANT**: Please read the README before submitting pull requests for this project. Additionally, if your PR closes any open GitHub issue, make sure you include Closes #XXXX in your comment.
2
+
3
+ - [ ] Add an entry to `CHANGELOG.md` that links to this PR under the "main (unreleased)" heading.
4
+
5
+ Description:
6
+
7
+ Please include a summary of the change and which issue is fixed or which feature is introduced.
8
+
9
+ If changes to the behavior are made, clearly describe what changes.
10
+
11
+ I will abide by the [code of conduct](https://github.com/fastruby/skunk/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ def hello
4
+ puts "hello world"
5
+ end