dev_metrics 0.2.0 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9932a3bb8544a01ac52f90c4a5217161e5ec4152741302f27e073d551c7c70b
4
- data.tar.gz: de933ff7150d18fbdc896ae51783bfd1955b4cdd0f2fd3b4caed1b4ca3d5c108
3
+ metadata.gz: 34f8fdcb4bcfa8353303ddc44a30dc227df6ecbf92d87b1f967c927d56ba2480
4
+ data.tar.gz: e77b9e47b4bd91487c0a4deea93df599cd1b31f778d4bcba32b8de8448d18b9b
5
5
  SHA512:
6
- metadata.gz: 235b658af21204f6c748e14049b4d65016dba87e72f798c799a58f51b225eba9c31fb0b228504bd40f3c88e13a220e0ebaf0d600d3775bdfec0b79de90c39b65
7
- data.tar.gz: f0cd5727a59f0b3259527539a8b22f196434941a2a65807ae3b1b3af8559c98b9c514f77283ca1e4c1698886a58a79569cea6a0b149ddfee07a4df62446ccb16
6
+ metadata.gz: 2b3cb0b8caec22116264a81ef34b3d62ca1c616852dc45eece9cfeead345f71901be7f91d3b87cc1d69cf66b30118fe37e90b4c82f9c6d95baf8128375fc1f9d
7
+ data.tar.gz: 0b0f53174f2f572538cac10c90c607c65fbdb421a1bac95ebf5839541ef31f218a0a955ccee0788a5e35cc7eb47755ff79ef4bbaa789b7b78f31d2649b591f63
data/bin/dev_metrics ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # filepath: bin/dev_metrics
3
+
4
+ require_relative "../lib/dev_metrics"
5
+
6
+ DevMetrics.run
@@ -7,22 +7,23 @@ require_relative 'query_builder'
7
7
  require_relative 'metrics_calc'
8
8
  require_relative 'pull_request_wrapper'
9
9
 
10
+
10
11
  module DevMetrics
11
12
  class Client
12
13
  GITHUB_GRAPHQL_API = 'https://api.github.com/graphql'.freeze
13
- ACCESS_TOKEN = ENV.fetch('GITHUB_ACCESS_TOKEN', nil)
14
14
 
15
15
  def initialize(config)
16
16
  @repo_name = config.repo_name
17
- @access_token = config.access_token || ENV.fetch('GITHUB_ACCESS_TOKEN', nil)
18
- @bot_accounts = config.bot_accounts || []
19
- @fix_branch_names = config.fix_branch_names || %w(hotfix fix rollback)
17
+ @access_token = config.access_token
18
+ @excluded_accounts = config.excluded_accounts || []
19
+ @rollback_branch_prefixes = config.rollback_branch_prefixes || %w(hotfix fix rollback)
20
20
  end
21
21
 
22
22
  def fetch(period: Date.today)
23
23
  uri = URI.parse(GITHUB_GRAPHQL_API)
24
24
  builder = DevMetrics::QueryBuilder.new(@repo_name)
25
25
 
26
+
26
27
  token = @access_token
27
28
 
28
29
  auth_request = build_request(uri, builder.access_auth_check, token)
@@ -38,8 +39,8 @@ module DevMetrics
38
39
  DevMetrics::MetricsCalc.new(
39
40
  parsed_pr_data,
40
41
  period,
41
- bot_accounts: @bot_accounts,
42
- fix_branch_names: @fix_branch_names
42
+ excluded_accounts: @excluded_accounts,
43
+ rollback_branch_prefixes: @rollback_branch_prefixes
43
44
  )
44
45
  end
45
46
 
@@ -0,0 +1,27 @@
1
+ require_relative 'format_base'
2
+
3
+ module DevMetrics
4
+ class Csv < FormatBase
5
+
6
+ def call
7
+ write
8
+ end
9
+
10
+ def file_name
11
+ "dev_metrics.csv"
12
+ end
13
+
14
+ def format
15
+ columns.map { |col| col['label'] || col }.join(',') + "\n" +
16
+ build_row.join(',')
17
+ end
18
+
19
+ def write
20
+ File.open(file_name, 'a') do |f| # ← append mode
21
+ f.puts columns.map { |col| col['label'] || col }.join(',') if File.size(file_name).zero?
22
+ f.puts build_row.join(',')
23
+ end
24
+ puts "CSV data written to #{file_name}"
25
+ end
26
+ end
27
+ end
@@ -1,25 +1,35 @@
1
+ require 'yaml'
2
+
1
3
  module DevMetrics
