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.
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Update
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
26
- $stderr.puts "Branch '#{branch_name}' not in database. Run 'git pkgs init --branch=#{branch_name}' first."
27
- exit 1
28
- end
23
+ error "Branch '#{branch_name}' not in database. Run 'git pkgs init --branch=#{branch_name}' first." unless branch
29
24
 
30
25
  since_sha = branch.last_analyzed_sha
31
26
  current_sha = repo.branch_target(branch_name)
@@ -62,60 +57,62 @@ module Git
62
57
  puts "Updating branch: #{branch_name}"
63
58
  puts "Found #{total} new commits"
64
59
 
65
- commits.each do |rugged_commit|
66
- processed += 1
67
- print "\rProcessing commit #{processed}/#{total}..."
68
-
69
- result = analyzer.analyze_commit(rugged_commit, snapshot)
70
-
71
- commit = Models::Commit.find_or_create_from_rugged(rugged_commit)
72
- Models::BranchCommit.find_or_create_by(
73
- branch: branch,
74
- commit: commit,
75
- position: last_position + processed
76
- )
77
-
78
- if result && result[:changes].any?
79
- dependency_commits += 1
80
- commit.update(has_dependency_changes: true)
81
-
82
- result[:changes].each do |change|
83
- manifest = Models::Manifest.find_or_create(
84
- path: change[:manifest_path],
85
- ecosystem: change[:ecosystem],
86
- kind: change[:kind]
87
- )
88
-
89
- Models::DependencyChange.create!(
90
- commit: commit,
91
- manifest: manifest,
92
- name: change[:name],
93
- ecosystem: change[:ecosystem],
94
- change_type: change[:change_type],
95
- requirement: change[:requirement],
96
- previous_requirement: change[:previous_requirement],
97
- dependency_type: change[:dependency_type]
98
- )
99
- end
60
+ ActiveRecord::Base.transaction do
61
+ commits.each do |rugged_commit|
62
+ processed += 1
63
+ print "\rProcessing commit #{processed}/#{total}..."
64
+
65
+ result = analyzer.analyze_commit(rugged_commit, snapshot)
66
+
67
+ commit = Models::Commit.find_or_create_from_rugged(rugged_commit)
68
+ Models::BranchCommit.find_or_create_by(
69
+ branch: branch,
70
+ commit: commit,
71
+ position: last_position + processed
72
+ )
73
+
74
+ if result && result[:changes].any?
75
+ dependency_commits += 1
76
+ commit.update(has_dependency_changes: true)
77
+
78
+ result[:changes].each do |change|
79
+ manifest = Models::Manifest.find_or_create(
80
+ path: change[:manifest_path],
81
+ ecosystem: change[:ecosystem],
82
+ kind: change[:kind]
83
+ )
84
+
85
+ Models::DependencyChange.create!(
86
+ commit: commit,
87
+ manifest: manifest,
88
+ name: change[:name],
89
+ ecosystem: change[:ecosystem],
90
+ change_type: change[:change_type],
91
+ requirement: change[:requirement],
92
+ previous_requirement: change[:previous_requirement],
93
+ dependency_type: change[:dependency_type]
94
+ )
95
+ end
100
96
 
101
- snapshot = result[:snapshot]
102
-
103
- snapshot.each do |(manifest_path, name), dep_info|
104
- manifest = Models::Manifest.find_by(path: manifest_path)
105
- Models::DependencySnapshot.find_or_create_by(
106
- commit: commit,
107
- manifest: manifest,
108
- name: name
109
- ) do |s|
110
- s.ecosystem = dep_info[:ecosystem]
111
- s.requirement = dep_info[:requirement]
112
- s.dependency_type = dep_info[:dependency_type]
97
+ snapshot = result[:snapshot]
98
+
99
+ snapshot.each do |(manifest_path, name), dep_info|
100
+ manifest = Models::Manifest.find_by(path: manifest_path)
101
+ Models::DependencySnapshot.find_or_create_by(
102
+ commit: commit,
103
+ manifest: manifest,
104
+ name: name
105
+ ) do |s|
106
+ s.ecosystem = dep_info[:ecosystem]
107
+ s.requirement = dep_info[:requirement]
108
+ s.dependency_type = dep_info[:dependency_type]
109
+ end
113
110
  end
