git-pkgs 0.2.0 → 0.4.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.
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Pkgs
5
+ module Commands
6
+ class Schema
7
+ include Output
8
+
9
+ FORMATS = %w[text sql json markdown].freeze
10
+
11
+ def initialize(args)
12
+ @args = args
13
+ @options = parse_options
14
+ end
15
+
16
+ def run
17
+ repo = Repository.new
18
+ require_database(repo)
19
+
20
+ Database.connect(repo.git_dir)
21
+ tables = fetch_schema
22
+
23
+ case @options[:format]
24
+ when "sql"
25
+ output_sql(tables)
26
+ when "json"
27
+ output_json(tables)
28
+ when "markdown"
29
+ output_markdown(tables)
30
+ else
31
+ output_text(tables)
32
+ end
33
+ end
34
+
35
+ def fetch_schema
36
+ conn = ActiveRecord::Base.connection
37
+ tables = {}
38
+
39
+ conn.tables.sort.each do |table_name|
40
+ next if table_name == "ar_internal_metadata"
41
+ next if table_name == "schema_migrations"
42
+
43
+ columns = conn.columns(table_name).map do |col|
44
+ {
45
+ name: col.name,
46
+ type: col.type,
47
+ sql_type: col.sql_type,
48
+ null: col.null,
49
+ default: col.default
50
+ }
51
+ end
52
+
53
+ indexes = conn.indexes(table_name).map do |idx|
54
+ {
55
+ name: idx.name,
56
+ columns: idx.columns,
57
+ unique: idx.unique
58
+ }
59
+ end
60
+
61
+ tables[table_name] = { columns: columns, indexes: indexes }
62
+ end
63
+
64
+ tables
65
+ end
66
+
67
+ def output_text(tables)
68
+ tables.each do |table_name, info|
69
+ puts "#{table_name}"
70
+ puts "-" * table_name.length
71
+
72
+ info[:columns].each do |col|
73
+ nullable = col[:null] ? "NULL" : "NOT NULL"
74
+ default = col[:default] ? " DEFAULT #{col[:default]}" : ""
75
+ puts " #{col[:name].ljust(25)} #{col[:sql_type].ljust(15)} #{nullable}#{default}"
76
+ end
77
+
78
+ if info[:indexes].any?
79
+ puts
80
+ puts " Indexes:"
81
+ info[:indexes].each do |idx|
82
+ unique = idx[:unique] ? "UNIQUE " : ""
83
+ puts " #{unique}#{idx[:name]} (#{idx[:columns].join(', ')})"
84
+ end
85
+ end
86
+
87
+ puts
88
+ end
89
+ end
90
+
91
+ def output_sql(tables)
92
+ conn = ActiveRecord::Base.connection
93
+
94
+ tables.each do |table_name, info|
95
+ sql = conn.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='#{table_name}'").first
96
+ puts sql["sql"] + ";" if sql
97
+ puts
98
+
99
+ info[:indexes].each do |idx|
100
+ idx_sql = conn.execute("SELECT sql FROM sqlite_master WHERE type='index' AND name='#{idx[:name]}'").first
101
+ puts idx_sql["sql"] + ";" if idx_sql && idx_sql["sql"]
102
+ end
103
+
104
+ puts if info[:indexes].any?
105
+ end
106
+ end
107
+
108
+ def output_json(tables)
109
+ require "json"
110
+ puts JSON.pretty_generate(tables)
111
+ end
112
+
113
+ def output_markdown(tables)
114
+ tables.each do |table_name, info|
115
+ puts "## #{table_name}"
116
+ puts
117
+ puts "| Column | Type | Nullable | Default |"
118
+ puts "|--------|------|----------|---------|"
119
+
120
+ info[:columns].each do |col|
121
+ nullable = col[:null] ? "Yes" : "No"
122
+ default = col[:default] || ""
123
+ puts "| #{col[:name]} | #{col[:sql_type]} | #{nullable} | #{default} |"
124
+ end
125
+
126
+ if info[:indexes].any?
127
+ puts
128
+ puts "**Indexes:**"
129
+ info[:indexes].each do |idx|
130
+ unique = idx[:unique] ? " (unique)" : ""
131
+ puts "- `#{idx[:name]}`#{unique}: #{idx[:columns].join(', ')}"
132
+ end
133
+ end
134
+
135
+ puts
136
+ end
137
+ end
138
+
139
+ def parse_options
140
+ options = { format: "text" }
141
+
142
+ parser = OptionParser.new do |opts|
143
+ opts.banner = "Usage: git pkgs schema [options]"
144
+
145
+ opts.on("--format=FORMAT", FORMATS, "Output format: #{FORMATS.join(', ')} (default: text)") do |v|
146
+ options[:format] = v
147
+ end
148
+
149
+ opts.on("-h", "--help", "Show this help") do
150
+ puts opts
151
+ exit
152
+ end
153
+ end
154
+
155
+ parser.parse!(@args)
156
+ options
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Search
7
+ include Output
8
+
7
9
  def initialize(args)
