git-pkgs 0.1.1 → 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.
@@ -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 History
9
+ include Output
10
+
7
11
  def initialize(args)
8
12
  @args = args
9
13
  @options = parse_options
@@ -13,11 +17,7 @@ module Git
13
17
  package_name = @args.shift
14
18
 
15
19
  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
20
+ require_database(repo)
21
21
 
22
22
  Database.connect(repo.git_dir)
23
23
 
@@ -31,19 +31,34 @@ module Git
31
31
  changes = changes.for_platform(@options[:ecosystem])
32
32
  end
33
33
 
34
+ if @options[:author]
35
+ author = @options[:author]
36
+ changes = changes.joins(:commit).where(
37
+ "commits.author_name LIKE ? OR commits.author_email LIKE ?",
38
+ "%#{author}%", "%#{author}%"
39
+ )
40
+ end
41
+
42
+ if @options[:since]
43
+ since_time = parse_time(@options[:since])
44
+ changes = changes.joins(:commit).where("commits.committed_at >= ?", since_time)
45
+ end
46
+
47
+ if @options[:until]
48
+ until_time = parse_time(@options[:until])
49
+ changes = changes.joins(:commit).where("commits.committed_at <= ?", until_time)
50
+ end
51
+
34
52
  if changes.empty?
35
- if package_name
36
- puts "No history found for '#{package_name}'"
37
- else
38
- puts "No dependency changes found"
39
- end
53
+ msg = package_name ? "No history found for '#{package_name}'" : "No dependency changes found"
54
+ empty_result msg
40
55
  return
41
56
  end
42
57
 
43
58
  if @options[:format] == "json"
44
59
  output_json(changes)
45
60
  else
46
- output_text(changes, package_name)
61
+ paginate { output_text(changes, package_name) }
47
62
  end
48
63
  end
49
64
 
@@ -61,13 +76,13 @@ module Git
61
76
 
62
77
  case change.change_type
63
78
  when "added"
64
- action = "Added"
79
+ action = Color.green("Added")
65
80
  version_info = change.requirement
66
81
  when "modified"
67
- action = "Updated"
82
+ action = Color.yellow("Updated")
68
83
  version_info = "#{change.previous_requirement} -> #{change.requirement}"
69
84
  when "removed"
70
- action = "Removed"
85
+ action = Color.red("Removed")
71
86
  version_info = change.requirement
72
87
  end
73
88
 
@@ -118,6 +133,22 @@ module Git
118
133
  options[:format] = v
119
134
  end
120
135
 
136
+ opts.on("--author=NAME", "Filter by author name or email") do |v|
137
+ options[:author] = v
138
+ end
139
+
140
+ opts.on("--since=DATE", "Show changes after date (YYYY-MM-DD)") do |v|
141
+ options[:since] = v
142
+ end
143
+
144
+ opts.on("--until=DATE", "Show changes before date (YYYY-MM-DD)") do |v|
145
+ options[:until] = v
146
+ end
147
+
148
+ opts.on("--no-pager", "Do not pipe output into a pager") do
149
+ options[:no_pager] = true
150
+ end
151
+
121
152
  opts.on("-h", "--help", "Show this help") do
122
153
  puts opts
123
154
  exit
@@ -127,6 +158,12 @@ module Git
127
158
  parser.parse!(@args)
128
159
  options
129
160
  end
161
+
162
+ def parse_time(str)
163
+ Time.parse(str)
164
+ rescue ArgumentError
165
+ error "Invalid date format: #{str}"
166
+ end
130
167
  end
131
168
  end
132
169
  end
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Hooks
7
+ include Output
8
+
7
9
  HOOK_SCRIPT = <<~SCRIPT
8
10
  #!/bin/sh
9
11
  # git-pkgs auto-update hook
@@ -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