gh-summary 0.1.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/LICENSE +21 -0
- data/README.md +101 -0
- data/bin/gh-summary +12 -0
- data/lib/gh_summary/auth_error.rb +3 -0
- data/lib/gh_summary/cli.rb +79 -0
- data/lib/gh_summary/colors.rb +12 -0
- data/lib/gh_summary/github.rb +46 -0
- data/lib/gh_summary/runner.rb +89 -0
- data/lib/gh_summary/version.rb +3 -0
- data/lib/gh_summary.rb +6 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 228ab8d8992d696d9e505a82f4ea178824652fffd40ec56c860f34d3ef2e5853
|
|
4
|
+
data.tar.gz: 5f8fac59f24a7a5e725d83fdb894684ed658bb454ddf02c874bc86705a06d57c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6a4208d706a7626fc6e74149bab22b0e0b277f49bce7df6e6b9b1ecd68179769d3761254635bd239c5c1a465088682a44f9bdbc24eaaca6940a6d88b0ae6ef64
|
|
7
|
+
data.tar.gz: 133ba481345bedb37913f1b27c0ef0e95baa66b42fa3bb147cdc55de9904ef29a405362ca595ff364b5196819ec5dcce248c843903f821032f919aa839236803
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nikita Miloserdov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# gh-summary
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/gh-summary)
|
|
4
|
+
|
|
5
|
+
Your GitHub day at a glance — PRs, reviews, issues, notifications — all in one terminal command.
|
|
6
|
+
|
|
7
|
+
Zero dependencies. Ruby + `gh` CLI.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
gem install gh-summary
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Ruby >= 3.0
|
|
18
|
+
- [gh](https://cli.github.com/), authenticated
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
gh-summary
|
|
24
|
+
gh-summary --short
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`--short` skips the recent activity section.
|
|
28
|
+
|
|
29
|
+
## Output
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
GitHub Summary 2026-04-13 14:54
|
|
33
|
+
────────────────────────────────────────────────────────────
|
|
34
|
+
PRs awaiting your review
|
|
35
|
+
torvalds/linux — Fix memory leak in page allocator
|
|
36
|
+
https://github.com/torvalds/linux/pull/842
|
|
37
|
+
rails/rails — ActiveRecord: add async batch find
|
|
38
|
+
https://github.com/rails/rails/pull/51203
|
|
39
|
+
(2 pending)
|
|
40
|
+
────────────────────────────────────────────────────────────
|
|
41
|
+
Your open PRs
|
|
42
|
+
matz/ruby — Optimize frozen string dedup in parser
|
|
43
|
+
https://github.com/matz/ruby/pull/9104
|
|
44
|
+
sinatra/sinatra — Add streaming response helpers
|
|
45
|
+
https://github.com/sinatra/sinatra/pull/1877
|
|
46
|
+
rack/rack — Fix multipart boundary parsing edge case
|
|
47
|
+
https://github.com/rack/rack/pull/2190
|
|
48
|
+
(3 open)
|
|
49
|
+
────────────────────────────────────────────────────────────
|
|
50
|
+
Issues assigned to you
|
|
51
|
+
homebrew/brew — Formula audit fails on Apple Silicon
|
|
52
|
+
https://github.com/homebrew/brew/issues/17450
|
|
53
|
+
(1 open)
|
|
54
|
+
────────────────────────────────────────────────────────────
|
|
55
|
+
Unread notifications
|
|
56
|
+
rails/rails (3):
|
|
57
|
+
[Issue] ActiveStorage mirror sync regression (mention)
|
|
58
|
+
[PullRequest] Fix connection pool exhaustion (review_requested)
|
|
59
|
+
[Release] v8.1.0 (subscribed)
|
|
60
|
+
ruby/ruby (1):
|
|
61
|
+
[Issue] YJIT segfault on ARM64 (assign)
|
|
62
|
+
(4 total unread)
|
|
63
|
+
────────────────────────────────────────────────────────────
|
|
64
|
+
Recent activity (your repos)
|
|
65
|
+
Repo Type Action Date
|
|
66
|
+
matz/ruby PullRequest opened 2026-04-12
|
|
67
|
+
sinatra/sinatra Issues closed 2026-04-11
|
|
68
|
+
rack/rack PullRequest merged 2026-04-10
|
|
69
|
+
────────────────────────────────────────────────────────────
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
make test
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Structure
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
.
|
|
82
|
+
├── bin
|
|
83
|
+
│ └── gh-summary
|
|
84
|
+
├── lib
|
|
85
|
+
│ ├── gh_summary
|
|
86
|
+
│ │ ├── auth_error.rb
|
|
87
|
+
│ │ ├── cli.rb
|
|
88
|
+
│ │ ├── colors.rb
|
|
89
|
+
│ │ ├── github.rb
|
|
90
|
+
│ │ └── runner.rb
|
|
91
|
+
│ └── gh_summary.rb
|
|
92
|
+
├── Makefile
|
|
93
|
+
├── README.md
|
|
94
|
+
└── test
|
|
95
|
+
├── test_auth_error.rb
|
|
96
|
+
├── test_cli.rb
|
|
97
|
+
├── test_colors.rb
|
|
98
|
+
├── test_github.rb
|
|
99
|
+
├── test_helper.rb
|
|
100
|
+
└── test_runner.rb
|
|
101
|
+
```
|
data/bin/gh-summary
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
|
|
5
|
+
require "gh_summary"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
GhSummary::Runner.new(short: ARGV.include?("--short")).run
|
|
9
|
+
rescue GhSummary::AuthError => error
|
|
10
|
+
GhSummary::CLI.new.error("Error: #{error.message}")
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module GhSummary
|
|
2
|
+
class CLI
|
|
3
|
+
include Colors
|
|
4
|
+
|
|
5
|
+
INDENT_SECTION = 2
|
|
6
|
+
INDENT_ITEM = 4
|
|
7
|
+
INDENT_SUB = 6
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def divider
|
|
13
|
+
output "#{DIM}#{"─" * 60}#{RESET}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def header(color, text)
|
|
17
|
+
divider
|
|
18
|
+
output "#{BOLD}#{color}#{left_padding(text, INDENT_SECTION)}#{RESET}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def item(repository, title, url)
|
|
22
|
+
output left_padding("#{repository} — #{title}", INDENT_ITEM)
|
|
23
|
+
output left_padding(url, INDENT_SUB)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sub_item(text)
|
|
27
|
+
output left_padding(text, INDENT_SUB)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def group(label)
|
|
31
|
+
output left_padding(label, INDENT_ITEM)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def count(number, label)
|
|
35
|
+
output "#{DIM}#{left_padding("(#{number} #{label})", INDENT_SECTION)}#{RESET}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def empty(text)
|
|
39
|
+
output "#{DIM}#{left_padding(text, INDENT_SECTION)}#{RESET}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def success(text)
|
|
43
|
+
output "#{GREEN}#{left_padding(text, INDENT_SECTION)}#{RESET}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def error(text)
|
|
47
|
+
$stderr.puts "#{RED}#{left_padding(text, INDENT_SECTION)}#{RESET}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def banner
|
|
51
|
+
output "\n#{BOLD}#{CYAN}#{left_padding("GitHub Summary", INDENT_SECTION)}#{RESET} #{DIM}#{Time.now.strftime("%Y-%m-%d %H:%M")}#{RESET}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def table(columns, rows)
|
|
55
|
+
column_widths = columns.map(&:length)
|
|
56
|
+
rows.each do |row|
|
|
57
|
+
row.each_with_index { |cell, index| column_widths[index] = [column_widths[index] || 0, cell.length].max }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
format_string = column_widths.map { |width| "%-#{width}s" }.join(" ")
|
|
61
|
+
output "#{DIM}#{left_padding(sprintf(format_string, *columns), INDENT_SECTION)}#{RESET}"
|
|
62
|
+
rows.each { |row| output left_padding(sprintf(format_string, *row), INDENT_SECTION) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def newline
|
|
66
|
+
output
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def output(text = "")
|
|
72
|
+
puts text
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def left_padding(text, spacing = INDENT_SECTION)
|
|
76
|
+
"#{" " * spacing}#{text}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
module GhSummary
|
|
5
|
+
class GitHub
|
|
6
|
+
def initialize
|
|
7
|
+
unless system("gh", "auth", "status", out: File::NULL, err: File::NULL)
|
|
8
|
+
raise AuthError, "gh is not authenticated. Run 'gh auth login' first."
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def search_prs(**options)
|
|
13
|
+
flags = options.map { |key, value| "--#{key}=#{value}" }
|
|
14
|
+
fetch_json("search", "prs", *flags, "--json", "title,repository,url,updatedAt", "--limit", "15")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def search_issues(**options)
|
|
18
|
+
flags = options.map { |key, value| "--#{key}=#{value}" }
|
|
19
|
+
fetch_json("search", "issues", *flags, "--json", "title,repository,url", "--limit", "15")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def notifications
|
|
23
|
+
fetch_json("api", "notifications")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def events(per_page: 20)
|
|
27
|
+
fetch_json("api", "events", "-f", "per_page=#{per_page}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def execute(*arguments)
|
|
33
|
+
output, _stderr, status = Open3.capture3("gh", *arguments)
|
|
34
|
+
status.success? ? output : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fetch_json(*arguments)
|
|
38
|
+
output = execute(*arguments)
|
|
39
|
+
return [] unless output
|
|
40
|
+
parsed = JSON.parse(output)
|
|
41
|
+
parsed.is_a?(Array) ? parsed : [parsed]
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
[]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module GhSummary
|
|
2
|
+
class Runner
|
|
3
|
+
include Colors
|
|
4
|
+
|
|
5
|
+
RELEVANT_EVENTS = %w[IssuesEvent PullRequestEvent ReleaseEvent ForkEvent WatchEvent].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(short: false)
|
|
8
|
+
@short_mode = short
|
|
9
|
+
@cli = CLI.new
|
|
10
|
+
@github = GitHub.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
@cli.banner
|
|
15
|
+
show_section(
|
|
16
|
+
YELLOW, "PRs awaiting your review",
|
|
17
|
+
@github.search_prs("review-requested": "@me", state: "open"),
|
|
18
|
+
empty: "None — you're clear!", empty_style: :success, count_label: "pending"
|
|
19
|
+
)
|
|
20
|
+
show_section(
|
|
21
|
+
GREEN, "Your open PRs",
|
|
22
|
+
@github.search_prs(author: "@me", state: "open"),
|
|
23
|
+
empty: "No open PRs", count_label: "open"
|
|
24
|
+
)
|
|
25
|
+
show_section(
|
|
26
|
+
RED, "Issues assigned to you",
|
|
27
|
+
@github.search_issues(assignee: "@me", state: "open"),
|
|
28
|
+
empty: "No assigned issues", count_label: "open"
|
|
29
|
+
)
|
|
30
|
+
show_notifications
|
|
31
|
+
show_recent_activity unless @short_mode
|
|
32
|
+
@cli.divider
|
|
33
|
+
@cli.newline
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def repository_name(item)
|
|
39
|
+
item.dig("repository", "nameWithOwner") || item.dig("repository", "name") || "?"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_section(color, title, items, empty:, count_label:, empty_style: :empty)
|
|
43
|
+
@cli.header(color, title)
|
|
44
|
+
if items.any?
|
|
45
|
+
items.each { |item| @cli.item(repository_name(item), item["title"], item["url"]) }
|
|
46
|
+
@cli.count(items.size, count_label)
|
|
47
|
+
else
|
|
48
|
+
@cli.send(empty_style, empty)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def show_notifications
|
|
53
|
+
@cli.header(MAGENTA, "Unread notifications")
|
|
54
|
+
notifications = @github.notifications
|
|
55
|
+
if notifications.any?
|
|
56
|
+
grouped_notifications = notifications.group_by { |notification| notification.dig("repository", "full_name") || "unknown" }
|
|
57
|
+
grouped_notifications.each do |repository, notification_items|
|
|
58
|
+
@cli.group("#{repository} (#{notification_items.size}):")
|
|
59
|
+
notification_items.first(5).each do |notification|
|
|
60
|
+
subject_type = notification.dig("subject", "type") || "?"
|
|
61
|
+
subject_title = notification.dig("subject", "title") || "?"
|
|
62
|
+
reason = notification["reason"] || "?"
|
|
63
|
+
@cli.sub_item("[#{subject_type}] #{subject_title} (#{reason})")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
@cli.count(notifications.size, "total unread")
|
|
67
|
+
else
|
|
68
|
+
@cli.success("All caught up!")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def show_recent_activity
|
|
73
|
+
@cli.header(CYAN, "Recent activity (your repos)")
|
|
74
|
+
all_events = @github.events(per_page: 20)
|
|
75
|
+
relevant_events = all_events.select { |event| RELEVANT_EVENTS.include?(event["type"]) }.first(10)
|
|
76
|
+
if relevant_events.any?
|
|
77
|
+
rows = relevant_events.map do |event|
|
|
78
|
+
[event.dig("repo", "name") || "?",
|
|
79
|
+
(event["type"] || "").sub("Event", ""),
|
|
80
|
+
event.dig("payload", "action") || "—",
|
|
81
|
+
(event["created_at"] || "").split("T").first]
|
|
82
|
+
end
|
|
83
|
+
@cli.table(%w[Repo Type Action Date], rows)
|
|
84
|
+
else
|
|
85
|
+
@cli.empty("No recent events")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/gh_summary.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gh-summary
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nikita Miloserdov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Shows open PRs, review requests, assigned issues, unread notifications,
|
|
13
|
+
and recent activity. Wraps the gh CLI. No dependencies.
|
|
14
|
+
email:
|
|
15
|
+
- nmiloserdov@proton.me
|
|
16
|
+
executables:
|
|
17
|
+
- gh-summary
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- bin/gh-summary
|
|
24
|
+
- lib/gh_summary.rb
|
|
25
|
+
- lib/gh_summary/auth_error.rb
|
|
26
|
+
- lib/gh_summary/cli.rb
|
|
27
|
+
- lib/gh_summary/colors.rb
|
|
28
|
+
- lib/gh_summary/github.rb
|
|
29
|
+
- lib/gh_summary/runner.rb
|
|
30
|
+
- lib/gh_summary/version.rb
|
|
31
|
+
homepage: https://github.com/wh1le/gh-summary
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://github.com/wh1le/gh-summary
|
|
36
|
+
source_code_uri: https://github.com/wh1le/gh-summary
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '3.0'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.7.2
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: GitHub activity digest in your terminal
|
|
54
|
+
test_files: []
|