114
111
  end
115
112
  end
116
- end
117
113
 
118
- branch.update(last_analyzed_sha: current_sha)
114
+ branch.update(last_analyzed_sha: current_sha)
115
+ end
119
116
 
120
117
  puts "\nDone!"
121
118
  puts "Processed #{total} new commits"
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Pkgs
5
+ module Commands
6
+ class Upgrade
7
+ include Output
8
+
9
+ def initialize(args)
10
+ @args = args
11
+ @options = parse_options
12
+ end
13
+
14
+ def run
15
+ repo = Repository.new
16
+ require_database(repo)
17
+
18
+ Database.connect(repo.git_dir, check_version: false)
19
+
20
+ stored = Database.stored_version || 0
21
+ current = Database::SCHEMA_VERSION
22
+
23
+ if stored >= current
24
+ puts "Database is up to date (version #{current})"
25
+ return
26
+ end
27
+
28
+ puts "Upgrading database from version #{stored} to #{current}..."
29
+ puts "This requires re-indexing the repository."
30
+ puts
31
+
32
+ # Run init --force
33
+ Init.new(["--force"]).run
34
+ end
35
+
36
+ def parse_options
37
+ options = {}
38
+
39
+ parser = OptionParser.new do |opts|
40
+ opts.banner = "Usage: git pkgs upgrade [options]"
41
+
42
+ opts.on("-h", "--help", "Show this help") do
43
+ puts opts
44
+ exit
45
+ end
46
+ end
47
+
48
+ parser.parse!(@args)
49
+ options
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Pkgs
5
+ module Commands
6
+ class Where
7
+ include Output
8
+
9
+ def initialize(args)
10
+ @args = args
11
+ @options = parse_options
12
+ end
13
+
14
+ def run
15
+ name = @args.first
16
+
17
+ error "Usage: git pkgs where <package-name>" unless name
18
+
19
+ repo = Repository.new
20
+ require_database(repo)
21
+
22
+ Database.connect(repo.git_dir)
23
+
24
+ workdir = File.dirname(repo.git_dir)
25
+ branch = Models::Branch.find_by(name: @options[:branch] || repo.default_branch)
26
+
27
+ unless branch
28
+ error "Branch not found. Run 'git pkgs init' first."
29
+ end
30
+
31
+ snapshots = Models::DependencySnapshot.current_for_branch(branch)
32
+ snapshots = snapshots.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem]
33
+
34
+ manifest_paths = snapshots.for_package(name).joins(:manifest).pluck("manifests.path").uniq
35
+
36
+ if manifest_paths.empty?
37
+ empty_result "Package '#{name}' not found in current dependencies"
38
+ return
39
+ end
40
+
41
+ results = manifest_paths.flat_map do |path|
42
+ find_in_manifest(name, File.join(workdir, path), path)
43
+ end
44
+
45
+ if results.empty?
46
+ empty_result "Package '#{name}' tracked but not found in current files"
47
+ return
48
+ end
49
+
50
+ if @options[:format] == "json"
51
+ output_json(results)
52
+ else
53
+ paginate { output_text(results, name) }
54
+ end
55
+ end
56
+
57
+ def find_in_manifest(name, full_path, display_path)
58
+ return [] unless File.exist?(full_path)
59
+
60
+ lines = File.readlines(full_path)
61
+ matches = []
62
+
63
+ lines.each_with_index do |line, idx|
64
+ next unless line.include?(name)
65
+
66
+ match = { path: display_path, line: idx + 1, content: line.rstrip }
67
+
68
+ if context_lines > 0
69
+ match[:before] = context_before(lines, idx)
70
+ match[:after] = context_after(lines, idx)
71
+ end
72
+
73
+ matches << match
74
+ end
75
+
76
+ matches
77
+ end
78
+
79
+ def context_lines
80
+ @options[:context] || 0
81
+ end
82
+
83
+ def context_before(lines, idx)
84
+ start_idx = [0, idx - context_lines].max
85
+ (start_idx...idx).map { |i| { line: i + 1, content: lines[i].rstrip } }
86
+ end
87
+
88
+ def context_after(lines, idx)
89
+ end_idx = [lines.length - 1, idx + context_lines].min
90
+ ((idx + 1)..end_idx).map { |i| { line: i + 1, content: lines[i].rstrip } }
91
+ end
92
+
93
+ def output_text(results, name)
94
+ results.each_with_index do |result, i|
95
+ puts "--" if i > 0 && context_lines > 0
96
+
97
+ result[:before]&.each do |ctx|
98
+ puts format_context_line(result[:path], ctx[:line], ctx[:content])
99
+ end
100
+
101
+ puts format_match_line(result[:path], result[:line], result[:content], name)
102
+
103
+ result[:after]&.each do |ctx|
104
+ puts format_context_line(result[:path], ctx[:line], ctx[:content])
105
+ end
106
+ end
107
+ end
108
+
109
+ def format_match_line(path, line_num, content, name)
110
+ path_str = Color.magenta(path)
111
+ line_str = Color.green(line_num.to_s)
112
+ highlighted = content.gsub(name, Color.red(name))
113
+ "#{path_str}:#{line_str}:#{highlighted}"
114
+ end
115
+
116
+ def format_context_line(path, line_num, content)
117
+ path_str = Color.magenta(path)
118
+ line_str = Color.green(line_num.to_s)
119
+ content_str = Color.dim(content)
120
+ "#{path_str}-#{line_str}-#{content_str}"
121
+ end
122
+
123
+ def output_json(results)
124
+ require "json"
125
+ puts JSON.pretty_generate(results)
126
+ end
127
+
128
+ def parse_options
129
+ options = {}
130
+
131
+ parser = OptionParser.new do |opts|
132
+ opts.banner = "Usage: git pkgs where <package-name> [options]"
133
+
134
+ opts.on("-b", "--branch=NAME", "Branch to search (default: current)") do |v|
135
+ options[:branch] = v
136
+ end
137
+
138
+ opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
139
+ options[:ecosystem] = v
140
+ end
141
+
142
+ opts.on("-C", "--context=NUM", Integer, "Show NUM lines of context") do |v|
143
+ options[:context] = v
144
+ end
145
+
146
+ opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
147
+ options[:format] = v
148
+ end
149
+
150
+ opts.on("--no-pager", "Do not pipe output into a pager") do
151
+ options[:no_pager] = true
152
+ end
153
+
154
+ opts.on("-h", "--help", "Show this help") do
155
+ puts opts
156
+ exit
157
+ end
158
+ end
159
+
160
+ parser.parse!(@args)
161
+ options
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -4,6 +4,8 @@ module Git
4
4
  module Pkgs