2
4
  class FormatBase
3
5
 
4
6
  attr_reader :metrics_calc
5
7
 
6
- def initialize(metrics_calc)
8
+ def initialize(metrics_calc, config_path = "dev_metrics_config.yml")
7
9
  @metrics_calc = metrics_calc
8
- write
10
+ @format_config = YAML.load_file(config_path)
9
11
  end
10
12
 
11
13
  private
12
14
 
13
- def data_format
14
- raise NotImplementedError, "You must implement the data_format method in a subclass"
15
- end
15
+ def write; end
16
+
17
+ def file_name; end
16
18
 
17
- def output_filename
18
- raise NotImplementedError, "You must implement the output_filename method in a subclass"
19
+ def columns
20
+ @format_config['columns'] || []
19
21
  end
20
22
 
21
- def write
22
- raise NotImplementedError, "You must implement the write method in a subclass"
23
+ def build_row
24
+ columns.map do |column|
25
+ key = column['key'] || column
26
+ begin
27
+ value = @metrics_calc.send(key)
28
+ rescue NoMethodError
29
+ value = "No defined for #{key}"
30
+ end
31
+ value
32
+ end
23
33
  end
24
34
  end
25
35
  end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+ require_relative 'format_base'
3
+
4
+ module DevMetrics
5
+ class Json < FormatBase
6
+
7
+ def call
8
+ write
9
+ end
10
+
11
+ private
12
+
13
+ def write
14
+ data = {}
15
+ columns.each_with_index do |col, idx|
16
+ key = col['label'] || col['key'] || col
17
+ data[key] = build_row[idx]
18
+ end
19
+ File.open(file_name, "w") do |file|
20
+ file.puts JSON.pretty_generate(data)
21
+ end
22
+ end
23
+
24
+ def file_name
25
+ "output.json"
26
+ end
27
+ end
28
+ end
@@ -1,34 +1,27 @@
1
- require_relative 'format_base'
2
-
3
- module DevMetrics
4
- class Markdown < FormatBase
5
-
6
- private
7
-
8
- def data_format
9
- output = ""
10
-
11
- output << "| #{metrics_calc.period} |"
12
- output << " #{metrics_calc.prs_length} |"
13
- output << " #{metrics_calc.rollback_prs_length} |"
14
- output << " #{metrics_calc.failure_rate}% |"
15
- output << " #{metrics_calc.lead_time} |"
16
- output << " #{metrics_calc.average_changed_line_size} |"
17
- output << " #{metrics_calc.average_changed_file} |"
18
- output << " [PRs for #{metrics_calc.period}](https://github.com/#{@repo_name}/pulls?q=is%3Apr+merged%3A#{metrics_calc.period}) |\n"
19
- output
20
- end
21
-
22
- def write
23
- formatted_data = data_format
24
- File.open(output_filename, 'a') do |file|
25
- file.write(formatted_data)
26
- end
27
- end
28
-
29
-
30
- def output_filename
31
- "metrics_report.md"
32
- end
33
- end
34
- end
1
+ require_relative 'format_base'
2
+
3
+ module DevMetrics
4
+ class Markdown < FormatBase
5
+
6
+ def call
7
+ write
8
+ end
9
+
10
+ private
11
+
12
+ def write
13
+ is_new_file = !File.exist?(file_name) || File.size(file_name).zero?
14
+ File.open(file_name, "a") do |file|
15
+ if is_new_file
16
+ file.puts "| " + columns.map { |col| col['label'] || col }.join(' | ') + " |"
17
+ file.puts "| " + (["---"] * columns.size).join(' | ') + " |"
18
+ end
19
+ file.puts "| " + build_row.join(' | ') + " |"
20
+ end
21
+ end
22
+
23
+ def file_name
24
+ "dev_metrics.md"
25
+ end
26
+ end
27
+ end
@@ -1,26 +1,30 @@
1
+ require 'pry'
2
+
1
3
  module DevMetrics
2
4
  class MetricsCalc
3
5
  attr_reader :period
4
6
 
5
- def initialize(prs, period, bot_accounts:, fix_branch_names:)
7
+ def initialize(prs, period, excluded_accounts:, rollback_branch_prefixes:)
6
8
  @prs = prs
