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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3fbd52dab7cb94bc936122dce281e3f08a854df2814ae8dd136cd7ceb05cc55
4
- data.tar.gz: d6b2c9fe0a68bae6a2295f84a82c60b16075f142632f3f4362e7a2ad1a8a0bec
3
+ metadata.gz: f7713f1ee707ff465eaba5f7fe683537dff7ba79331f9eebdf067dac2cad9f2e
4
+ data.tar.gz: a4e1c363ee31bcaff8a1c704f1a79457180e3bc0d1b65d4a6b8bac7bf1356a2c
5
5
  SHA512:
6
- metadata.gz: 7c7d6bfe3c753170d59d469f79ac94bfd8655a72fa41141d3808a73cae96ab5ed048c7af2ef7092a06910ae8520630d2d8df6c5065490d064bf254ed05064b16
7
- data.tar.gz: a9327e2dc50834fc63c7e9afbab1b05954347c98dfa2e96e4908c3c9e91d7d8e07d9509ca24fec7f84edc4e8ce6bb3458d424718d0a5ec8cb8ed3dd58eb21870
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] ? Collectors::Github.new.collect(window: window) : []
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 map_pull_request(item, repo, time)
82
- pull = item.dig("payload", "pull_request")
83
- return nil if pull.nil?
84
-
85
- case item.dig("payload", "action")
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
 
@@ -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
- review: "reviewed PR", issue_closed: "closed issue" }.fetch(event.kind)
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
@@ -1,3 +1,3 @@
1
1
  module Spill
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
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.0
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