gitfollow 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/CHANGELOG.md +55 -0
- data/LICENSE +21 -0
- data/README.md +437 -0
- data/bin/gitfollow +15 -0
- data/lib/gitfollow/cli.rb +298 -0
- data/lib/gitfollow/client.rb +88 -0
- data/lib/gitfollow/notifier.rb +191 -0
- data/lib/gitfollow/storage.rb +186 -0
- data/lib/gitfollow/tracker.rb +237 -0
- data/lib/gitfollow/version.rb +5 -0
- data/lib/gitfollow.rb +12 -0
- metadata +255 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module GitFollow
|
|
7
|
+
class Storage
|
|
8
|
+
DEFAULT_DATA_DIR = File.join(Dir.home, '.gitfollow')
|
|
9
|
+
SNAPSHOTS_FILE = 'snapshots.json'
|
|
10
|
+
HISTORY_FILE = 'history.json'
|
|
11
|
+
|
|
12
|
+
attr_reader :data_dir
|
|
13
|
+
|
|
14
|
+
def initialize(data_dir: DEFAULT_DATA_DIR)
|
|
15
|
+
@data_dir = data_dir
|
|
16
|
+
ensure_data_dir_exists
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def save_snapshot(username:, followers:, following:)
|
|
20
|
+
snapshots = load_snapshots
|
|
21
|
+
|
|
22
|
+
snapshot = {
|
|
23
|
+
'username' => username,
|
|
24
|
+
'timestamp' => Time.now.utc.iso8601,
|
|
25
|
+
'followers' => deep_stringify(followers),
|
|
26
|
+
'following' => deep_stringify(following),
|
|
27
|
+
'stats' => {
|
|
28
|
+
'followers_count' => followers.size,
|
|
29
|
+
'following_count' => following.size,
|
|
30
|
+
'mutual_count' => calculate_mutual(followers, following).size
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
snapshots[username] ||= []
|
|
35
|
+
snapshots[username] << snapshot
|
|
36
|
+
|
|
37
|
+
write_json(snapshots_file_path, snapshots)
|
|
38
|
+
snapshot
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def latest_snapshot(username)
|
|
42
|
+
snapshots = load_snapshots
|
|
43
|
+
snapshots[username]&.last
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def all_snapshots(username)
|
|
47
|
+
snapshots = load_snapshots
|
|
48
|
+
snapshots[username] || []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save_history_entry(username:, event_type:, user_data:)
|
|
52
|
+
history = load_history
|
|
53
|
+
|
|
54
|
+
entry = {
|
|
55
|
+
'username' => username,
|
|
56
|
+
'event_type' => event_type.to_s,
|
|
57
|
+
'user' => deep_stringify(user_data),
|
|
58
|
+
'timestamp' => Time.now.utc.iso8601
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
history[username] ||= []
|
|
62
|
+
history[username] << entry
|
|
63
|
+
|
|
64
|
+
write_json(history_file_path, history)
|
|
65
|
+
entry
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def get_history(username, limit: nil)
|
|
69
|
+
history = load_history
|
|
70
|
+
entries = history[username] || []
|
|
71
|
+
limit ? entries.last(limit) : entries
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear_data(username)
|
|
75
|
+
snapshots = load_snapshots
|
|
76
|
+
history = load_history
|
|
77
|
+
|
|
78
|
+
snapshots.delete(username)
|
|
79
|
+
history.delete(username)
|
|
80
|
+
|
|
81
|
+
write_json(snapshots_file_path, snapshots)
|
|
82
|
+
write_json(history_file_path, history)
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def data_exists?(username)
|
|
87
|
+
snapshots = load_snapshots
|
|
88
|
+
!snapshots[username].nil? && !snapshots[username].empty?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def export(username:, format:, output_file:)
|
|
92
|
+
case format
|
|
93
|
+
when :json
|
|
94
|
+
export_json(username, output_file)
|
|
95
|
+
when :csv
|
|
96
|
+
export_csv(username, output_file)
|
|
97
|
+
else
|
|
98
|
+
raise ArgumentError, "Unsupported export format: #{format}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Convert all symbol keys to strings recursively
|
|
105
|
+
def deep_stringify(obj)
|
|
106
|
+
case obj
|
|
107
|
+
when Hash
|
|
108
|
+
obj.each_with_object({}) do |(key, value), hash|
|
|
109
|
+
hash[key.to_s] = deep_stringify(value)
|
|
110
|
+
end
|
|
111
|
+
when Array
|
|
112
|
+
obj.map { |item| deep_stringify(item) }
|
|
113
|
+
else
|
|
114
|
+
obj
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def calculate_mutual(followers, following)
|
|
119
|
+
follower_logins = followers.map { |f| f['login'] || f[:login] }
|
|
120
|
+
following.select { |f| follower_logins.include?(f['login'] || f[:login]) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ensure_data_dir_exists
|
|
124
|
+
FileUtils.mkdir_p(@data_dir)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def snapshots_file_path
|
|
128
|
+
File.join(@data_dir, SNAPSHOTS_FILE)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def history_file_path
|
|
132
|
+
File.join(@data_dir, HISTORY_FILE)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def load_snapshots
|
|
136
|
+
load_json(snapshots_file_path)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def load_history
|
|
140
|
+
load_json(history_file_path)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def load_json(file_path)
|
|
144
|
+
return {} unless File.exist?(file_path)
|
|
145
|
+
|
|
146
|
+
JSON.parse(File.read(file_path))
|
|
147
|
+
rescue JSON::ParserError
|
|
148
|
+
{}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def write_json(file_path, data)
|
|
152
|
+
File.write(file_path, JSON.pretty_generate(data))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def export_json(username, output_file)
|
|
156
|
+
data = {
|
|
157
|
+
'username' => username,
|
|
158
|
+
'snapshots' => all_snapshots(username),
|
|
159
|
+
'history' => get_history(username)
|
|
160
|
+
}
|
|
161
|
+
write_json(output_file, data)
|
|
162
|
+
output_file
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def export_csv(username, output_file)
|
|
166
|
+
require 'csv'
|
|
167
|
+
|
|
168
|
+
history = get_history(username)
|
|
169
|
+
|
|
170
|
+
CSV.open(output_file, 'w') do |csv|
|
|
171
|
+
csv << ['Timestamp', 'Event Type', 'Username', 'User ID']
|
|
172
|
+
|
|
173
|
+
history.each do |entry|
|
|
174
|
+
csv << [
|
|
175
|
+
entry['timestamp'],
|
|
176
|
+
entry['event_type'],
|
|
177
|
+
entry['user']['login'],
|
|
178
|
+
entry['user']['id']
|
|
179
|
+
]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
output_file
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitFollow
|
|
4
|
+
class Tracker
|
|
5
|
+
attr_reader :client, :storage, :username
|
|
6
|
+
|
|
7
|
+
def initialize(client:, storage:)
|
|
8
|
+
@client = client
|
|
9
|
+
@storage = storage
|
|
10
|
+
@username = client.username
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initial_setup
|
|
14
|
+
followers = @client.fetch_followers
|
|
15
|
+
following = @client.fetch_following
|
|
16
|
+
|
|
17
|
+
@storage.save_snapshot(
|
|
18
|
+
username: @username,
|
|
19
|
+
followers: followers,
|
|
20
|
+
following: following
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_changes
|
|
25
|
+
current_followers = @client.fetch_followers
|
|
26
|
+
current_following = @client.fetch_following
|
|
27
|
+
|
|
28
|
+
previous_snapshot = @storage.latest_snapshot(@username)
|
|
29
|
+
|
|
30
|
+
if previous_snapshot.nil?
|
|
31
|
+
return {
|
|
32
|
+
first_run: true,
|
|
33
|
+
message: 'No previous data found. Initializing...'
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
changes = detect_changes(previous_snapshot, current_followers, current_following)
|
|
38
|
+
|
|
39
|
+
# Save new snapshot
|
|
40
|
+
@storage.save_snapshot(
|
|
41
|
+
username: @username,
|
|
42
|
+
followers: current_followers,
|
|
43
|
+
following: current_following
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
save_history_entries(changes)
|
|
47
|
+
|
|
48
|
+
changes
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def statistics
|
|
52
|
+
latest = @storage.latest_snapshot(@username)
|
|
53
|
+
|
|
54
|
+
return nil if latest.nil?
|
|
55
|
+
|
|
56
|
+
followers = latest['followers']
|
|
57
|
+
following = latest['following']
|
|
58
|
+
mutual = calculate_mutual(followers, following)
|
|
59
|
+
|
|
60
|
+
history = @storage.get_history(@username)
|
|
61
|
+
new_followers_count = history.count { |e| e['event_type'] == 'new_follower' }
|
|
62
|
+
unfollows_count = history.count { |e| e['event_type'] == 'unfollowed' }
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
followers_count: followers.size,
|
|
66
|
+
following_count: following.size,
|
|
67
|
+
mutual_count: mutual.size,
|
|
68
|
+
ratio: calculate_ratio(followers.size, following.size),
|
|
69
|
+
total_new_followers: new_followers_count,
|
|
70
|
+
total_unfollows: unfollows_count,
|
|
71
|
+
last_updated: latest['timestamp']
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate_report(format: :text)
|
|
76
|
+
stats = statistics
|
|
77
|
+
|
|
78
|
+
return 'No data available. Run `gitfollow check` first.' if stats.nil?
|
|
79
|
+
|
|
80
|
+
history = @storage.get_history(@username, limit: 20)
|
|
81
|
+
|
|
82
|
+
case format
|
|
83
|
+
when :markdown
|
|
84
|
+
generate_markdown_report(stats, history)
|
|
85
|
+
when :text
|
|
86
|
+
generate_text_report(stats, history)
|
|
87
|
+
else
|
|
88
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def mutual_followers
|
|
93
|
+
latest = @storage.latest_snapshot(@username)
|
|
94
|
+
return [] if latest.nil?
|
|
95
|
+
|
|
96
|
+
calculate_mutual(latest['followers'], latest['following'])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def non_followers
|
|
100
|
+
latest = @storage.latest_snapshot(@username)
|
|
101
|
+
return [] if latest.nil?
|
|
102
|
+
|
|
103
|
+
following = latest['following']
|
|
104
|
+
follower_logins = latest['followers'].map { |f| f['login'] || f[:login] }
|
|
105
|
+
|
|
106
|
+
following.reject { |f| follower_logins.include?(f['login'] || f[:login]) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def detect_changes(previous_snapshot, current_followers, _current_following)
|
|
112
|
+
previous_followers = previous_snapshot['followers']
|
|
113
|
+
|
|
114
|
+
# Extract IDs, handling both symbol and string keys
|
|
115
|
+
previous_follower_ids = previous_followers.map { |f| f['id'] || f[:id] }
|
|
116
|
+
current_follower_ids = current_followers.map { |f| f['id'] || f[:id] }
|
|
117
|
+
|
|
118
|
+
new_follower_ids = current_follower_ids - previous_follower_ids
|
|
119
|
+
unfollowed_ids = previous_follower_ids - current_follower_ids
|
|
120
|
+
|
|
121
|
+
# Find users by ID, handling both symbol and string keys
|
|
122
|
+
new_followers = current_followers.select { |f| new_follower_ids.include?(f['id'] || f[:id]) }
|
|
123
|
+
unfollowed = previous_followers.select { |f| unfollowed_ids.include?(f['id'] || f[:id]) }
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
new_followers: new_followers,
|
|
127
|
+
unfollowed: unfollowed,
|
|
128
|
+
has_changes: !new_followers.empty? || !unfollowed.empty?,
|
|
129
|
+
previous_count: previous_followers.size,
|
|
130
|
+
current_count: current_followers.size,
|
|
131
|
+
net_change: current_followers.size - previous_followers.size
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def save_history_entries(changes)
|
|
136
|
+
changes[:new_followers].each do |user|
|
|
137
|
+
@storage.save_history_entry(
|
|
138
|
+
username: @username,
|
|
139
|
+
event_type: :new_follower,
|
|
140
|
+
user_data: user
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
changes[:unfollowed].each do |user|
|
|
145
|
+
@storage.save_history_entry(
|
|
146
|
+
username: @username,
|
|
147
|
+
event_type: :unfollowed,
|
|
148
|
+
user_data: user
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def calculate_mutual(followers, following)
|
|
154
|
+
follower_logins = followers.map { |f| f['login'] || f[:login] }
|
|
155
|
+
following.select { |f| follower_logins.include?(f['login'] || f[:login]) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def calculate_ratio(followers_count, following_count)
|
|
159
|
+
return 0.0 if following_count.zero?
|
|
160
|
+
|
|
161
|
+
(followers_count.to_f / following_count).round(2)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def generate_markdown_report(stats, history)
|
|
165
|
+
report = []
|
|
166
|
+
report << "# GitFollow Report for @#{@username}"
|
|
167
|
+
report << ''
|
|
168
|
+
report << "**Last Updated:** #{Time.parse(stats[:last_updated]).strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
169
|
+
report << ''
|
|
170
|
+
report << '## Statistics'
|
|
171
|
+
report << ''
|
|
172
|
+
report << "- **Followers:** #{stats[:followers_count]}"
|
|
173
|
+
report << "- **Following:** #{stats[:following_count]}"
|
|
174
|
+
report << "- **Mutual:** #{stats[:mutual_count]}"
|
|
175
|
+
report << "- **Ratio:** #{stats[:ratio]}"
|
|
176
|
+
report << "- **Total New Followers:** #{stats[:total_new_followers]}"
|
|
177
|
+
report << "- **Total Unfollows:** #{stats[:total_unfollows]}"
|
|
178
|
+
report << ''
|
|
179
|
+
report << '## Recent Activity'
|
|
180
|
+
report << ''
|
|
181
|
+
|
|
182
|
+
if history.empty?
|
|
183
|
+
report << '_No recent activity_'
|
|
184
|
+
else
|
|
185
|
+
report << '| Timestamp | Event | User |'
|
|
186
|
+
report << '|-----------|-------|------|'
|
|
187
|
+
|
|
188
|
+
history.reverse.each do |entry|
|
|
189
|
+
timestamp = Time.parse(entry['timestamp']).strftime('%Y-%m-%d %H:%M')
|
|
190
|
+
event = entry['event_type'] == 'new_follower' ? '✅ New Follower' : '❌ Unfollowed'
|
|
191
|
+
user = "@#{entry['user']['login']}"
|
|
192
|
+
|
|
193
|
+
report << "| #{timestamp} | #{event} | #{user} |"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
report << ''
|
|
198
|
+
report << '---'
|
|
199
|
+
report << '_Generated with [GitFollow](https://github.com/bulletdev/gitfollow)_'
|
|
200
|
+
|
|
201
|
+
report.join("\n")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def generate_text_report(stats, history)
|
|
205
|
+
report = []
|
|
206
|
+
report << "GitFollow Report for @#{@username}"
|
|
207
|
+
report << ('=' * 50)
|
|
208
|
+
report << ''
|
|
209
|
+
report << "Last Updated: #{Time.parse(stats[:last_updated]).strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
210
|
+
report << ''
|
|
211
|
+
report << 'Statistics:'
|
|
212
|
+
report << " Followers: #{stats[:followers_count]}"
|
|
213
|
+
report << " Following: #{stats[:following_count]}"
|
|
214
|
+
report << " Mutual: #{stats[:mutual_count]}"
|
|
215
|
+
report << " Ratio: #{stats[:ratio]}"
|
|
216
|
+
report << " Total New: #{stats[:total_new_followers]}"
|
|
217
|
+
report << " Total Unfollows: #{stats[:total_unfollows]}"
|
|
218
|
+
report << ''
|
|
219
|
+
report << 'Recent Activity:'
|
|
220
|
+
report << ('-' * 50)
|
|
221
|
+
|
|
222
|
+
if history.empty?
|
|
223
|
+
report << ' No recent activity'
|
|
224
|
+
else
|
|
225
|
+
history.reverse.each do |entry|
|
|
226
|
+
timestamp = Time.parse(entry['timestamp']).strftime('%Y-%m-%d %H:%M')
|
|
227
|
+
event = entry['event_type'] == 'new_follower' ? '✅ NEW' : '❌ UNFOLLOW'
|
|
228
|
+
user = "@#{entry['user']['login']}"
|
|
229
|
+
|
|
230
|
+
report << " [#{timestamp}] #{event} #{user}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
report.join("\n")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
data/lib/gitfollow.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'gitfollow/version'
|
|
4
|
+
require_relative 'gitfollow/client'
|
|
5
|
+
require_relative 'gitfollow/storage'
|
|
6
|
+
require_relative 'gitfollow/tracker'
|
|
7
|
+
require_relative 'gitfollow/notifier'
|
|
8
|
+
require_relative 'gitfollow/cli'
|
|
9
|
+
|
|
10
|
+
# GitFollow - Track GitHub followers and unfollows
|
|
11
|
+
module GitFollow
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gitfollow
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael D. Bullet
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: colorize
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: csv
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: dotenv
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: octokit
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '8.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '8.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: thor
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.3'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.3'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: tty-spinner
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.9'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.9'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: tty-table
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.12'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.12'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rake
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '13.0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '13.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rspec
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '3.12'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '3.12'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rubocop
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '1.60'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '1.60'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: rubocop-rspec
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.26'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '2.26'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: simplecov
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - "~>"
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '0.22'
|
|
173
|
+
type: :development
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - "~>"
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0.22'
|
|
180
|
+
- !ruby/object:Gem::Dependency
|
|
181
|
+
name: vcr
|
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
|
183
|
+
requirements:
|
|
184
|
+
- - "~>"
|
|
185
|
+
- !ruby/object:Gem::Version
|
|
186
|
+
version: '6.2'
|
|
187
|
+
type: :development
|
|
188
|
+
prerelease: false
|
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - "~>"
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: '6.2'
|
|
194
|
+
- !ruby/object:Gem::Dependency
|
|
195
|
+
name: webmock
|
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - "~>"
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: '3.19'
|
|
201
|
+
type: :development
|
|
202
|
+
prerelease: false
|
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
204
|
+
requirements:
|
|
205
|
+
- - "~>"
|
|
206
|
+
- !ruby/object:Gem::Version
|
|
207
|
+
version: '3.19'
|
|
208
|
+
description: A CLI tool to monitor your GitHub followers, detect new followers and
|
|
209
|
+
unfollows, and generate detailed reports with automated notifications.
|
|
210
|
+
email:
|
|
211
|
+
- contato@michaelbullet.com
|
|
212
|
+
executables:
|
|
213
|
+
- gitfollow
|
|
214
|
+
extensions: []
|
|
215
|
+
extra_rdoc_files: []
|
|
216
|
+
files:
|
|
217
|
+
- CHANGELOG.md
|
|
218
|
+
- LICENSE
|
|
219
|
+
- README.md
|
|
220
|
+
- bin/gitfollow
|
|
221
|
+
- lib/gitfollow.rb
|
|
222
|
+
- lib/gitfollow/cli.rb
|
|
223
|
+
- lib/gitfollow/client.rb
|
|
224
|
+
- lib/gitfollow/notifier.rb
|
|
225
|
+
- lib/gitfollow/storage.rb
|
|
226
|
+
- lib/gitfollow/tracker.rb
|
|
227
|
+
- lib/gitfollow/version.rb
|
|
228
|
+
homepage: https://github.com/bulletdev/gitfollow
|
|
229
|
+
licenses:
|
|
230
|
+
- MIT
|
|
231
|
+
metadata:
|
|
232
|
+
homepage_uri: https://github.com/bulletdev/gitfollow
|
|
233
|
+
source_code_uri: https://github.com/bulletdev/gitfollow.git
|
|
234
|
+
changelog_uri: https://github.com/bulletdev/gitfollow/blob/main/CHANGELOG.md
|
|
235
|
+
bug_tracker_uri: https://github.com/bulletdev/gitfollow/issues
|
|
236
|
+
documentation_uri: https://github.com/bulletdev/gitfollow/blob/main/README.md
|
|
237
|
+
rubygems_mfa_required: 'true'
|
|
238
|
+
rdoc_options: []
|
|
239
|
+
require_paths:
|
|
240
|
+
- lib
|
|
241
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
242
|
+
requirements:
|
|
243
|
+
- - ">="
|
|
244
|
+
- !ruby/object:Gem::Version
|
|
245
|
+
version: 3.4.5
|
|
246
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
247
|
+
requirements:
|
|
248
|
+
- - ">="
|
|
249
|
+
- !ruby/object:Gem::Version
|
|
250
|
+
version: '0'
|
|
251
|
+
requirements: []
|
|
252
|
+
rubygems_version: 3.6.9
|
|
253
|
+
specification_version: 4
|
|
254
|
+
summary: Track GitHub followers and unfollows with ease
|
|
255
|
+
test_files: []
|