github-pulse 0.2.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.
- checksums.yaml +7 -0
- data/AGENTS.md +45 -0
- data/README.md +186 -0
- data/Rakefile +4 -0
- data/exe/github-pulse +6 -0
- data/github-pulse-report.html +330 -0
- data/lib/github/pulse/analyzer.rb +438 -0
- data/lib/github/pulse/cli.rb +78 -0
- data/lib/github/pulse/date_helpers.rb +19 -0
- data/lib/github/pulse/gh_client.rb +247 -0
- data/lib/github/pulse/git_analyzer.rb +167 -0
- data/lib/github/pulse/github_client.rb +115 -0
- data/lib/github/pulse/html_reporter.rb +747 -0
- data/lib/github/pulse/reporter.rb +132 -0
- data/lib/github/pulse/version.rb +7 -0
- data/lib/github/pulse.rb +15 -0
- data/sig/github/pulse.rbs +6 -0
- metadata +134 -0
@@ -0,0 +1,438 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
require_relative "date_helpers"
|
5
|
+
|
6
|
+
module Github
|
7
|
+
module Pulse
|
8
|
+
class Analyzer
|
9
|
+
using DateHelpers
|
10
|
+
attr_reader :repo_path, :github_repo, :token, :since, :until_date, :small_threshold, :medium_threshold
|
11
|
+
|
12
|
+
def initialize(repo_path:, github_repo: nil, token: nil, since: nil, until: nil, small_threshold: 50, medium_threshold: 250)
|
13
|
+
@repo_path = File.expand_path(repo_path)
|
14
|
+
@github_repo = github_repo
|
15
|
+
@token = token
|
16
|
+
@since = since
|
17
|
+
@until_date = binding.local_variable_get(:until)
|
18
|
+
@small_threshold = small_threshold
|
19
|
+
@medium_threshold = medium_threshold
|
20
|
+
end
|
21
|
+
|
22
|
+
def analyze
|
23
|
+
report = {
|
24
|
+
metadata: {
|
25
|
+
analyzed_at: Time.now.iso8601,
|
26
|
+
repository: nil,
|
27
|
+
period: {
|
28
|
+
since: since,
|
29
|
+
until: until_date
|
30
|
+
}
|
31
|
+
},
|
32
|
+
pull_requests: {},
|
33
|
+
commits: {},
|
34
|
+
lines_of_code: {},
|
35
|
+
commit_activity: {},
|
36
|
+
visualization_data: {}
|
37
|
+
}
|
38
|
+
|
39
|
+
# Try to analyze local git repository ONLY if no explicit GitHub repo is specified
|
40
|
+
# OR if the local repo matches the specified GitHub repo
|
41
|
+
should_analyze_local = false
|
42
|
+
local_github_repo = nil
|
43
|
+
|
44
|
+
if File.exist?(File.join(repo_path, ".git"))
|
45
|
+
git_analyzer = GitAnalyzer.new(repo_path)
|
46
|
+
local_github_repo = git_analyzer.remote_url
|
47
|
+
|
48
|
+
# Only analyze local git if:
|
49
|
+
# 1. No GitHub repo was specified (analyzing local only)
|
50
|
+
# 2. The specified GitHub repo matches the local repo's remote
|
51
|
+
if !github_repo || (github_repo == local_github_repo)
|
52
|
+
should_analyze_local = true
|
53
|
+
|
54
|
+
# Get commits by author
|
55
|
+
commits = git_analyzer.analyze_commits(since: since, until_date: until_date)
|
56
|
+
report[:commits] = format_commits_data(commits)
|
57
|
+
|
58
|
+
# Get lines of code by author
|
59
|
+
report[:lines_of_code] = git_analyzer.lines_of_code_by_author
|
60
|
+
|
61
|
+
# Get commit activity
|
62
|
+
report[:commit_activity] = git_analyzer.commit_activity_by_day
|
63
|
+
|
64
|
+
# Set GitHub repo from remote if not specified
|
65
|
+
@github_repo ||= local_github_repo
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# If we have GitHub repo info, fetch additional data
|
70
|
+
if github_repo
|
71
|
+
# Try gh CLI first if no token provided
|
72
|
+
if !token
|
73
|
+
require_relative "gh_client"
|
74
|
+
gh_client = GhClient.new(repo: github_repo)
|
75
|
+
|
76
|
+
if gh_client.available?
|
77
|
+
puts "Using GitHub CLI (gh) for API access..."
|
78
|
+
|
79
|
+
# Get repository info
|
80
|
+
report[:metadata][:repository] = gh_client.repository_info
|
81
|
+
|
82
|
+
# Get pull requests
|
83
|
+
prs = gh_client.pull_requests(since: since, until_date: until_date)
|
84
|
+
report[:pull_requests] = format_pull_requests_data(prs)
|
85
|
+
|
86
|
+
# Get contributor statistics
|
87
|
+
contrib_stats = gh_client.contributors_stats
|
88
|
+
report[:contributor_stats] = format_contributor_stats(contrib_stats)
|
89
|
+
|
90
|
+
# Get commits from GitHub if not analyzing local
|
91
|
+
if !should_analyze_local
|
92
|
+
commits = gh_client.commits_data(since: since, until_date: until_date)
|
93
|
+
report[:commits] = format_commits_data(commits)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get commit activity from GitHub
|
97
|
+
if report[:commit_activity].empty?
|
98
|
+
activity = gh_client.commit_activity
|
99
|
+
report[:commit_activity] = format_commit_activity(activity)
|
100
|
+
end
|
101
|
+
else
|
102
|
+
puts "Warning: No GitHub token provided and gh CLI not available or not authenticated."
|
103
|
+
puts "To enable GitHub features, either:"
|
104
|
+
puts " 1. Set GITHUB_TOKEN environment variable"
|
105
|
+
puts " 2. Install and authenticate gh CLI: https://cli.github.com"
|
106
|
+
end
|
107
|
+
else
|
108
|
+
# Use token-based client
|
109
|
+
github_client = GithubClient.new(repo: github_repo, token: token)
|
110
|
+
|
111
|
+
# Get repository info
|
112
|
+
report[:metadata][:repository] = github_client.repository_info
|
113
|
+
|
114
|
+
# Get pull requests
|
115
|
+
prs = github_client.pull_requests(since: since, until_date: until_date)
|
116
|
+
report[:pull_requests] = format_pull_requests_data(prs)
|
117
|
+
|
118
|
+
# Get contributor statistics
|
119
|
+
contrib_stats = github_client.contributors_stats
|
120
|
+
report[:contributor_stats] = format_contributor_stats(contrib_stats)
|
121
|
+
|
122
|
+
# Get commits from GitHub if not analyzing local
|
123
|
+
if !should_analyze_local
|
124
|
+
# For token-based client, we'd need to implement a commits method
|
125
|
+
# For now, contributor stats provides some commit data
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get commit activity from GitHub
|
129
|
+
if report[:commit_activity].empty?
|
130
|
+
activity = github_client.commit_activity
|
131
|
+
report[:commit_activity] = format_commit_activity(activity)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Generate visualization data
|
137
|
+
report[:visualization_data] = generate_visualization_data(report)
|
138
|
+
|
139
|
+
report
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def format_commits_data(commits_by_author)
|
145
|
+
formatted = {}
|
146
|
+
|
147
|
+
commits_by_author.each do |author, commits|
|
148
|
+
formatted[author] = {
|
149
|
+
total_commits: commits.size,
|
150
|
+
total_additions: commits.sum { |c| c[:additions] },
|
151
|
+
total_deletions: commits.sum { |c| c[:deletions] },
|
152
|
+
commits: commits.map do |c|
|
153
|
+
{
|
154
|
+
sha: c[:sha][0..7],
|
155
|
+
message: c[:message],
|
156
|
+
time: c[:time].iso8601,
|
157
|
+
additions: c[:additions],
|
158
|
+
deletions: c[:deletions]
|
159
|
+
}
|
160
|
+
end
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
formatted
|
165
|
+
end
|
166
|
+
|
167
|
+
def format_pull_requests_data(prs)
|
168
|
+
by_author = {}
|
169
|
+
|
170
|
+
prs.each do |pr|
|
171
|
+
author = pr[:author]
|
172
|
+
by_author[author] ||= {
|
173
|
+
total_prs: 0,
|
174
|
+
merged: 0,
|
175
|
+
open: 0,
|
176
|
+
closed: 0,
|
177
|
+
total_additions: 0,
|
178
|
+
total_deletions: 0,
|
179
|
+
pull_requests: []
|
180
|
+
}
|
181
|
+
|
182
|
+
by_author[author][:total_prs] += 1
|
183
|
+
by_author[author][:merged] += 1 if pr[:merged_at]
|
184
|
+
by_author[author][:open] += 1 if pr[:state] == "open"
|
185
|
+
by_author[author][:closed] += 1 if pr[:state] == "closed" && !pr[:merged_at]
|
186
|
+
by_author[author][:total_additions] += pr[:additions]
|
187
|
+
by_author[author][:total_deletions] += pr[:deletions]
|
188
|
+
|
189
|
+
by_author[author][:pull_requests] << {
|
190
|
+
number: pr[:number],
|
191
|
+
title: pr[:title],
|
192
|
+
created_at: pr[:created_at].iso8601,
|
193
|
+
merged_at: pr[:merged_at]&.iso8601,
|
194
|
+
closed_at: pr[:closed_at]&.iso8601,
|
195
|
+
state: pr[:state],
|
196
|
+
merged: !pr[:merged_at].nil?,
|
197
|
+
additions: pr[:additions],
|
198
|
+
deletions: pr[:deletions],
|
199
|
+
changed_files: pr[:changed_files]
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
by_author
|
204
|
+
end
|
205
|
+
|
206
|
+
def format_contributor_stats(stats)
|
207
|
+
return {} unless stats
|
208
|
+
|
209
|
+
formatted = {}
|
210
|
+
|
211
|
+
stats.each do |contributor|
|
212
|
+
author = contributor[:author]
|
213
|
+
formatted[author] = {
|
214
|
+
total_commits: contributor[:total_commits],
|
215
|
+
weekly_activity: contributor[:weeks].select { |w| w[:commits] > 0 }.map do |week|
|
216
|
+
{
|
217
|
+
week: week[:week_start].iso8601,
|
218
|
+
commits: week[:commits],
|
219
|
+
additions: week[:additions],
|
220
|
+
deletions: week[:deletions]
|
221
|
+
}
|
222
|
+
end
|
223
|
+
}
|
224
|
+
end
|
225
|
+
|
226
|
+
formatted
|
227
|
+
end
|
228
|
+
|
229
|
+
def format_commit_activity(activity)
|
230
|
+
return {} unless activity
|
231
|
+
|
232
|
+
activity_hash = {}
|
233
|
+
|
234
|
+
activity.each do |week|
|
235
|
+
date = week[:week_start]
|
236
|
+
activity_hash[date.iso8601] = {
|
237
|
+
total: week[:total],
|
238
|
+
days: week[:days]
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
activity_hash
|
243
|
+
end
|
244
|
+
|
245
|
+
def generate_visualization_data(report)
|
246
|
+
viz_data = {}
|
247
|
+
|
248
|
+
# Pull requests over time (for stacked bar chart)
|
249
|
+
if report[:pull_requests].any?
|
250
|
+
pr_timeline = Hash.new { |h, k| h[k] = Hash.new(0) }
|
251
|
+
|
252
|
+
report[:pull_requests].each do |author, data|
|
253
|
+
data[:pull_requests].each do |pr|
|
254
|
+
date = Date.parse(pr[:created_at]).beginning_of_month.iso8601
|
255
|
+
pr_timeline[date][author] += 1
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
viz_data[:pull_requests_timeline] = pr_timeline.sort.map do |date, authors|
|
260
|
+
{ date: date, authors: authors }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Lines of code by author (for bar chart)
|
265
|
+
if report[:lines_of_code].any?
|
266
|
+
viz_data[:lines_of_code_chart] = report[:lines_of_code].map do |author, lines|
|
267
|
+
{ author: author, lines: lines }
|
268
|
+
end.sort_by { |d| -d[:lines] }
|
269
|
+
end
|
270
|
+
|
271
|
+
# Commit activity over time (for line chart)
|
272
|
+
if report[:commit_activity].any?
|
273
|
+
viz_data[:commit_activity_chart] = report[:commit_activity].map do |date, count|
|
274
|
+
{ date: date.to_s, commits: count }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Commits by author over time (for stacked area chart)
|
279
|
+
if report[:commits].any?
|
280
|
+
commit_timeline = Hash.new { |h, k| h[k] = Hash.new(0) }
|
281
|
+
|
282
|
+
report[:commits].each do |author, data|
|
283
|
+
data[:commits].each do |commit|
|
284
|
+
date = Date.parse(commit[:time]).beginning_of_week.iso8601
|
285
|
+
commit_timeline[date][author] += 1
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
viz_data[:commits_timeline] = commit_timeline.sort.map do |date, authors|
|
290
|
+
{ date: date, authors: authors }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Lines changed over time by author (additions + deletions)
|
295
|
+
if report[:contributor_stats] && report[:contributor_stats].any?
|
296
|
+
lines_timeline = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = { additions: 0, deletions: 0 } } }
|
297
|
+
|
298
|
+
report[:contributor_stats].each do |author, data|
|
299
|
+
data[:weekly_activity].each do |week|
|
300
|
+
date = week[:week]
|
301
|
+
lines_timeline[date][author][:additions] += week[:additions]
|
302
|
+
lines_timeline[date][author][:deletions] += week[:deletions]
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
viz_data[:lines_changed_timeline] = lines_timeline.sort.map do |date, authors|
|
307
|
+
{ date: date, authors: authors }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# PR cycle time over time (p50/p90/max days by week)
|
312
|
+
if report[:pull_requests].any?
|
313
|
+
by_week = Hash.new { |h, k| h[k] = [] }
|
314
|
+
report[:pull_requests].each do |_author, data|
|
315
|
+
data[:pull_requests].each do |pr|
|
316
|
+
next unless pr[:merged]
|
317
|
+
begin
|
318
|
+
created = Date.parse(pr[:created_at])
|
319
|
+
# merged_at not stored in nested pr, but we track merged flag only; fall back to closed date when merged
|
320
|
+
# We didn't include timestamps other than created_at; skip if unavailable
|
321
|
+
rescue
|
322
|
+
next
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
# We can compute cycle time only if detailed merged_at is available in PRs
|
327
|
+
# The GitHub clients provide merged_at at top-level before formatting.
|
328
|
+
# To preserve it, check if we stored it in pull_requests data; if not, skip this section.
|
329
|
+
# Detect presence
|
330
|
+
has_merged_at = report[:pull_requests].values.any? do |d|
|
331
|
+
d[:pull_requests].any? { |pr| pr.key?(:merged_at) }
|
332
|
+
end rescue false
|
333
|
+
|
334
|
+
if has_merged_at
|
335
|
+
by_week = Hash.new { |h, k| h[k] = [] }
|
336
|
+
report[:pull_requests].each do |_author, data|
|
337
|
+
data[:pull_requests].each do |pr|
|
338
|
+
next unless pr[:merged]
|
339
|
+
begin
|
340
|
+
created = Time.parse(pr[:created_at])
|
341
|
+
merged_at = Time.parse(pr[:merged_at])
|
342
|
+
days = ((merged_at - created) / 86400.0)
|
343
|
+
week = Date.parse(pr[:created_at]).beginning_of_week.iso8601
|
344
|
+
by_week[week] << days
|
345
|
+
rescue
|
346
|
+
next
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
viz_data[:pr_cycle_time_timeline] = by_week.sort.map do |week, values|
|
352
|
+
sorted = values.sort
|
353
|
+
p50 = percentile(sorted, 0.5)
|
354
|
+
p90 = percentile(sorted, 0.9)
|
355
|
+
{ week: week, p50: p50.round(2), p90: p90.round(2), max: sorted.last.round(2), count: values.size }
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# PR size mix over time (small/medium/large by week)
|
361
|
+
if report[:pull_requests].any?
|
362
|
+
buckets_by_week = Hash.new { |h, k| h[k] = { small: 0, medium: 0, large: 0 } }
|
363
|
+
report[:pull_requests].each do |_author, data|
|
364
|
+
data[:pull_requests].each do |pr|
|
365
|
+
begin
|
366
|
+
additions = pr[:additions] || 0
|
367
|
+
deletions = pr[:deletions] || 0
|
368
|
+
size = additions + deletions
|
369
|
+
week = Date.parse(pr[:created_at]).beginning_of_week.iso8601
|
370
|
+
if size <= small_threshold
|
371
|
+
buckets_by_week[week][:small] += 1
|
372
|
+
elsif size <= medium_threshold
|
373
|
+
buckets_by_week[week][:medium] += 1
|
374
|
+
else
|
375
|
+
buckets_by_week[week][:large] += 1
|
376
|
+
end
|
377
|
+
rescue
|
378
|
+
next
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
viz_data[:pr_size_mix_timeline] = buckets_by_week.sort.map { |week, counts| { week: week, **counts } }
|
383
|
+
end
|
384
|
+
|
385
|
+
# Commit activity heatmap (weekday x hour)
|
386
|
+
if report[:commits].any?
|
387
|
+
heatmap = Array.new(7) { Array.new(24, 0) }
|
388
|
+
report[:commits].each do |_author, data|
|
389
|
+
data[:commits].each do |commit|
|
390
|
+
begin
|
391
|
+
t = Time.parse(commit[:time])
|
392
|
+
wday = t.wday # 0..6 (Sun..Sat)
|
393
|
+
hour = t.hour
|
394
|
+
heatmap[wday][hour] += 1
|
395
|
+
rescue
|
396
|
+
next
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
viz_data[:commit_activity_heatmap] = heatmap
|
401
|
+
end
|
402
|
+
|
403
|
+
# Open PRs aging buckets
|
404
|
+
if report[:pull_requests].any?
|
405
|
+
buckets = { "0-3d" => 0, "4-7d" => 0, "8-14d" => 0, "15+d" => 0 }
|
406
|
+
report[:pull_requests].each do |_author, data|
|
407
|
+
data[:pull_requests].each do |pr|
|
408
|
+
next unless pr[:state] == "open"
|
409
|
+
begin
|
410
|
+
age_days = ((Time.now - Time.parse(pr[:created_at])) / 86400.0)
|
411
|
+
key = case age_days
|
412
|
+
when 0..3 then "0-3d"
|
413
|
+
when 3...7 then "4-7d"
|
414
|
+
when 7...15 then "8-14d"
|
415
|
+
else "15+d"
|
416
|
+
end
|
417
|
+
buckets[key] += 1
|
418
|
+
rescue
|
419
|
+
next
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
viz_data[:open_prs_aging] = buckets
|
424
|
+
end
|
425
|
+
|
426
|
+
viz_data
|
427
|
+
end
|
428
|
+
|
429
|
+
def percentile(sorted_values, p)
|
430
|
+
return 0 if sorted_values.empty?
|
431
|
+
rank = (p * (sorted_values.length - 1))
|
432
|
+
lower = sorted_values[rank.floor]
|
433
|
+
upper = sorted_values[rank.ceil]
|
434
|
+
lower + (upper - lower) * (rank - rank.floor)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Github
|
7
|
+
module Pulse
|
8
|
+
class CLI < Thor
|
9
|
+
desc "analyze [REPO_PATH]", "Analyze GitHub repository activity"
|
10
|
+
option :repo, type: :string, desc: "GitHub repository (owner/repo format)"
|
11
|
+
option :token, type: :string, desc: "GitHub token (or set GITHUB_TOKEN env, or use gh CLI)"
|
12
|
+
option :output, type: :string, default: "github-pulse-report.json", desc: "Output file path"
|
13
|
+
option :format, type: :string, default: "json", enum: ["json", "pretty", "html"], desc: "Output format"
|
14
|
+
option :since, type: :string, desc: "Analyze activity since this date (YYYY-MM-DD)"
|
15
|
+
option :until, type: :string, desc: "Analyze activity until this date (YYYY-MM-DD)"
|
16
|
+
option :small_threshold, type: :numeric, default: 50, desc: "PR size threshold for 'small' (additions+deletions)"
|
17
|
+
option :medium_threshold, type: :numeric, default: 250, desc: "PR size threshold for 'medium' (additions+deletions)"
|
18
|
+
|
19
|
+
def analyze(repo_path = ".")
|
20
|
+
token = options[:token] || ENV["GITHUB_TOKEN"]
|
21
|
+
|
22
|
+
analyzer = Analyzer.new(
|
23
|
+
repo_path: repo_path,
|
24
|
+
github_repo: options[:repo],
|
25
|
+
token: token,
|
26
|
+
since: options[:since],
|
27
|
+
until: options[:until],
|
28
|
+
small_threshold: options[:small_threshold],
|
29
|
+
medium_threshold: options[:medium_threshold]
|
30
|
+
)
|
31
|
+
|
32
|
+
puts "Analyzing repository activity..."
|
33
|
+
report = analyzer.analyze
|
34
|
+
|
35
|
+
output_file = options[:output]
|
36
|
+
|
37
|
+
if options[:format] == "html"
|
38
|
+
require_relative "html_reporter"
|
39
|
+
reporter = HtmlReporter.new(report)
|
40
|
+
output = reporter.generate
|
41
|
+
output_file = output_file.sub(/\.json$/, '.html') unless output_file.end_with?('.html')
|
42
|
+
else
|
43
|
+
reporter = Reporter.new(report)
|
44
|
+
output = reporter.generate(format: options[:format].to_sym)
|
45
|
+
end
|
46
|
+
|
47
|
+
File.write(output_file, output)
|
48
|
+
puts "Report saved to #{output_file}"
|
49
|
+
|
50
|
+
if options[:format] == "pretty"
|
51
|
+
puts "\n" + output
|
52
|
+
elsif options[:format] == "html"
|
53
|
+
puts "Open #{output_file} in your browser to view the interactive report"
|
54
|
+
|
55
|
+
# Try to open in browser automatically
|
56
|
+
case RUBY_PLATFORM
|
57
|
+
when /darwin/
|
58
|
+
system("open", output_file)
|
59
|
+
when /linux/
|
60
|
+
system("xdg-open", output_file)
|
61
|
+
when /win32|mingw/
|
62
|
+
system("start", output_file)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue StandardError => e
|
66
|
+
say "Error: #{e.message}", :red
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "version", "Display version"
|
71
|
+
def version
|
72
|
+
puts "github-pulse #{Github::Pulse::VERSION}"
|
73
|
+
end
|
74
|
+
|
75
|
+
default_task :analyze
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module Github
|
6
|
+
module Pulse
|
7
|
+
module DateHelpers
|
8
|
+
refine Date do
|
9
|
+
def beginning_of_week
|
10
|
+
self - (self.cwday - 1)
|
11
|
+
end
|
12
|
+
|
13
|
+
def beginning_of_month
|
14
|
+
Date.new(year, month, 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|