7
9
  @period = period
8
- @bot_accounts = bot_accounts || []
9
- @fix_branch_names = fix_branch_names
10
+ @excluded_accounts = excluded_accounts || []
11
+ @rollback_branch_prefixes = rollback_branch_prefixes
10
12
  end
11
13
 
12
14
  # Returns the number of PRs excluding those created by bot accounts.
13
15
  #
14
16
  # @return [Integer] Number of PRs excluding bots.
15
17
  def prs_length
16
- count_prs_with_excluded_account(@prs, @bot_accounts).length
18
+ count_prs_with_excluded_account(@prs, @excluded_accounts).length
17
19
  end
18
20
 
19
21
  # Returns the number of rollback PRs (branch name matches fix patterns).
20
22
  #
21
23
  # @return [Integer] Number of rollback PRs.
22
24
  def rollback_prs_length
23
- @prs.count { |pr| pr.head_ref_name&.match?(/^#{@fix_branch_names.join('|')}/) }
25
+ return 0 if @rollback_branch_prefixes.nil? || @rollback_branch_prefixes.empty?
26
+ regex = /^#{@rollback_branch_prefixes.join('|')}/
27
+ @prs.count { |pr| pr.head_ref_name&.match?(regex) }
24
28
  end
25
29
 
26
30
  # Returns the average lead time for PRs (from creation to merge) as a formatted string.
@@ -200,9 +204,9 @@ module DevMetrics
200
204
 
201
205
  private
202
206
 
203
- def count_prs_with_excluded_account(prs, bot_accounts)
204
- return prs if bot_accounts.empty?
205
- prs.reject { |pr| bot_accounts.include?(pr.author) }
207
+ def count_prs_with_excluded_account(prs, excluded_accounts)
208
+ return prs if excluded_accounts.empty?
209
+ prs.reject { |pr| excluded_accounts.include?(pr.author) }
206
210
  end
207
211
 
208
212
  def format_time(seconds)
@@ -4,6 +4,7 @@ module DevMetrics
4
4
  def initialize(repo)
5
5
  @repo = repo
6
6
  end
7
+
7
8
  def access_auth_check
8
9
  to_body <<~GRAPHQL
9
10
  {
@@ -11,7 +12,7 @@ module DevMetrics
11
12
  id
12
13
  }
13
14
  }
14
- GRAPHQL
15
+ GRAPHQL
15
16
  end
16
17
 
17
18
  def pull_requests_for(period)
@@ -22,20 +23,35 @@ module DevMetrics
22
23
  node {
23
24
  ... on PullRequest {
24
25
  url
26
+ number
25
27
  title
26
- author { login }
28
+ body
29
+ state
30
+ createdAt
31
+ updatedAt
32
+ closedAt
27
33
  mergedAt
34
+ author { login }
35
+ assignees(first: 10) { nodes { login } }
36
+ labels(first: 10) { nodes { name } }
28
37
  headRefName
29
- publishedAt
38
+ baseRefName
30
39
  additions
31
40
  deletions
32
41
  changedFiles
42
+ commits { totalCount }
43
+ reviews { totalCount }
44
+ reviewRequests { totalCount }
45
+ mergedBy { login }
46
+ milestone { title }
47
+ isDraft
48
+ mergeable
33
49
  }
34
50
  }
35
51
  }
36
52
  }
37
53
  }
38
- GRAPHQL
54
+ GRAPHQL
39
55
  end
40
56
 
41
57
  private
@@ -1,3 +1,3 @@
1
1
  module DevMetrics
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.5"
3
3
  end
data/lib/dev_metrics.rb CHANGED
@@ -1,17 +1,40 @@
1
+ require 'optparse'
2
+ require 'yaml'
1
3
  require_relative 'dev_metrics/markdown'
2
4
  require_relative 'dev_metrics/version'
3
5
  require_relative 'dev_metrics/client'
4
6
  require_relative 'dev_metrics/metrics_calc'
7
+ require_relative 'dev_metrics/csv'
8
+ require_relative 'dev_metrics/json'
9
+
5
10
 
6
11
  module DevMetrics
7
12
  class Config
8
- attr_accessor :access_token, :repo_name, :bot_accounts, :fix_branch_names
13
+ attr_accessor :access_token, :repo_name, :excluded_accounts, :rollback_branch_prefixes
9
14
 