5
5
  module Commands
6
6
  class Why
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
  package_name = @args.shift
14
16
 
15
- unless package_name
16
- $stderr.puts "Usage: git pkgs why <package>"
17
- exit 1
18
- end
17
+ error "Usage: git pkgs why <package>" unless package_name
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
 
@@ -40,7 +35,7 @@ module Git
40
35
  added_change = added_change.first
41
36
 
42
37
  unless added_change
43
- puts "Package '#{package_name}' not found in dependency history"
38
+ empty_result "Package '#{package_name}' not found in dependency history"
44
39
  return
45
40
  end
46
41
 
@@ -7,13 +7,24 @@ module Git
7
7
  module Pkgs
8
8
  class Database
9
9
  DB_FILE = "pkgs.sqlite3"
10
+ SCHEMA_VERSION = 1
10
11
 
11
12
  def self.path(git_dir = nil)
13
+ return ENV["GIT_PKGS_DB"] if ENV["GIT_PKGS_DB"] && !ENV["GIT_PKGS_DB"].empty?
14
+
12
15
  git_dir ||= find_git_dir
13
16
  File.join(git_dir, DB_FILE)
14
17
  end
15
18
 
16
19
  def self.find_git_dir
20
+ # Respect GIT_DIR environment variable
21
+ if ENV["GIT_DIR"] && !ENV["GIT_DIR"].empty?
22
+ git_dir = ENV["GIT_DIR"]
23
+ return git_dir if File.directory?(git_dir)
24
+
25
+ raise NotInGitRepoError, "GIT_DIR '#{git_dir}' does not exist"
26
+ end
27
+
17
28
  dir = Dir.pwd
