Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Commit dbcd030

Browse files
committed
Implement stateless command execution for list, show, and diff commands
1 parent 88d86e0 commit dbcd030

File tree

7 files changed

+524
-18
lines changed

7 files changed

+524
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## [Unreleased]
22

3+
- `--stateless` flag for `list`, `show`, and `diff` commands (auto-enabled when no database exists)
34
- Fix `-f` flag conflict in `diff` command (was defined for both `--from` and `--format`)
5+
- Disable GPG signing in test suite for faster tests
46

57
## [0.6.2] - 2026-01-06
68

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ git pkgs list
9898
git pkgs list --commit=abc123
9999
git pkgs list --ecosystem=rubygems
100100
git pkgs list --manifest=Gemfile
101+
git pkgs list --stateless # parse manifests directly, no database needed
101102
```
102103

103104
Example output:
@@ -247,6 +248,7 @@ Shows dependencies sorted by how long since they were last changed in your repo.
247248
```bash
248249
git pkgs diff --from=abc123 --to=def456
249250
git pkgs diff --from=HEAD~10
251+
git pkgs diff main..feature --stateless # no database needed
250252
```
251253

252254
This shows added, removed, and modified packages with version info.
@@ -257,6 +259,7 @@ This shows added, removed, and modified packages with version info.
257259
git pkgs show # show dependency changes in HEAD
258260
git pkgs show abc123 # specific commit
259261
git pkgs show HEAD~5 # relative ref
262+
git pkgs show --stateless # no database needed
260263
```
261264

262265
Like `git show` but for dependencies. Shows what was added, modified, or removed in a single commit.
@@ -327,7 +330,7 @@ Useful for understanding the [database structure](docs/schema.md) or generating
327330

328331
### CI usage
329332

330-
You can run git-pkgs in CI to show dependency changes in pull requests:
333+
You can run git-pkgs in CI to show dependency changes in pull requests. Use `--stateless` to skip database initialization for faster runs:
331334

332335
```yaml
333336
# .github/workflows/deps.yml
@@ -346,8 +349,7 @@ jobs:
346349
with:
347350
ruby-version: '3.3'
348351
- run: gem install git-pkgs
349-
- run: git pkgs init
350-
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD
352+
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD --stateless
351353
```
352354
353355
### Diff driver

lib/git/pkgs/analyzer.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,80 @@ def parse_manifest_by_oid(blob_oid, manifest_path)
288288
@blob_cache[cache_key] = { result: result, hits: 0 }
289289
result
290290
end
291+
292+
# Parse all manifest files at a given commit (stateless)
293+
def dependencies_at_commit(rugged_commit)
294+
deps = []
295+
manifest_paths = find_manifest_paths_in_tree(rugged_commit.tree)
296+
297+
manifest_paths.each do |path|
298+
result = parse_manifest_at_commit(rugged_commit, path)
299+
next unless result && result[:dependencies]
300+
301+
result[:dependencies].each do |dep|
302+
deps << {
303+
manifest_path: path,
304+
name: dep[:name],
305+
ecosystem: result[:platform],
306+
requirement: dep[:requirement],
307+
dependency_type: dep[:type]
308+
}
309+
end
310+
end
311+
312+
deps
313+
end
314+
315+
# Compute changes between two commits (stateless)
316+
def diff_commits(from_commit, to_commit)
317+
from_deps = dependencies_at_commit(from_commit).group_by { |d| [d[:manifest_path], d[:name]] }
318+
to_deps = dependencies_at_commit(to_commit).group_by { |d| [d[:manifest_path], d[:name]] }
319+
320+
added = []
321+
modified = []
322+
removed = []
323+
324+
# Find added and modified
325+
to_deps.each do |key, to_list|
326+
to_dep = to_list.first
327+
if from_deps[key]
328+
from_dep = from_deps[key].first
329+
if from_dep[:requirement] != to_dep[:requirement]
330+
modified << to_dep.merge(previous_requirement: from_dep[:requirement])
331+
end
332+
else
333+
added << to_dep
334+
end
335+
end
336+
337+
# Find removed
338+
from_deps.each do |key, from_list|
339+
removed << from_list.first unless to_deps[key]
340+
end
341+
342+
{ added: added, modified: modified, removed: removed }
343+
end
344+
345+
def find_manifest_paths_in_tree(tree, prefix = "")
346+
paths = []
347+
348+
tree.each do |entry|
349+
full_path = prefix.empty? ? entry[:name] : "#{prefix}/#{entry[:name]}"
350+
351+
if entry[:type] == :tree
352+
subtree = repository.lookup(entry[:oid])
353+
paths.concat(find_manifest_paths_in_tree(subtree, full_path))
354+
elsif entry[:type] == :blob && full_path.match?(QUICK_MANIFEST_REGEX)
355+
paths << full_path
356+
end
357+
end
358+
359+
identify_manifests_cached(paths)
360+
end
361+
362+
def lookup(oid)
363+
repository.lookup(oid)
364+
end
291365
end
292366
end
293367
end

