git-pkgs 0.2.0 → 0.3.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.
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Info
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
  db_path = Database.path(repo.git_dir)
21
19
  Database.connect(repo.git_dir)
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Init
7
+ include Output
8
+
7
9
  BATCH_SIZE = 100
8
10
  SNAPSHOT_INTERVAL = 20 # Store snapshot every N dependency-changing commits
9
11
 
@@ -21,15 +23,12 @@ module Git
21
23
  end
22
24
 
23
25
  Database.drop if @options[:force]
24
- Database.connect(repo.git_dir)
26
+ Database.connect(repo.git_dir, check_version: false)
25
27
  Database.create_schema(with_indexes: false)
26
28
  Database.optimize_for_bulk_writes
27
29
 
28
30
  branch_name = @options[:branch] || repo.default_branch
29
- unless repo.branch_exists?(branch_name)
30
- $stderr.puts "Branch '#{branch_name}' not found"
31
- exit 1
32
- end
31
+ error "Branch '#{branch_name}' not found" unless repo.branch_exists?(branch_name)
33
32
 
34
33
  branch = Models::Branch.find_or_create(branch_name)
35
34
  analyzer = Analyzer.new(repo)
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class List
7
+ include Output
8
+
7
9
  def initialize(args)
8
10
  @args = args
9
11
  @options = parse_options
@@ -11,26 +13,19 @@ 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
  commit_sha = @options[:commit] || repo.head_sha
23
21
  target_commit = Models::Commit.find_by(sha: commit_sha)
24
22
 
25
- unless target_commit
26
- $stderr.puts "Commit #{commit_sha[0, 7]} not found in database"
27
- exit 1
28
- end
23
+ error "Commit #{commit_sha[0, 7]} not found in database" unless target_commit
29
24
 
30
25
  deps = compute_dependencies_at_commit(target_commit, repo)
31
26
 
32
27
  if deps.empty?
33
- puts "No dependencies found"
28
+ empty_result "No dependencies found"
34
29
  return
35
30
  end
36
31
 
@@ -47,16 +42,20 @@ module Git
47
42
  require "json"
48
43
  puts JSON.pretty_generate(deps)
49
44
  else
50
- grouped = deps.group_by { |d| [d[:manifest_path], d[:ecosystem]] }
45
+ paginate { output_text(deps) }
46
+ end
47
+ end
51
48
 
52
- grouped.each do |(path, platform), manifest_deps|
53
- puts "#{path} (#{platform}):"
54
- manifest_deps.sort_by { |d| d[:name] }.each do |dep|
55
- type_suffix = dep[:dependency_type] ? " [#{dep[:dependency_type]}]" : ""
56
- puts " #{dep[:name]} #{dep[:requirement]}#{type_suffix}"
57
- end
58
- puts
49
+ def output_text(deps)
50
+ grouped = deps.group_by { |d| [d[:manifest_path], d[:ecosystem]] }
51
+
52
+ grouped.each do |(path, platform), manifest_deps|
53
+ puts "#{path} (#{platform}):"
54
+ manifest_deps.sort_by { |d| d[:name] }.each do |dep|
55
+ type_suffix = dep[:dependency_type] ? " [#{dep[:dependency_type]}]" : ""
56
+ puts " #{dep[:name]} #{dep[:requirement]}#{type_suffix}"
59
57
  end
58
+ puts
60
59
  end
61
60
  end
62
61
 
@@ -144,6 +143,10 @@ module Git
144
143
  options[:format] = v
145
144
  end
146
145
 
146
+ opts.on("--no-pager", "Do not pipe output into a pager") do
147
+ options[:no_pager] = true
148
+ end
149
+
147
150
  opts.on("-h", "--help", "Show this help") do
148
151
  puts opts