18
29
  loop do
19
30
  git_dir = File.join(dir, ".git")
@@ -26,12 +37,13 @@ module Git
26
37
  end
27
38
  end
28
39
 
29
- def self.connect(git_dir = nil)
40
+ def self.connect(git_dir = nil, check_version: true)
30
41
  db_path = path(git_dir)
31
42
  ActiveRecord::Base.establish_connection(
32
43
  adapter: "sqlite3",
33
44
  database: db_path
34
45
  )
46
+ check_version! if check_version
35
47
  end
36
48
 
37
49
  def self.connect_memory
@@ -48,6 +60,10 @@ module Git
48
60
 
49
61
  def self.create_schema(with_indexes: true)
50
62
  ActiveRecord::Schema.define do
63
+ create_table :schema_info, if_not_exists: true do |t|
64
+ t.integer :version, null: false
65
+ end
66
+
51
67
  create_table :branches, if_not_exists: true do |t|
52
68
  t.string :name, null: false
53
69
  t.string :last_analyzed_sha
@@ -104,6 +120,7 @@ module Git
104
120
  end
105
121
  end
106
122
 
123
+ set_version
107
124
  create_bulk_indexes if with_indexes
108
125
  end
109
126
 
@@ -122,6 +139,43 @@ module Git
122
139
  conn.add_index :dependency_snapshots, :ecosystem, if_not_exists: true
123
140
  end
124
141
 
142
+ def self.stored_version
143
+ conn = ActiveRecord::Base.connection
144
+ return nil unless conn.table_exists?(:schema_info)
145
+
146
+ result = conn.select_value("SELECT version FROM schema_info LIMIT 1")
147
+ result&.to_i
148
+ end
149
+
150
+ def self.set_version(version = SCHEMA_VERSION)
151
+ conn = ActiveRecord::Base.connection
152
+ conn.execute("DELETE FROM schema_info")
153
+ conn.execute("INSERT INTO schema_info (version) VALUES (#{version})")
154
+ end
155
+
156
+ def self.needs_upgrade?
157
+ conn = ActiveRecord::Base.connection
158
+
159
+ # No tables at all = fresh database, no upgrade needed
160
+ return false unless conn.table_exists?(:commits)
161
+
162
+ # Has commits table but no schema_info = old database, needs upgrade
163
+ return true unless conn.table_exists?(:schema_info)
164
+
165
+ # Check version
166
+ stored = stored_version || 0
167
+ stored < SCHEMA_VERSION
168
+ end
169
+
170
+ def self.check_version!
171
+ return unless needs_upgrade?
172
+
173
+ stored = stored_version || 0
174
+ $stderr.puts "Database schema is outdated (version #{stored}, current is #{SCHEMA_VERSION})."
175
+ $stderr.puts "Run 'git pkgs upgrade' to update."
176
+ exit 1
177
+ end
178
+
125
179
  def self.optimize_for_bulk_writes
126
180
  conn = ActiveRecord::Base.connection