lib/git/pkgs/commands/diff.rb

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ def initialize(args)
1313

1414
def run
1515
repo = Repository.new
16-
require_database(repo)
17-
18-
Database.connect(repo.git_dir)
16+
use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
1917

2018
from_ref, to_ref = parse_range_argument
2119
from_ref ||= @options[:from]
@@ -30,6 +28,46 @@ def run
3028
error "Could not resolve '#{from_ref}'. Check that the ref exists." unless from_sha
3129
error "Could not resolve '#{to_ref}'. Check that the ref exists." unless to_sha
3230

31+
if use_stateless
32+
run_stateless(repo, from_sha, to_sha)
33+
else
34+
run_with_database(repo, from_sha, to_sha)
35+
end
36+
end
37+
38+
def run_stateless(repo, from_sha, to_sha)
39+
from_commit = repo.lookup(from_sha)
40+
to_commit = repo.lookup(to_sha)
41+
42+
analyzer = Analyzer.new(repo)
43+
diff = analyzer.diff_commits(from_commit, to_commit)
44+
45+
if @options[:ecosystem]
46+
diff[:added] = diff[:added].select { |d| d[:ecosystem] == @options[:ecosystem] }
47+
diff[:modified] = diff[:modified].select { |d| d[:ecosystem] == @options[:ecosystem] }
48+
diff[:removed] = diff[:removed].select { |d| d[:ecosystem] == @options[:ecosystem] }
49+
end
50+
51+
if diff[:added].empty? && diff[:modified].empty? && diff[:removed].empty?
52+
if @options[:format] == "json"
53+
require "json"
54+
puts JSON.pretty_generate({ from: from_sha[0..7], to: to_sha[0..7], added: [], modified: [], removed: [] })
55+
else
56+
empty_result "No dependency changes between #{from_sha[0..7]} and #{to_sha[0..7]}"
57+
end
58+
return
59+
end
60+
61+
if @options[:format] == "json"
62+
output_json_stateless(from_sha, to_sha, diff)
63+
else
64+
paginate { output_text_stateless(from_sha, to_sha, diff) }
65+
end
66+
end
67+
68+
def run_with_database(repo, from_sha, to_sha)
69+
Database.connect(repo.git_dir)
70+
3371
from_commit = Models::Commit.find_or_create_from_repo(repo, from_sha)
3472
to_commit = Models::Commit.find_or_create_from_repo(repo, to_sha)
3573

@@ -154,6 +192,81 @@ def output_json(from_commit, to_commit, changes)
154192
puts JSON.pretty_generate(data)
155193
end
156194