8
10
  @args = args
9
11
  @options = parse_options
@@ -12,17 +14,10 @@ module Git
12
14
  def run
13
15
  pattern = @args.first
14
16
 
15
- unless pattern
16
- $stderr.puts "Usage: git pkgs search <pattern>"
17
- exit 1
18
- end
17
+ error "Usage: git pkgs search <pattern>" unless pattern
19
18
 
20
19
  repo = Repository.new
21
-
22
- unless Database.exists?(repo.git_dir)
23
- $stderr.puts "Database not initialized. Run 'git pkgs init' first."
24
- exit 1
25
- end
20
+ require_database(repo)
26
21
 
27
22
  Database.connect(repo.git_dir)
28
23
 
@@ -43,14 +38,14 @@ module Git
43
38
  matches = query.distinct.pluck(:name, :ecosystem)
44
39
 
45
40
  if matches.empty?
46
- puts "No dependencies found matching '#{pattern}'"
41
+ empty_result "No dependencies found matching '#{pattern}'"
47
42
  return
48
43
  end
49
44
 
50
45
  if @options[:format] == "json"
51
46
  output_json(matches, pattern)
52
47
  else
53
- output_text(matches, pattern)
48
+ paginate { output_text(matches, pattern) }
54
49
  end
55
50
  end
56
51
 
@@ -137,6 +132,10 @@ module Git
137
132
  options[:format] = v
138
133
  end
139
134
 
135
+ opts.on("--no-pager", "Do not pipe output into a pager") do
136
+ options[:no_pager] = true
137
+ end
138
+
140
139
  opts.on("-h", "--help", "Show this help") do
141
140
  puts opts
142
141
  exit
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Show
7
+ include Output
8
+
7
9
  def initialize(args)
8
10
  @args = args
9
11
  @options = parse_options
@@ -13,27 +15,15 @@ module Git
13
15
  ref = @args.shift || "HEAD"
14
16
 
15
17
  repo = Repository.new
16
-
17
- unless Database.exists?(repo.git_dir)
18
- $stderr.puts "Database not initialized. Run 'git pkgs init' first."
19
- exit 1
20
- end
18
+ require_database(repo)
21
19
 
22
20
  Database.connect(repo.git_dir)
23
21
 
24
22
  sha = repo.rev_parse(ref)
25
-
26
- unless sha
27
- $stderr.puts "Could not resolve '#{ref}'"
28
- exit 1
29
- end
23
+ error "Could not resolve '#{ref}'" unless sha
30
24
 
31
25
  commit = find_or_create_commit(repo, sha)
32
-
33
- unless commit
34
- $stderr.puts "Commit '#{sha[0..7]}' not found"
35
- exit 1
36
- end
26
+ error "Commit '#{sha[0..7]}' not found" unless commit
37
27
 
38
28
  changes = Models::DependencyChange
39
29
  .includes(:commit, :manifest)
@@ -44,14 +34,14 @@ module Git
44
34
  end
45
35
 
46
36
  if changes.empty?
47
- puts "No dependency changes in #{commit.short_sha}"
37
+ empty_result "No dependency changes in #{commit.short_sha}"
48
38
  return
49
39
  end
50
40
 
51
41
  if @options[:format] == "json"
52
42
  output_json(commit, changes)
53
43
  else
54
- output_text(commit, changes)
44
+ paginate { output_text(commit, changes) }
55
45
  end
56
46
  end
57
47
 
@@ -66,25 +56,25 @@ module Git
66
56
  removed = changes.select { |c| c.change_type == "removed" }