10
15
  def initialize
11
16
  @access_token = nil
12
17
  @repo_name = nil
13
- @bot_accounts = nil
14
- @fix_branch_names = nil
18
+ @excluded_accounts = []
19
+ @rollback_branch_prefixes = []
20
+ end
21
+
22
+ def load_from_yaml(path = "dev_metrics_config.yml")
23
+ unless File.exist?(path)
24
+ warn "Config file '#{path}' not found. Please create it before running this tool."
25
+ exit 1
26
+ end
27
+
28
+ config = YAML.load_file(path)
29
+ if config["access_token"].nil? || config["repo_name"].nil?
30
+ warn "Config file must include 'access_token' and 'repo_name'."
31
+ exit 1
32
+ end
33
+
34
+ @access_token = config["access_token"]
35
+ @repo_name = config["repo_name"]
36
+ @excluded_accounts = config["excluded_accounts"] || []
37
+ @rollback_branch_prefixes = config["rollback_branch_prefixes"] || []
15
38
  end
16
39
  end
17
40
 
@@ -21,15 +44,57 @@ module DevMetrics
21
44
  @configuration
22
45
  end
23
46
 
24
- def self.configure
25
- yield(configuration)
47
+ def self.option_parse(argv)
48
+ options = {
49
+ period: (Date.today << 1).strftime("%Y-%m"),
50
+ format: "csv",
51
+ config: "dev_metrics_config.yml"
52
+ }
53
+ OptionParser.new do |opts|
54
+ opts.banner = "Usage: dev_metrics [options]"
55
+
56
+ opts.on("-c", "--config FILE", "Specify config YAML file (default: dev_metrics_config.yml)") do |file|
57
+ options[:config] = file
58
+ end
59
+
60
+ opts.on("-p", "--period PERIOD", "Specify the period for metrics (e.g., '2025-05' or '2025-05-01..2025-05-31')") do |period|
61
+ options[:period] = period
62
+ end
63
+
64
+ opts.on("-f", "--format FORMAT", "Specify the output format (e.g., 'csv', 'json', 'markdown')") do |format|
65
+ options[:format] = format
66
+ end
67
+
68
+ opts.on("-h", "--help", "Display this help message") do
69
+ puts opts
70
+ exit
71
+ end
72
+ end.parse!(argv)
73
+ options
26
74
  end
27
75
 
28
- # Run: fetch PRs, calculate metrics, and pass to formatter
29
- def self.run(period:, format:)
76
+ def self.run(argv: nil)
77
+ argv ||= ARGV
78
+ options = option_parse(argv)
79
+ config_file = options[:config] || "dev_metrics_config.yml"
80
+ @configuration.load_from_yaml(config_file)
81
+ period = options[:period]
82
+ format = options[:format]
83
+
30
84
  client = DevMetrics::Client.new(@configuration)
31
85
  prs = client.fetch(period: period)
32
86
 
33
- format.new(prs)
87
+ case format
88
+ when "csv"
89
+ DevMetrics::Csv.new(prs).call
90
+ when "markdown"
91
+ DevMetrics::Markdown.new(prs).call
92
+ when "json"
93
+ DevMetrics::Json.new(prs).call
94
+ else
95
+ warn "Unknown format: #{format}"
96
+ warn "Available formats: csv, markdown, json"
97
+ exit 1
98
+ end
34
99
  end
35
100
  end
metadata CHANGED
@@ -1,31 +1,33 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dev_metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - sean2121
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-16 00:00:00.000000000 Z
11
+ date: 2025-06-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
15
- executables: []
15
+ executables:
16
+ - dev_metrics
16
17
  extensions: []
17
18
  extra_rdoc_files: []
18
19
  files:
20
+ - bin/dev_metrics
19
21
  - lib/dev_metrics.rb
20
22
  - lib/dev_metrics/client.rb
23
+ - lib/dev_metrics/csv.rb
21
24
  - lib/dev_metrics/format_base.rb
25
+ - lib/dev_metrics/json.rb
22
26
  - lib/dev_metrics/markdown.rb
23
27
  - lib/dev_metrics/metrics_calc.rb
24
28
  - lib/dev_metrics/pull_request_wrapper.rb
25
29
  - lib/dev_metrics/query_builder.rb
26
30
  - lib/dev_metrics/version.rb