127
181
  conn.execute("PRAGMA synchronous = OFF")
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "pager"
5
+
6
+ module Git
7
+ module Pkgs
8
+ module Output
9
+ include Pager
10
+ # Print error message and exit with code 1.
11
+ # Use for user errors (bad input, invalid refs) and system errors (db missing).
12
+ # When format is :json, outputs JSON to stdout; otherwise outputs text to stderr.
13
+ def error(msg, format: nil)
14
+ format ||= @options[:format] if defined?(@options) && @options.is_a?(Hash)
15
+
16
+ if format == "json"
17
+ puts JSON.generate({ error: msg })
18
+ else
19
+ $stderr.puts msg
20
+ end
21
+ exit 1
22
+ end
23
+
24
+ # Print informational message for "no results" cases.
25
+ # When format is :json, outputs empty JSON array/object; otherwise outputs text.
26
+ def empty_result(msg, format: nil, json_value: [])
27
+ format ||= @options[:format] if defined?(@options) && @options.is_a?(Hash)
28
+
29
+ if format == "json"
30
+ puts JSON.generate(json_value)
31
+ else
32
+ puts msg
33
+ end
34
+ end
35
+
36
+ # Standard check for database existence.
37
+ def require_database(repo)
38
+ return if Database.exists?(repo.git_dir)
39
+
40
+ error "Database not initialized. Run 'git pkgs init' first."
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Git
6
+ module Pkgs
7
+ module Pager
8
+ # Returns the pager command following git's precedence:
9
+ # 1. GIT_PAGER environment variable
10
+ # 2. core.pager git config
11
+ # 3. PAGER environment variable
12
+ # 4. less as fallback
13
+ def git_pager
14
+ return ENV["GIT_PAGER"] if ENV["GIT_PAGER"] && !ENV["GIT_PAGER"].empty?
15
+
16
+ config_pager = `git config --get core.pager`.chomp
17
+ return config_pager unless config_pager.empty?
18
+
19
+ return ENV["PAGER"] if ENV["PAGER"] && !ENV["PAGER"].empty?
20
+
21
+ "less -FRSX"
22
+ end
23
+
24
+ # Returns true if paging is disabled (pager set to empty string or 'cat')
25
+ def pager_disabled?
26
+ pager = git_pager
27
+ pager.empty? || pager == "cat"
28
+ end
29
+
30
+ # Captures output from a block and sends it through the pager.
31
+ # Falls back to direct output if:
32
+ # - stdout is not a TTY
33
+ # - pager is disabled
34
+ # - pager command fails
35
+ def paginate
36
+ if !$stdout.tty? || pager_disabled? || paging_disabled?
37
+ yield
38
+ return
39
+ end
40
+
41
+ output = StringIO.new
42
+ old_stdout = $stdout
43
+ $stdout = output
44
+
45
+ begin
46
+ yield
47
+ ensure
48
+ $stdout = old_stdout
49
+ end
50
+
51
+ content = output.string
52
+ return if content.empty?
53
+
54
+ IO.popen(git_pager, "w") { |io| io.write(content) }
55
+ rescue Errno::EPIPE
56
+ # User quit pager early
57
+ rescue Errno::ENOENT
58
+ # Pager command not found, fall back to direct output
59
+ print content
60
+ end
61
+
62
+ # Check if paging was disabled via --no-pager option
63
+ def paging_disabled?
64
+ defined?(@options) && @options.is_a?(Hash) && @options[:no_pager]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -7,9 +7,9 @@ module Git
7
7
  class Repository
8
8
  attr_reader :path
9
9
 
10
- def initialize(path = Dir.pwd)
11
- @path = path
12
- @rugged = Rugged::Repository.new(path)
10
+ def initialize(path = nil)
11
+ @path = path || ENV["GIT_DIR"] || Dir.pwd
12
+ @rugged = Rugged::Repository.new(@path)
13
13
  end
14
14
 
15
15
  def git_dir
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Git
4
4
  module Pkgs
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/git/pkgs.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "pkgs/version"
4
+ require_relative "pkgs/output"
5
+ require_relative "pkgs/color"
4
6
  require_relative "pkgs/cli"
5
7
  require_relative "pkgs/database"
6
8
  require_relative "pkgs/repository"
@@ -21,13 +23,18 @@ require_relative "pkgs/commands/list"
21
23
  require_relative "pkgs/commands/history"
22
24
  require_relative "pkgs/commands/why"
23
25
  require_relative "pkgs/commands/blame"
24
- require_relative "pkgs/commands/outdated"
26
+ require_relative "pkgs/commands/stale"
25
27
  require_relative "pkgs/commands/stats"
26
28
  require_relative "pkgs/commands/diff"
27
29
  require_relative "pkgs/commands/tree"
28
30
  require_relative "pkgs/commands/branch"
29
31
  require_relative "pkgs/commands/search"
30
32
  require_relative "pkgs/commands/show"
33
+ require_relative "pkgs/commands/where"
34
+ require_relative "pkgs/commands/log"
35
+ require_relative "pkgs/commands/upgrade"
36
+ require_relative "pkgs/commands/schema"
37
+ require_relative "pkgs/commands/diff_driver"
31
38
 
32
39
  module Git
33
40
  module Pkgs