195+
def output_text_stateless(from_sha, to_sha, diff)
196+
puts "Dependency changes from #{from_sha[0..7]} to #{to_sha[0..7]}:"
197+
puts
198+
199+
if diff[:added].any?
200+
puts Color.green("Added:")
201+
diff[:added].group_by { |d| d[:name] }.each do |name, pkg_changes|
202+
latest = pkg_changes.last
203+
puts Color.green(" + #{name} #{latest[:requirement]} (#{latest[:manifest_path]})")
204+
end
205+
puts
206+
end
207+
208+
if diff[:modified].any?
209+
puts Color.yellow("Modified:")
210+
diff[:modified].group_by { |d| d[:name] }.each do |name, pkg_changes|
211+
latest = pkg_changes.last
212+
puts Color.yellow(" ~ #{name} #{latest[:previous_requirement]} -> #{latest[:requirement]}")
213+
end
214+
puts
215+
end
216+
217+
if diff[:removed].any?
218+
puts Color.red("Removed:")
219+
diff[:removed].group_by { |d| d[:name] }.each do |name, pkg_changes|
220+
latest = pkg_changes.last
221+
puts Color.red(" - #{name} (was #{latest[:requirement]})")
222+
end
223+
puts
224+
end
225+
226+
added_count = Color.green("+#{diff[:added].map { |d| d[:name] }.uniq.count}")
227+
removed_count = Color.red("-#{diff[:removed].map { |d| d[:name] }.uniq.count}")
228+
modified_count = Color.yellow("~#{diff[:modified].map { |d| d[:name] }.uniq.count}")
229+
puts "Summary: #{added_count} #{removed_count} #{modified_count}"
230+
end
231+
232+
def output_json_stateless(from_sha, to_sha, diff)
233+
require "json"
234+
235+
format_change = lambda do |change|
236+
{
237+
name: change[:name],
238+
ecosystem: change[:ecosystem],
239+
requirement: change[:requirement],
240+
manifest: change[:manifest_path]
241+
}
242+
end
243+
244+
format_modified = lambda do |change|
245+
{
246+
name: change[:name],
247+
ecosystem: change[:ecosystem],
248+
previous_requirement: change[:previous_requirement],
249+
requirement: change[:requirement],
250+
manifest: change[:manifest_path]
251+
}
252+
end
253+
254+
data = {
255+
from: from_sha[0..7],
256+
to: to_sha[0..7],
257+
added: diff[:added].map { |c| format_change.call(c) },
258+
modified: diff[:modified].map { |c| format_modified.call(c) },
259+
removed: diff[:removed].map { |c| format_change.call(c) },
260+
summary: {
261+
added: diff[:added].map { |d| d[:name] }.uniq.count,
262+
modified: diff[:modified].map { |d| d[:name] }.uniq.count,
263+
removed: diff[:removed].map { |d| d[:name] }.uniq.count
264+
}
265+
}
266+
267+
puts JSON.pretty_generate(data)
268+
end
269+
157270
def parse_range_argument
158271
return [nil, nil] if @args.empty?
159272

@@ -203,6 +316,10 @@ def parse_options
203316
options[:no_pager] = true
204317
end
205318

319+
opts.on("--stateless", "Parse manifests directly without database") do
320+
options[:stateless] = true
321+
end
322+
206323
opts.on("-h", "--help", "Show this help") do
207324
puts opts
208325
exit

lib/git/pkgs/commands/list.rb

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@ def initialize(args)
1313

1414
def run
1515
repo = Repository.new
16-
require_database(repo)
16+
use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
1717

18-
Database.connect(repo.git_dir)
19-
20-
commit_sha = @options[:commit] || repo.head_sha
21-
target_commit = Models::Commit.first(sha: commit_sha)
22-
23-
error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
24-
25-
deps = compute_dependencies_at_commit(target_commit, repo)
18+
if use_stateless
19+
deps = run_stateless(repo)
20+
else
21+
deps = run_with_database(repo)
22+
end
2623

2724
# Apply filters
2825
if @options[:manifest]
@@ -50,6 +47,27 @@ def run
5047
end
5148
end
5249

50+
def run_stateless(repo)
51+
commit_sha = @options[:commit] || repo.head_sha
52+
rugged_commit = repo.lookup(repo.rev_parse(commit_sha))
53+
54+
error "Could not resolve '#{commit_sha}'. Check that the ref exists." unless rugged_commit
55+
56+
analyzer = Analyzer.new(repo)
57+
analyzer.dependencies_at_commit(rugged_commit)
58+
end
59+
60+
def run_with_database(repo)
61+
Database.connect(repo.git_dir)
62+
63+
commit_sha = @options[:commit] || repo.head_sha
64+
target_commit = Models::Commit.first(sha: commit_sha)
65+
66+
error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
67+
68+
compute_dependencies_at_commit(target_commit, repo)
69+
end
70+
5371
def output_text(deps)
5472
grouped = deps.group_by { |d| [d[:manifest_path], d[:ecosystem]] }
5573

@@ -157,6 +175,10 @@ def parse_options
157175
options[:no_pager] = true
158176
end
159177

178+
opts.on("--stateless", "Parse manifests directly without database") do
179+
options[:stateless] = true
180+
end
181+
160182
opts.on("-h", "--help", "Show this help") do
161183
puts opts
162184
exit

0 commit comments

Comments
 (0)