27
- - lib/spec/dev_metrics/metrics_calc_spec.rb
28
- - lib/spec/spec_helper.rb
29
31
  homepage:
30
32
  licenses:
31
33
  - MIT
@@ -45,7 +47,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
47
  - !ruby/object:Gem::Version
46
48
  version: '0'
47
49
  requirements: []
48
- rubygems_version: 3.3.5
50
+ rubygems_version: 3.2.33
49
51
  signing_key:
50
52
  specification_version: 4
51
53
  summary: A RubyGem for tracking and analyzing development metrics from various repositories.
@@ -1,107 +0,0 @@
1
- require_relative '../spec_helper'
2
- require 'dev_metrics/metrics_calc'
3
- require 'time'
4
-
5
- RSpec.describe DevMetrics::MetricsCalc do
6
- let(:period) { "2025-04" }
7
- let(:config) { { bot_accounts: [], fix_branch_names: %w(hotfix fix rollback) } }
8
-
9
- let(:pr1) do
10
- double(
11
- additions: 10,
12
- deletions: 2,
13
- changed_files: 1,
14
- commits_count: 3,
15
- reviews_count: 2,
16
- review_requests_count: 1,
17
- draft?: false,
18
- labels: ["bug"],
19
- assignees: ["alice"],
20
- milestone: "v1.0",
21
- created_at: Time.parse("2025-04-01T10:00:00Z"),
22
- closed_at: Time.parse("2025-04-02T10:00:00Z"),
23
- merged_at: Time.parse("2025-04-02T10:00:00Z"),
24
- head_ref_name: "feature/abc",
25
- author: "alice"
26
- )
27
- end
28
-
29
- let(:pr2) do
30
- double(
31
- additions: 5,
32
- deletions: 5,
33
- changed_files: 2,
34
- commits_count: 2,
35
- reviews_count: 1,
36
- review_requests_count: 0,
37
- draft?: true,
38
- labels: ["enhancement"],
39
- assignees: ["bob"],
40
- milestone: "v1.0",
41
- created_at: Time.parse("2025-04-03T10:00:00Z"),
42
- closed_at: Time.parse("2025-04-04T10:00:00Z"),
43
- merged_at: nil,
44
- head_ref_name: "fix/xyz",
45
- author: "bob"
46
- )
47
- end
48
-
49
- let(:prs) { [pr1, pr2] }
50
-
51
- subject do
52
- described_class.new(
53
- prs,
54
- period,
55
- bot_accounts: config[:bot_accounts],
56
- fix_branch_names: config[:fix_branch_names]
57
- )
58
- end
59
-
60
- it "calculates prs_length" do
61
- expect(subject.prs_length).to eq 2
62
- end
63
-
64
- it "calculates rollback_prs_length" do
65
- expect(subject.rollback_prs_length).to eq 1
66
- end
67
-
68
- it "calculates average_changed_line_size" do
69
- expect(subject.average_changed_line_size).to eq 11.0
70
- end
71
-
72
- it "calculates average_commits_count" do
73
- expect(subject.average_commits_count).to eq 2.5
74
- end
75
-
76
- it "calculates draft_pr_rate" do
77
- expect(subject.draft_pr_rate).to eq 50.0
78
- end
79
-
80
- it "calculates label_counts" do
81
- expect(subject.label_counts).to eq({ "bug" => 1, "enhancement" => 1 })
82
- end
83
-
84
- it "calculates assignee_counts" do
85
- expect(subject.assignee_counts).to eq({ "alice" => 1, "bob" => 1 })
86
- end
87
-
88
- it "calculates milestone_counts" do
89
- expect(subject.milestone_counts).to eq({ "v1.0" => 2 })
90
- end
91
-
92
- it "calculates average_pr_age" do
93
- expect(subject.average_pr_age).to be_a(Float)
94
- end
95
-
96
- it "calculates draft_pr_count" do
97
- expect(subject.draft_pr_count).to eq 1
98
- end
99
-
100
- it "calculates merged_pr_count" do
101
- expect(subject.merged_pr_count).to eq 1
102
- end
103
-
104
- it "calculates closed_unmerged_pr_count" do
105
- expect(subject.closed_unmerged_pr_count).to eq 1
106
- end
107
- end
@@ -1,3 +0,0 @@
1
- require 'rspec'
2
-
3
- $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)