67
57
 
68
58
  if added.any?
69
- puts "Added:"
59
+ puts Color.green("Added:")
70
60
  added.each do |change|
71
- puts " #{change.name} #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})"
61
+ puts Color.green(" + #{change.name} #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})")
72
62
  end
73
63
  puts
74
64
  end
75
65
 
76
66
  if modified.any?
77
- puts "Modified:"
67
+ puts Color.yellow("Modified:")
78
68
  modified.each do |change|
79
- puts " #{change.name} #{change.previous_requirement} -> #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})"
69
+ puts Color.yellow(" ~ #{change.name} #{change.previous_requirement} -> #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})")
80
70
  end
81
71
  puts
82
72
  end
83
73
 
84
74
  if removed.any?
85
- puts "Removed:"
75
+ puts Color.red("Removed:")
86
76
  removed.each do |change|
87
- puts " #{change.name} #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})"
77
+ puts Color.red(" - #{change.name} #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})")
88
78
  end
89
79
  puts
90
80
  end
@@ -151,6 +141,10 @@ module Git
151
141
  options[:format] = v
152
142
  end
153
143
 
144
+ opts.on("--no-pager", "Do not pipe output into a pager") do
145
+ options[:no_pager] = true
146
+ end
147
+
154
148
  opts.on("-h", "--help", "Show this help") do
155
149
  puts opts
156
150
  exit
@@ -3,7 +3,9 @@
3
3
  module Git
4
4
  module Pkgs
5
5
  module Commands
6
- class Outdated
6
+ class Stale
7
+ include Output
8
+
7
9
  def initialize(args)
8
10
  @args = args
9
11
  @options = parse_options
@@ -11,21 +13,14 @@ module Git
11
13
 
12
14
  def run
13
15
  repo = Repository.new
14
-
15
- unless Database.exists?(repo.git_dir)
16
- $stderr.puts "Database not initialized. Run 'git pkgs init' first."
17
- exit 1
18
- end
16
+ require_database(repo)
19
17
 
20
18
  Database.connect(repo.git_dir)
21
19
 
22
20
  branch_name = @options[:branch] || repo.default_branch
23
21
  branch = Models::Branch.find_by(name: branch_name)
24
22
 
25
- unless branch&.last_analyzed_sha
26
- $stderr.puts "No analysis found for branch '#{branch_name}'"
27
- exit 1
28
- end
23
+ error "No analysis found for branch '#{branch_name}'" unless branch&.last_analyzed_sha
29
24
 
30
25
  current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
31
26
  snapshots = current_commit&.dependency_snapshots&.includes(:manifest) || []
@@ -35,7 +30,7 @@ module Git
35
30
  end
36
31
 
37
32
  if snapshots.empty?
38
- puts "No dependencies found"
33
+ empty_result "No dependencies found"
39
34
  return
40
35
  end
41
36
 
@@ -72,10 +67,14 @@ module Git
72
67
  end
73
68
 
74
69
  if outdated_data.empty?
75
- puts "All dependencies have been updated recently"
70
+ empty_result "All dependencies have been updated recently"
76
71
  return
77
72
  end
78
73
 
74
+ paginate { output_text(outdated_data) }
75
+ end
76
+
77
+ def output_text(outdated_data)
79
78
  puts "Dependencies by last update:"
80
79
  puts
81
80
 
@@ -93,7 +92,7 @@ module Git
93
92
  options = {}
94
93
 
95
94
  parser = OptionParser.new do |opts|
96
- opts.banner = "Usage: git pkgs outdated [options]"
95
+ opts.banner = "Usage: git pkgs stale [options]"
97
96
 
98
97
  opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
99
98
  options[:ecosystem] = v
@@ -107,6 +106,10 @@ module Git
107
106
  options[:days] = v
108
107
  end
109
108
 
109
+ opts.on("--no-pager", "Do not pipe output into a pager") do
110
+ options[:no_pager] = true
111
+ end
112
+
110
113
  opts.on("-h", "--help", "Show this help") do
111
114
  puts opts
112
115
  exit
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Git
4
6
  module Pkgs
5
7
  module Commands
6
8
  class Stats
9
+ include Output
10
+
7
11
  def initialize(args)
8
12
  @args = args
9
13
  @options = parse_options