149
152
  exit
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Git
6
+ module Pkgs
7
+ module Commands
8
+ class Log
9
+ include Output
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
+
22
+ commits = Models::Commit
23
+ .where(has_dependency_changes: true)
24
+ .order(committed_at: :desc)
25
+
26
+ commits = commits.where("author_name LIKE ? OR author_email LIKE ?",
27
+ "%#{@options[:author]}%", "%#{@options[:author]}%") if @options[:author]
28
+ commits = commits.where("committed_at >= ?", parse_time(@options[:since])) if @options[:since]
29
+ commits = commits.where("committed_at <= ?", parse_time(@options[:until])) if @options[:until]
30
+
31
+ commits = commits.limit(@options[:limit] || 20)
32
+
33
+ if commits.empty?
34
+ empty_result "No commits with dependency changes found"
35
+ return
36
+ end
37
+
38
+ if @options[:format] == "json"
39
+ output_json(commits)
40
+ else
41
+ paginate { output_text(commits) }
42
+ end
43
+ end
44
+
45
+ def output_text(commits)
46
+ commits.each do |commit|
47
+ changes = commit.dependency_changes
48
+ changes = changes.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem]
49
+ next if changes.empty?
50
+
51
+ puts "#{commit.short_sha} #{commit.message&.lines&.first&.strip}"
52
+ puts "Author: #{commit.author_name} <#{commit.author_email}>"
53
+ puts "Date: #{commit.committed_at.strftime("%Y-%m-%d")}"
54
+ puts
55
+
56
+ added = changes.select { |c| c.change_type == "added" }
57
+ modified = changes.select { |c| c.change_type == "modified" }
58
+ removed = changes.select { |c| c.change_type == "removed" }
59
+
60
+ added.each do |change|
61
+ puts " + #{change.name} #{change.requirement}"
62
+ end
63
+
64
+ modified.each do |change|
65
+ puts " ~ #{change.name} #{change.previous_requirement} -> #{change.requirement}"
66
+ end
67
+
68
+ removed.each do |change|
69
+ puts " - #{change.name}"
70
+ end
71
+
72
+ puts
73
+ end
74
+ end
75
+
76
+ def output_json(commits)
77
+ require "json"
78
+
79
+ data = commits.map do |commit|
80
+ changes = commit.dependency_changes
81
+ changes = changes.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem]
82
+
83
+ {
84
+ sha: commit.sha,
85
+ short_sha: commit.short_sha,
86
+ message: commit.message&.lines&.first&.strip,
87
+ author_name: commit.author_name,
88
+ author_email: commit.author_email,
89
+ date: commit.committed_at.iso8601,
90
+ changes: changes.map do |change|
91
+ {
92
+ name: change.name,
93
+ change_type: change.change_type,
94
+ requirement: change.requirement,
95
+ previous_requirement: change.previous_requirement,
96
+ ecosystem: change.ecosystem
97
+ }
98
+ end
99
+ }
100
+ end
101
+
102
+ puts JSON.pretty_generate(data)
103
+ end
104
+
105
+ def parse_time(str)
106
+ Time.parse(str)
107
+ rescue ArgumentError
108
+ error "Invalid date format: #{str}"
109
+ end
110
+
111
+ def parse_options
112
+ options = {}
113
+
114
+ parser = OptionParser.new do |opts|
115
+ opts.banner = "Usage: git pkgs log [options]"
116
+
117
+ opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
118
+ options[:ecosystem] = v
119
+ end
120
+
121
+ opts.on("--author=NAME", "Filter by author name or email") do |v|
122
+ options[:author] = v
123
+ end
124
+
125
+ opts.on("--since=DATE", "Show commits after date") do |v|
126
+ options[:since] = v
127
+ end
128
+
129
+ opts.on("--until=DATE", "Show commits before date") do |v|
130
+ options[:until] = v
131
+ end
132
+
133
+ opts.on("-n", "--limit=N", Integer, "Limit commits (default: 20)") do |v|
134
+ options[:limit] = v
135
+ end
136
+
137
+ opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
138
+ options[:format] = v
139
+ end
140
+
141
+ opts.on("--no-pager", "Do not pipe output into a pager") do
142
+ options[:no_pager] = true
143
+ end
144
+
145
+ opts.on("-h", "--help", "Show this help") do
146
+ puts opts
147
+ exit
148
+ end
149
+ end
150
+
151
+ parser.parse!(@args)
152
+ options
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -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