spill 0.1.0 → 0.1.1
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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +5 -1
- data/lib/spill/cli.rb +6 -1
- data/lib/spill/collectors/github.rb +70 -10
- data/lib/spill/renderer.rb +9 -2
- data/lib/spill/repo_remotes.rb +29 -0
- data/lib/spill/report.rb +12 -2
- data/lib/spill/version.rb +1 -1
- data/lib/spill.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7713f1ee707ff465eaba5f7fe683537dff7ba79331f9eebdf067dac2cad9f2e
|
|
4
|
+
data.tar.gz: a4e1c363ee31bcaff8a1c704f1a79457180e3bc0d1b65d4a6b8bac7bf1356a2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29fe4ca3b85ce7433e870763062cf498fa18bf784ada04be13afec01be93782f9fb4d42180263b910f739ee181eb919b2d33198953fa6a130d8f974be97d004a
|
|
7
|
+
data.tar.gz: 3a7a4ffb24c0ddf4ad35b26d1a773561774c03e4498e946b60016e50fac710e4ee6b6b6dc4a9e6fbc211c35879e4e3ed69f8232d3267040df7cc3452f473a47b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.1] - 2026-07-04
|
|
4
|
+
|
|
5
|
+
- Merged PRs now sourced via the search API instead of the events feed, fixing
|
|
6
|
+
attribution (merges no longer get lost or misattributed to the merger).
|
|
7
|
+
- Opened PRs are sourced via the search API too, restoring titles that the thin
|
|
8
|
+
events-feed payload didn't carry.
|
|
9
|
+
- New coverage: `:commented` events (issue and PR review comments, deduped per
|
|
10
|
+
thread) and an `Explored:` line for repos you starred.
|
|
11
|
+
- GitHub work events are scoped to the repos under the scanned root (matched by
|
|
12
|
+
origin remote); starred repos stay global.
|
|
13
|
+
|
|
3
14
|
## [0.1.0] - 2026-07-04
|
|
4
15
|
|
|
5
16
|
- Initial release: Done/Doing standup report from local git and GitHub (`gh`).
|
data/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Your standup, spilled. `spill` scans a folder of git repos and prints what you
|
|
4
4
|
**did** and what you're **doing** — synthesized from local git and (optionally)
|
|
5
5
|
your GitHub activity via the `gh` CLI. Not a commit dump: merged PRs, reviews
|
|
6
|
-
you gave, branches in flight, uncommitted work
|
|
6
|
+
you gave, comments you left, branches in flight, uncommitted work — even the
|
|
7
|
+
repos you starred along the way.
|
|
7
8
|
|
|
8
9
|
$ cd ~/code && spill
|
|
9
10
|
|
|
@@ -16,6 +17,7 @@ you gave, branches in flight, uncommitted work.
|
|
|
16
17
|
GitHub
|
|
17
18
|
merged PR #12 (acme/website) — Fix nav
|
|
18
19
|
reviewed PR #87 (acme/dashboard) — Payout calc
|
|
20
|
+
commented on #103 (acme/website) — Rate limiting rollout
|
|
19
21
|
|
|
20
22
|
DOING
|
|
21
23
|
icebreaker-bingo · feed-page: 3 unpushed commits
|
|
@@ -24,6 +26,8 @@ you gave, branches in flight, uncommitted work.
|
|
|
24
26
|
|
|
25
27
|
3 quiet repos skipped
|
|
26
28
|
|
|
29
|
+
Explored: nilbuild/git-standup
|
|
30
|
+
|
|
27
31
|
## Install
|
|
28
32
|
|
|
29
33
|
gem install spill
|
data/lib/spill/cli.rb
CHANGED
|
@@ -37,9 +37,14 @@ module Spill
|
|
|
37
37
|
repo_paths = RepoFinder.find(args.first || Dir.pwd)
|
|
38
38
|
local = Collectors::LocalGit.new(repo_paths: repo_paths, author: options[:author])
|
|
39
39
|
.collect(window: window)
|
|
40
|
-
github = options[:github] ?
|
|
40
|
+
github = options[:github] ? fetch_github(repo_paths, window) : []
|
|
41
41
|
Report.build(local: local, github: github,
|
|
42
42
|
repos: repo_paths.map { |path| File.basename(path) }, window: window)
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
def self.fetch_github(repo_paths, window)
|
|
46
|
+
scope = RepoRemotes.github_slugs(repo_paths)
|
|
47
|
+
Collectors::Github.new.collect(window: window, scope: scope)
|
|
48
|
+
end
|
|
44
49
|
end
|
|
45
50
|
end
|
|
@@ -19,7 +19,7 @@ module Spill
|
|
|
19
19
|
@runner = runner
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def collect(window:)
|
|
22
|
+
def collect(window:, scope: nil)
|
|
23
23
|
login = fetch_login
|
|
24
24
|
return nil if login.nil?
|
|
25
25
|
|
|
@@ -28,11 +28,22 @@ module Spill
|
|
|
28
28
|
|
|
29
29
|
events = raw.filter_map { |item| map_event(item) }
|
|
30
30
|
.select { |event| event.timestamp >= window.since }
|
|
31
|
+
events = dedupe_commented(events)
|
|
32
|
+
|
|
31
33
|
open_prs = open_pr_events(login)
|
|
32
34
|
return nil if open_prs.nil?
|
|
33
35
|
|
|
36
|
+
merged_prs = merged_pr_events(login, window)
|
|
37
|
+
return nil if merged_prs.nil?
|
|
38
|
+
|
|
39
|
+
opened_prs = opened_pr_events(login, window)
|
|
40
|
+
return nil if opened_prs.nil?
|
|
41
|
+
|
|
34
42
|
events.concat(open_prs)
|
|
43
|
+
events.concat(merged_prs)
|
|
44
|
+
events.concat(opened_prs)
|
|
35
45
|
events << truncation_event(raw) if truncated?(raw, window)
|
|
46
|
+
events = apply_scope(events, scope) unless scope.nil?
|
|
36
47
|
events
|
|
37
48
|
rescue StandardError
|
|
38
49
|
nil
|
|
@@ -40,6 +51,12 @@ module Spill
|
|
|
40
51
|
|
|
41
52
|
private
|
|
42
53
|
|
|
54
|
+
def apply_scope(events, scope)
|
|
55
|
+
events.select do |event|
|
|
56
|
+
event.kind == :starred || event.kind == :github_truncated || scope.include?(event.repo.to_s.downcase)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
def fetch_login
|
|
44
61
|
out, ok = @runner.call([ "api", "user" ])
|
|
45
62
|
return nil unless ok
|
|
@@ -71,21 +88,25 @@ module Spill
|
|
|
71
88
|
|
|
72
89
|
time = Time.parse(created).localtime
|
|
73
90
|
case item["type"]
|
|
74
|
-
when "PullRequestEvent" then map_pull_request(item, repo, time)
|
|
75
91
|
when "PullRequestReviewEvent" then build(:review, item.dig("payload", "pull_request"), repo, time)
|
|
76
92
|
when "IssuesEvent"
|
|
77
93
|
build(:issue_closed, item.dig("payload", "issue"), repo, time) if item.dig("payload", "action") == "closed"
|
|
94
|
+
when "IssueCommentEvent"
|
|
95
|
+
build(:commented, item.dig("payload", "issue"), repo, time) if item.dig("payload", "action") == "created"
|
|
96
|
+
when "PullRequestReviewCommentEvent"
|
|
97
|
+
if item.dig("payload", "action") == "created"
|
|
98
|
+
build(:commented, item.dig("payload", "pull_request"), repo, time)
|
|
99
|
+
end
|
|
100
|
+
when "WatchEvent"
|
|
101
|
+
Event.new(source: :github, kind: :starred, repo: repo, timestamp: time) if item.dig("payload", "action") == "started"
|
|
78
102
|
end
|
|
79
103
|
end
|
|
80
104
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
when "opened" then build(:pr_opened, pull, repo, time)
|
|
87
|
-
when "closed" then build(:pr_merged, pull, repo, time) if pull["merged"]
|
|
88
|
-
end
|
|
105
|
+
def dedupe_commented(events)
|
|
106
|
+
commented, other = events.partition { |event| event.kind == :commented }
|
|
107
|
+
deduped = commented.group_by { |event| [ event.repo, event.ref ] }
|
|
108
|
+
.map { |_key, group| group.max_by(&:timestamp) }
|
|
109
|
+
other + deduped
|
|
89
110
|
end
|
|
90
111
|
|
|
91
112
|
def build(kind, subject, repo, time)
|
|
@@ -110,6 +131,45 @@ module Spill
|
|
|
110
131
|
nil
|
|
111
132
|
end
|
|
112
133
|
|
|
134
|
+
def merged_pr_events(login, window)
|
|
135
|
+
since = window.since.strftime("%Y-%m-%d")
|
|
136
|
+
query = "search/issues?q=is:pr+author:#{login}+merged:%3E=#{since}&per_page=50&advanced_search=true"
|
|
137
|
+
out, ok = @runner.call([ "api", query ])
|
|
138
|
+
return nil unless ok
|
|
139
|
+
|
|
140
|
+
JSON.parse(out).fetch("items", []).filter_map do |item|
|
|
141
|
+
merged_at = item.dig("pull_request", "merged_at") || item["closed_at"]
|
|
142
|
+
next if merged_at.nil?
|
|
143
|
+
|
|
144
|
+
time = Time.parse(merged_at).localtime
|
|
145
|
+
next if time < window.since
|
|
146
|
+
|
|
147
|
+
Event.new(source: :github, kind: :pr_merged,
|
|
148
|
+
repo: item["repository_url"].to_s.split("/repos/").last,
|
|
149
|
+
title: item["title"], ref: "##{item["number"]}", timestamp: time)
|
|
150
|
+
end
|
|
151
|
+
rescue JSON::ParserError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def opened_pr_events(login, window)
|
|
156
|
+
since = window.since.strftime("%Y-%m-%d")
|
|
157
|
+
query = "search/issues?q=is:pr+author:#{login}+created:%3E=#{since}&per_page=50&advanced_search=true"
|
|
158
|
+
out, ok = @runner.call([ "api", query ])
|
|
159
|
+
return nil unless ok
|
|
160
|
+
|
|
161
|
+
JSON.parse(out).fetch("items", []).filter_map do |item|
|
|
162
|
+
time = Time.parse(item["created_at"]).localtime
|
|
163
|
+
next if time < window.since
|
|
164
|
+
|
|
165
|
+
Event.new(source: :github, kind: :pr_opened,
|
|
166
|
+
repo: item["repository_url"].to_s.split("/repos/").last,
|
|
167
|
+
title: item["title"], ref: "##{item["number"]}", timestamp: time)
|
|
168
|
+
end
|
|
169
|
+
rescue JSON::ParserError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
113
173
|
def truncated?(raw, window)
|
|
114
174
|
return false if raw.size < MAX_PAGES * PAGE_SIZE
|
|
115
175
|
|
data/lib/spill/renderer.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Spill
|
|
|
11
11
|
lines.concat(doing_lines(report, color))
|
|
12
12
|
lines.concat(quiet_lines(report, color))
|
|
13
13
|
end
|
|
14
|
+
lines.concat(explored_lines(report, color))
|
|
14
15
|
report.notes.each { |note| lines << "" << style(note, :dim, color) }
|
|
15
16
|
lines.join("\n") << "\n"
|
|
16
17
|
end
|
|
@@ -52,11 +53,17 @@ module Spill
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def github_line(event)
|
|
55
|
-
verb = { pr_merged: "merged PR", pr_opened: "opened PR",
|
|
56
|
-
|
|
56
|
+
verb = { pr_merged: "merged PR", pr_opened: "opened PR", review: "reviewed PR",
|
|
57
|
+
issue_closed: "closed issue", commented: "commented on" }.fetch(event.kind)
|
|
57
58
|
"#{verb} #{event.ref} (#{event.repo})#{title_suffix(event.title)}"
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
def explored_lines(report, color)
|
|
62
|
+
return [] if report.explored.empty?
|
|
63
|
+
|
|
64
|
+
[ "", style("Explored: #{report.explored.join(", ")}", :dim, color) ]
|
|
65
|
+
end
|
|
66
|
+
|
|
60
67
|
def title_suffix(title)
|
|
61
68
|
title.to_s.empty? ? "" : " — #{title}"
|
|
62
69
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
module Spill
|
|
5
|
+
module RepoRemotes
|
|
6
|
+
SSH_PATTERN = %r{\Agit@github\.com:(?<slug>.+?)(?:\.git)?/?\z}
|
|
7
|
+
SSH_PROTOCOL_PATTERN = %r{\Assh://git@github\.com/(?<slug>.+?)(?:\.git)?/?\z}
|
|
8
|
+
HTTPS_PATTERN = %r{\Ahttps://github\.com/(?<slug>.+?)(?:\.git)?/?\z}
|
|
9
|
+
|
|
10
|
+
def self.github_slugs(repo_paths)
|
|
11
|
+
repo_paths.filter_map { |path| slug_for(path) }.to_set
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.slug_for(path)
|
|
15
|
+
url = origin_url(path)
|
|
16
|
+
return nil if url.nil?
|
|
17
|
+
|
|
18
|
+
match = SSH_PATTERN.match(url) || SSH_PROTOCOL_PATTERN.match(url) || HTTPS_PATTERN.match(url)
|
|
19
|
+
match && match[:slug].downcase
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.origin_url(path)
|
|
23
|
+
out, _err, status = Open3.capture3("git", "-C", path, "remote", "get-url", "origin")
|
|
24
|
+
status.success? ? out.strip : nil
|
|
25
|
+
rescue Errno::ENOENT
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/spill/report.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Spill
|
|
2
|
-
Report = Data.define(:window, :done, :github_done, :doing, :quiet, :notes) do
|
|
3
|
-
GITHUB_DONE_KINDS = %i[ pr_opened pr_merged review issue_closed ].freeze
|
|
2
|
+
Report = Data.define(:window, :done, :github_done, :doing, :quiet, :explored, :notes) do
|
|
3
|
+
GITHUB_DONE_KINDS = %i[ pr_opened pr_merged review issue_closed commented ].freeze
|
|
4
4
|
LOCAL_STATE_KINDS = %i[ branch_wip dirty_tree ].freeze
|
|
5
5
|
|
|
6
6
|
def self.build(local:, github:, repos:, window:)
|
|
@@ -15,6 +15,7 @@ module Spill
|
|
|
15
15
|
github_done: github_events.select { |e| GITHUB_DONE_KINDS.include?(e.kind) }.sort_by(&:timestamp),
|
|
16
16
|
doing: build_doing(state, github_events),
|
|
17
17
|
quiet: repos - (commits.map(&:repo) + state.map(&:repo)).uniq,
|
|
18
|
+
explored: build_explored(github_events),
|
|
18
19
|
notes: build_notes(github, github_events)
|
|
19
20
|
)
|
|
20
21
|
end
|
|
@@ -34,6 +35,15 @@ module Spill
|
|
|
34
35
|
state.sort_by { |e| [ e.repo, e.kind.to_s ] } + open_prs
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def self.build_explored(github_events)
|
|
39
|
+
github_events.select { |e| e.kind == :starred }
|
|
40
|
+
.group_by(&:repo)
|
|
41
|
+
.map { |repo, events| [ repo, events.map(&:timestamp).max ] }
|
|
42
|
+
.sort_by { |_repo, time| time }
|
|
43
|
+
.reverse
|
|
44
|
+
.map(&:first)
|
|
45
|
+
end
|
|
46
|
+
|
|
37
47
|
def self.build_notes(github, github_events)
|
|
38
48
|
notes = []
|
|
39
49
|
notes << "GitHub: skipped (gh not available)" if github.nil?
|
data/lib/spill/version.rb
CHANGED
data/lib/spill.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative "spill/version"
|
|
|
2
2
|
require_relative "spill/event"
|
|
3
3
|
require_relative "spill/window"
|
|
4
4
|
require_relative "spill/repo_finder"
|
|
5
|
+
require_relative "spill/repo_remotes"
|
|
5
6
|
require_relative "spill/collectors/local_git"
|
|
6
7
|
require_relative "spill/collectors/github"
|
|
7
8
|
require_relative "spill/report"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spill
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tugcem Yalcin
|
|
@@ -30,6 +30,7 @@ files:
|
|
|
30
30
|
- lib/spill/event.rb
|
|
31
31
|
- lib/spill/renderer.rb
|
|
32
32
|
- lib/spill/repo_finder.rb
|
|
33
|
+
- lib/spill/repo_remotes.rb
|
|
33
34
|
- lib/spill/report.rb
|
|
34
35
|
- lib/spill/version.rb
|
|
35
36
|
- lib/spill/window.rb
|