@@ -11,11 +15,7 @@ module Git
11
15
 
12
16
  def run
13
17
  repo = Repository.new
14
-
15
- unless Database.exists?(repo.git_dir)
16
- $stderr.puts "Database not initialized. Run 'git pkgs init' first."
17
- exit 1
18
- end
18
+ require_database(repo)
19
19
 
20
20
  Database.connect(repo.git_dir)
21
21
 
@@ -31,19 +31,27 @@ module Git
31
31
  require "json"
32
32
  puts JSON.pretty_generate(data)
33
33
  else
34
- output_text(data)
34
+ paginate { output_text(data) }
35
35
  end
36
36
  end
37
37
  end
38
38
 
39
39
  def collect_stats(branch, branch_name)
40
40
  ecosystem = @options[:ecosystem]
41
+ since_time = @options[:since] ? parse_time(@options[:since]) : nil
42
+ until_time = @options[:until] ? parse_time(@options[:until]) : nil
43
+
44
+ commits = branch&.commits || Models::Commit.none
45
+ commits = commits.where("committed_at >= ?", since_time) if since_time
46
+ commits = commits.where("committed_at <= ?", until_time) if until_time
41
47
 
42
48
  data = {
43
49
  branch: branch_name,
44
50
  ecosystem: ecosystem,
45
- commits_analyzed: branch&.commits&.count || 0,
46
- commits_with_changes: branch&.commits&.where(has_dependency_changes: true)&.count || 0,
51
+ since: @options[:since],
52
+ until: @options[:until],
53
+ commits_analyzed: commits.count,
54
+ commits_with_changes: commits.where(has_dependency_changes: true).count,
47
55
  current_dependencies: {},
48
56
  changes: {},
49
57
  most_changed: [],
@@ -62,8 +70,10 @@ module Git
62
70
  }
63
71
  end
64
72
 
65
- changes = Models::DependencyChange.all
73
+ changes = Models::DependencyChange.joins(:commit)
66
74
  changes = changes.where(ecosystem: ecosystem) if ecosystem
75
+ changes = changes.where("commits.committed_at >= ?", since_time) if since_time
76
+ changes = changes.where("commits.committed_at <= ?", until_time) if until_time
67
77
 
68
78
  data[:changes] = {
69
79
  total: changes.count,
@@ -84,7 +94,10 @@ module Git
84
94
  manifests = manifests.where(ecosystem: ecosystem) if ecosystem
85
95
 
86
96
  data[:manifests] = manifests.map do |manifest|
87
- { path: manifest.path, ecosystem: manifest.ecosystem, changes: manifest.dependency_changes.count }
97
+ manifest_changes = manifest.dependency_changes.joins(:commit)
98
+ manifest_changes = manifest_changes.where("commits.committed_at >= ?", since_time) if since_time
99
+ manifest_changes = manifest_changes.where("commits.committed_at <= ?", until_time) if until_time
100
+ { path: manifest.path, ecosystem: manifest.ecosystem, changes: manifest_changes.count }
88
101
  end
89
102
 
90
103
  data
@@ -97,6 +110,8 @@ module Git
97
110
 
98
111
  puts "Branch: #{data[:branch]}"
99
112
  puts "Ecosystem: #{data[:ecosystem]}" if data[:ecosystem]
113
+ puts "Since: #{data[:since]}" if data[:since]
114
+ puts "Until: #{data[:until]}" if data[:until]
100
115
  puts "Commits analyzed: #{data[:commits_analyzed]}"
101
116
  puts "Commits with changes: #{data[:commits_with_changes]}"
102
117
  puts
@@ -144,11 +159,16 @@ module Git
144
159
  end
145
160
 
146
161
  def output_by_author
162
+ since_time = @options[:since] ? parse_time(@options[:since]) : nil
163
+ until_time = @options[:until] ? parse_time(@options[:until]) : nil
164
+
147
165
  changes = Models::DependencyChange
148
166
  .joins(:commit)
149
167
  .where(change_type: "added")
150
168
 
151
169
  changes = changes.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem]
170
+ changes = changes.where("commits.committed_at >= ?", since_time) if since_time
171
+ changes = changes.where("commits.committed_at <= ?", until_time) if until_time
152
172
 
153
173
  counts = changes
154
174
  .group("commits.author_name")
@@ -157,7 +177,7 @@ module Git
157
177
  .count
158
178
 
159
179
  if counts.empty?
160
- puts "No dependency additions found"
180
+ empty_result "No dependency additions found"
161
181
  return
162
182
  end
163
183
 
@@ -166,15 +186,25 @@ module Git
166
186
  data = counts.map { |name, count| { author: name, added: count } }
167
187
  puts JSON.pretty_generate(data)
168
188
  else
169
- puts "Dependencies Added by Author"
170
- puts "=" * 40
171
- puts
172
- counts.each do |name, count|
173
- puts " #{count.to_s.rjust(4)} #{name}"
174
- end
189
+ paginate { output_by_author_text(counts) }
190
+ end
191
+ end
192
+
193
+ def output_by_author_text(counts)
194
+ puts "Dependencies Added by Author"
195
+ puts "=" * 40
196
+ puts
197
+ counts.each do |name, count|
198
+ puts " #{count.to_s.rjust(4)} #{name}"
175
199
  end
176
200
  end
177
201
 
202
+ def parse_time(str)
203
+ Time.parse(str)
204
+ rescue ArgumentError
205
+ error "Invalid date format: #{str}"
206
+ end
207
+
178
208
  def parse_options
179
209
  options = {}
180
210
 
@@ -193,6 +223,14 @@ module Git
193
223
  options[:format] = v
194
224
  end
195
225
 
226
+ opts.on("--since=DATE", "Show changes after date") do |v|
227
+ options[:since] = v
228
+ end
229
+
230
+ opts.on("--until=DATE", "Show changes before date") do |v|
231
+ options[:until] = v
232
+ end
233
+
196
234
  opts.on("--by-author", "Show dependencies added by author") do
197
235
  options[:by_author] = true
198
236
  end
@@ -201,6 +239,10 @@ module Git
201
239
  options[:limit] = v
202
240
  end
203
241
 
242
+ opts.on("--no-pager", "Do not pipe output into a pager") do
243
+ options[:no_pager] = true
244
+ end
245
+
204
246
  opts.on("-h", "--help", "Show this help") do
205
247
  puts opts
206
248
  exit
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Tree
7
+ include Output
8
+
7
9
  def initialize(args)
8
10
  @args = args
9
11
  @options = parse_options
@@ -11,11 +13,7 @@ module Git
11
13
 
12
14
  def run
13
15
  repo = Repository.new
14
-
15
- unless Database.exists?(repo.git_dir)
16
- $stderr.puts "Database not initialized. Run 'git pkgs init' first."
17
- exit 1
18
- end
16
+ require_database(repo)
19
17
 
20
18
  Database.connect(repo.git_dir)
21
19
 
@@ -23,10 +21,7 @@ module Git
23
21
  commit_sha = @options[:commit] || repo.head_sha
24
22
  commit = find_commit_with_snapshot(commit_sha, repo)
25
23
 
26
- unless commit
27
- $stderr.puts "No dependency data found for commit #{commit_sha[0, 7]}"
28
- exit 1
29
- end
24
+ error "No dependency data found for commit #{commit_sha[0, 7]}" unless commit
30
25
 
31
26
  # Get current snapshots
32
27
  snapshots = commit.dependency_snapshots.includes(:manifest)
@@ -36,13 +31,17 @@ module Git
36
31
  end
37
32
 
38
33
  if snapshots.empty?
39
- puts "No dependencies found"
34
+ empty_result "No dependencies found"
40
35
  return
41
36
  end
42
37
 
43
38
  # Group by manifest and build tree
44
39
  grouped = snapshots.group_by { |s| s.manifest }
45
40
 
41
+ paginate { output_text(grouped, snapshots) }
42
+ end
43
+
44
+ def output_text(grouped, snapshots)
46
45
  grouped.each do |manifest, deps|
47
46
  puts "#{manifest.path} (#{manifest.ecosystem})"
48
47
  puts
@@ -109,6 +108,10 @@ module Git
109
108
  options[:branch] = v
110
109
  end
111
110
 
111
+ opts.on("--no-pager", "Do not pipe output into a pager") do
112
+ options[:no_pager] = true
113
+ end
114
+
112
115
  opts.on("-h", "--help", "Show this help") do
113
116
  puts opts
114
117
  exit