git-pkgs 0.3.0 → 0.5.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.
data/README.md CHANGED
@@ -43,7 +43,7 @@ Options:
43
43
  - `--branch=NAME` - analyze a specific branch (default: default branch)
44
44
  - `--since=SHA` - start analysis from a specific commit
45
45
  - `--force` - rebuild the database from scratch
46
- - `--hooks` - install git hooks for auto-updating
46
+ - `--no-hooks` - skip installing git hooks (hooks are installed by default)
47
47
 
48
48
  Example output:
49
49
  ```
@@ -52,7 +52,7 @@ Processing commit 5191/5191...
52
52
  Done!
53
53
  Analyzed 5191 commits
54
54
  Found 2531 commits with dependency changes
55
- Stored 28239 snapshots (every 20 changes)
55
+ Stored 28239 snapshots (every 50 changes)
56
56
  Blob cache: 3141 unique blobs, 2349 had cache hits
57
57
  ```
58
58
 
@@ -86,7 +86,7 @@ Snapshot Coverage
86
86
  ----------------------------------------
87
87
  Commits with dependency changes: 2531
88
88
  Commits with snapshots: 127
89
- Coverage: 5.0% (1 snapshot per ~20 changes)
89
+ Coverage: 2.0% (1 snapshot per ~50 changes)
90
90
  ```
91
91
 
92
92
  ### List dependencies
@@ -258,6 +258,23 @@ git pkgs show HEAD~5 # relative ref
258
258
 
259
259
  Like `git show` but for dependencies. Shows what was added, modified, or removed in a single commit.
260
260
 
261
+ ### Find where a package is declared
262
+
263
+ ```bash
264
+ git pkgs where rails # find in manifest files
265
+ git pkgs where lodash -C 2 # show 2 lines of context
266
+ git pkgs where express --ecosystem=npm
267
+ ```
268
+
269
+ Shows which manifest files declare a package and the exact line:
270
+
271
+ ```
272
+ Gemfile:5:gem "rails", "~> 7.0"
273
+ Gemfile.lock:142: rails (7.0.8)
274
+ ```
275
+
276
+ Like `grep` but scoped to manifest files that git-pkgs knows about.
277
+
261
278
  ### List commits with dependency changes
262
279
 
263
280
  ```bash
@@ -270,16 +287,18 @@ Like `git log` but only shows commits that changed dependencies, with the change
270
287
 
271
288
  ### Keep database updated
272
289
 
273
- After the initial analysis, you can incrementally update the database with new commits:
290
+ After the initial analysis, the database updates automatically via git hooks installed during init. You can also update manually:
274
291
 
275
292
  ```bash
276
293
  git pkgs update
277
294
  ```
278
295
 
279
- You can also install git hooks to update automatically after commits and merges:
296
+ To manage hooks separately:
280
297
 
281
298
  ```bash
282
- git pkgs hooks --install
299
+ git pkgs hooks # show hook status
300
+ git pkgs hooks --install # install hooks
301
+ git pkgs hooks --uninstall # remove hooks
283
302
  ```
284
303
 
285
304
  ### Upgrading
@@ -328,6 +347,45 @@ jobs:
328
347
  - run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD
329
348
  ```
330
349
 
350
+ ### Diff driver
351
+
352
+ Install a git textconv driver that shows semantic dependency changes instead of raw lockfile diffs:
353
+
354
+ ```bash
355
+ git pkgs diff-driver --install
356
+ ```
357
+
358
+ Now `git diff` on lockfiles shows a sorted dependency list instead of raw lockfile changes:
359
+
360
+ ```diff
361
+ diff --git a/Gemfile.lock b/Gemfile.lock
362
+ --- a/Gemfile.lock
363
+ +++ b/Gemfile.lock
364
+ @@ -1,3 +1,3 @@
365
+ +kamal 1.0.0
366
+ -puma 5.0.0
367
+ +puma 6.0.0
368
+ rails 7.0.0
369
+ -sidekiq 6.0.0
370
+ ```
371
+
372
+ Use `git diff --no-textconv` to see the raw lockfile diff. To remove: `git pkgs diff-driver --uninstall`
373
+
374
+ ### Shell completions
375
+
376
+ Enable tab completion for commands:
377
+
378
+ ```bash
379
+ # Bash: add to ~/.bashrc
380
+ eval "$(git pkgs completions bash)"
381
+
382
+ # Zsh: add to ~/.zshrc
383
+ eval "$(git pkgs completions zsh)"
384
+
385
+ # Or auto-install to standard completion directories
386
+ git pkgs completions install
387
+ ```
388
+
331
389
  ## Configuration
332
390
 
333
391
  git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-config).
@@ -336,6 +394,21 @@ git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-conf
336
394
 
337
395
  **Pager** follows git's precedence: `GIT_PAGER` env, `core.pager` config, `PAGER` env, then `less -FRSX`. Use `--no-pager` flag or `git config core.pager cat` to disable.
338
396
 
397
+ **Ecosystem filtering** lets you limit which package ecosystems are tracked:
398
+
399
+ ```bash
400
+ git config --add pkgs.ecosystems rubygems
401
+ git config --add pkgs.ecosystems npm
402
+ git pkgs info --ecosystems # show enabled/disabled ecosystems
403
+ ```
404
+
405
+ **Ignored paths** let you skip directories or files from analysis:
406
+
407
+ ```bash
408
+ git config --add pkgs.ignoredDirs third_party
409
+ git config --add pkgs.ignoredFiles test/fixtures/package.json
410
+ ```
411
+
339
412
  **Environment variables:**
340
413
 
341
414
  - `GIT_DIR` - git directory location (standard git variable)
@@ -343,55 +416,32 @@ git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-conf
343
416
 
344
417
  ## Performance
345
418
 
346
- Benchmarked on a MacBook Pro analyzing [octobox](https://github.com/octobox/octobox) (5191 commits, 8 years of history): init takes about 18 seconds at roughly 300 commits/sec, producing an 8.3 MB database. About half the commits (2531) had dependency changes.
419
+ Benchmarked on an M1 MacBook Pro analyzing [octobox](https://github.com/octobox/octobox) (5191 commits, 8 years of history): init takes about 18 seconds at roughly 300 commits/sec, producing an 8.3 MB database. About half the commits (2531) had dependency changes.
347
420
 
348
421
  Optimizations:
349
- - Bulk inserts with transaction batching (100 commits per transaction)
422
+ - Bulk inserts with transaction batching (500 commits per transaction)
350
423
  - Blob SHA caching (75% cache hit rate for repeated manifest content)
351
424
  - Deferred index creation during bulk load
352
- - Sparse snapshots (every 20 dependency-changing commits) for storage efficiency
425
+ - Sparse snapshots (every 50 dependency-changing commits) for storage efficiency
353
426
  - SQLite WAL mode for write performance
354
427
 
355
428
  ## Supported ecosystems
356
429
 
357
430
  git-pkgs uses [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary) for parsing, supporting:
358
431
 
359
- Actions, Anaconda, BentoML, Bower, Cargo, CocoaPods, Cog, CPAN, CRAN, CycloneDX, Docker, Dub, DVC, Elm, Go, Haxelib, Homebrew, Julia, Maven, Meteor, MLflow, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, SPDX, Vcpkg
432
+ Actions, BentoML, Bower, Cargo, CocoaPods, Cog, Conda, CPAN, CRAN, Docker, Dub, DVC, Elm, Go, Haxelib, Homebrew, Julia, Maven, Meteor, MLflow, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, Vcpkg
360
433
 
361
- ## How it works
434
+ Some ecosystems require remote parsing services and are disabled by default: Carthage, Clojars, Hackage, Hex, SwiftPM. Enable with `git config --add pkgs.ecosystems <name>`.
362
435
 
363
- git-pkgs walks your git history, extracts dependency files at each commit, and diffs them to detect changes. Results are stored in a SQLite database for fast querying.
436
+ SBOM formats (CycloneDX, SPDX) are not supported as they duplicate information from the actual lockfiles.
364
437
 
365
- The database schema stores:
366
- - Commits with dependency changes
367
- - Dependency changes (added/modified/removed) with before/after versions
368
- - Periodic snapshots of full dependency state for efficient point-in-time queries
438
+ ## Contributing
369
439
 
370
- See [docs/schema.md](docs/schema.md) for full schema documentation.
440
+ Bug reports, feature requests, and pull requests are welcome. If you're unsure about a change, open an issue first to discuss it.
371
441
 
372
- Since the database is just SQLite, you can query it directly for ad-hoc analysis:
442
+ Good first contributions: adding tests, improving error messages, or supporting new manifest formats via [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary).
373
443
 
374
- ```bash
375
- sqlite3 .git/pkgs.sqlite3 "
376
- -- who added the most dependencies?
377
- SELECT c.author_name, COUNT(*) as deps_added
378
- FROM dependency_changes dc
379
- JOIN commits c ON dc.commit_id = c.id
380
- WHERE dc.change_type = 'added'
381
- GROUP BY c.author_name
382
- ORDER BY deps_added DESC
383
- LIMIT 10;
384
- "
385
- ```
386
-
387
- ## Development
388
-
389
- ```bash
390
- git clone https://github.com/andrew/git-pkgs
391
- cd git-pkgs
392
- bin/setup
393
- rake test
394
- ```
444
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and architecture docs.
395
445
 
396
446
  ## License
397
447
 
@@ -24,8 +24,6 @@ module Git
24
24
  Podfile Podfile.lock *.podspec *.podspec.json
25
25
  packages.config packages.lock.json Project.json Project.lock.json
26
26
  *.nuspec paket.lock *.csproj project.assets.json
27
- cyclonedx.xml cyclonedx.json *.cdx.xml *.cdx.json
28
- *.spdx *.spdx.json
29
27
  bower.json bentofile.yaml
30
28
  META.json META.yml
31
29
  environment.yml environment.yaml
@@ -46,7 +44,7 @@ module Git
46
44
  QUICK_MANIFEST_REGEX = Regexp.union(
47
45
  QUICK_MANIFEST_PATTERNS.map do |pattern|
48
46
  if pattern.include?('*')
49
- Regexp.new(pattern.gsub('.', '\\.').gsub('*', '.*'))
47
+ Regexp.new(Regexp.escape(pattern).gsub('\\*', '.*'))
50
48
  else
51
49
  /(?:^|\/)#{Regexp.escape(pattern)}$/
52
50
  end
@@ -56,6 +54,8 @@ module Git
56
54
  def initialize(repository)
57
55
  @repository = repository
58
56
  @blob_cache = {}
57
+ @manifest_path_cache = {}
58
+ Config.configure_bibliothecary
59
59
  end
60
60
 
61
61
  # Quick check if any paths might be manifests (fast regex check)
@@ -63,6 +63,23 @@ module Git
63
63
  paths.any? { |p| p.match?(QUICK_MANIFEST_REGEX) }
64
64
  end
65
65
 
66
+ # Cached version of Bibliothecary.identify_manifests
67
+ def identify_manifests_cached(paths)
68
+ uncached = paths.reject { |p| @manifest_path_cache.key?(p) }
69
+
70
+ if uncached.any?
71
+ # Call Bibliothecary only for uncached paths
72
+ manifests = Bibliothecary.identify_manifests(uncached)
73
+ manifest_set = manifests.to_set
74
+
75
+ uncached.each do |path|
76
+ @manifest_path_cache[path] = manifest_set.include?(path)
77
+ end
78
+ end
79
+
80
+ paths.select { |p| @manifest_path_cache[p] }
81
+ end
82
+
66
83
  # Quick check if a commit touches any manifest files
67
84
  def has_manifest_changes?(rugged_commit)
68
85
  return false if repository.merge_commit?(rugged_commit)
@@ -72,7 +89,7 @@ module Git
72
89
 
73
90
  return false unless might_have_manifests?(all_paths)
74
91
 
75
- Bibliothecary.identify_manifests(all_paths).any?
92
+ identify_manifests_cached(all_paths).any?
76
93
  end
77
94
 
78
95
  def analyze_commit(rugged_commit, previous_snapshot = {})
@@ -87,9 +104,9 @@ module Git
87
104
  all_paths = added_paths + modified_paths + removed_paths
88
105
  return nil unless might_have_manifests?(all_paths)
89
106
 
90
- added_manifests = Bibliothecary.identify_manifests(added_paths)
91
- modified_manifests = Bibliothecary.identify_manifests(modified_paths)
92
- removed_manifests = Bibliothecary.identify_manifests(removed_paths)
107
+ added_manifests = identify_manifests_cached(added_paths)
108
+ modified_manifests = identify_manifests_cached(modified_paths)
109
+ removed_manifests = identify_manifests_cached(removed_paths)
93
110
 
94
111
  return nil if added_manifests.empty? && modified_manifests.empty? && removed_manifests.empty?
95
112
 
@@ -99,7 +116,7 @@ module Git
99
116
  # Process added manifest files
100
117
  added_manifests.each do |manifest_path|
101
118
  result = parse_manifest_at_commit(rugged_commit, manifest_path)
102
- next unless result
119
+ next unless result && result[:dependencies]
103
120
 
104
121
  result[:dependencies].each do |dep|
105
122
  changes << {
@@ -203,7 +220,7 @@ module Git
203
220
  # Process removed manifest files
204
221
  removed_manifests.each do |manifest_path|
205
222
  result = parse_manifest_before_commit(rugged_commit, manifest_path)
206
- next unless result
223
+ next unless result && result[:dependencies]
207
224
 
208
225
  result[:dependencies].each do |dep|
209
226
  changes << {
@@ -229,9 +246,14 @@ module Git
229
246
 
230
247
  # Cache stats for debugging
231
248
  def cache_stats
232
- hits = @blob_cache.values.count { |v| v[:hits] > 0 }
233
- total = @blob_cache.size
234
- { cached_blobs: total, blobs_with_hits: hits }
249
+ blob_hits = @blob_cache.values.count { |v| v[:hits] > 0 }
250
+ blob_total = @blob_cache.size
251
+ manifest_paths = @manifest_path_cache.size
252
+ {
253
+ cached_blobs: blob_total,
254
+ blobs_with_hits: blob_hits,
255
+ cached_paths: manifest_paths
256
+ }
235
257
  end
236
258
 
237
259
  def parse_manifest_at_commit(rugged_commit, manifest_path)
@@ -251,7 +273,7 @@ module Git
251
273
  end
252
274
 
253
275
  def parse_manifest_by_oid(blob_oid, manifest_path)
254
- cache_key = "#{blob_oid}:#{manifest_path}"
276
+ cache_key = [blob_oid, manifest_path]
255
277
 
256
278
  if @blob_cache.key?(cache_key)
257
279
  @blob_cache[cache_key][:hits] += 1
@@ -262,6 +284,7 @@ module Git
262
284
  return nil unless content
263
285
 
264
286
  result = Bibliothecary.analyse_file(manifest_path, content).first
287
+ result = nil if result && Config.filter_ecosystem?(result[:platform])
265
288
  @blob_cache[cache_key] = { result: result, hits: 0 }
266
289
  result
267
290
  end
data/lib/git/pkgs/cli.rb CHANGED
@@ -5,7 +5,40 @@ require "optparse"
5
5
  module Git
6
6
  module Pkgs
7
7
  class CLI
8
- COMMANDS = %w[init update hooks info list tree history search why blame stale stats diff branch show log upgrade schema].freeze
8
+ COMMAND_GROUPS = {
9
+ "Setup" => {
10
+ "init" => "Initialize the package database for this repository",
11
+ "update" => "Update the database with new commits",
12
+ "hooks" => "Manage git hooks for auto-updating",
13
+ "upgrade" => "Upgrade database after git-pkgs update",
14
+ "info" => "Show database size and row counts",
15
+ "branch" => "Manage tracked branches",
16
+ "schema" => "Show database schema",
17
+ "diff-driver" => "Install git textconv driver for lockfile diffs",
18
+ "completions" => "Generate shell completions"
19
+ },
20
+ "Query" => {
21
+ "list" => "List dependencies at a commit",
22
+ "tree" => "Show dependency tree grouped by type",
23
+ "search" => "Find a dependency across all history",
24
+ "where" => "Show where a package appears in manifest files",
25
+ "why" => "Explain why a dependency exists"
26
+ },
27
+ "History" => {
28
+ "history" => "Show the history of a package",
29
+ "blame" => "Show who added each dependency",
30
+ "log" => "List commits with dependency changes",
31
+ "show" => "Show dependency changes in a commit",
32
+ "diff" => "Show dependency changes between commits"
33
+ },
34
+ "Analysis" => {
35
+ "stats" => "Show dependency statistics",
36
+ "stale" => "Show dependencies that haven't been updated"
37
+ }
38
+ }.freeze
39
+
40
+ COMMANDS = COMMAND_GROUPS.values.flat_map(&:keys).freeze
41
+ COMMAND_DESCRIPTIONS = COMMAND_GROUPS.values.reduce({}, :merge).freeze
9
42
  ALIASES = { "praise" => "blame", "outdated" => "stale" }.freeze
10
43
 
11
44
  def self.run(args)
@@ -18,6 +51,9 @@ module Git
18
51
  end
19
52
 
20
53
  def run
54
+ Git::Pkgs.configure_from_env
55
+ parse_global_options
56
+
21
57
  command = @args.shift
22
58
 
23
59
  case command
@@ -34,45 +70,66 @@ module Git
34
70
  end
35
71
  end
36
72
 
73
+ def parse_global_options
74
+ while @args.first&.start_with?("-")
75
+ arg = @args.first
76
+ case arg
77
+ when "-q", "--quiet"
78
+ Git::Pkgs.quiet = true
79
+ @args.shift
80
+ when /^--git-dir=(.+)$/
81
+ Git::Pkgs.git_dir = $1
82
+ @args.shift
83
+ when "--git-dir"
84
+ @args.shift
85
+ Git::Pkgs.git_dir = @args.shift
86
+ when /^--work-tree=(.+)$/
87
+ Git::Pkgs.work_tree = $1
88
+ @args.shift
89
+ when "--work-tree"
90
+ @args.shift
91
+ Git::Pkgs.work_tree = @args.shift
92
+ else
93
+ break
94
+ end
95
+ end
96
+ end
97
+
37
98
  def run_command(command)
38
99
  command = ALIASES.fetch(command, command)
39
- command_class = Commands.const_get(command.capitalize.gsub(/_([a-z])/) { $1.upcase })
100
+ # Convert kebab-case or snake_case to PascalCase
101
+ class_name = command.split(/[-_]/).map(&:capitalize).join
102
+ command_class = Commands.const_get(class_name)
40
103
  command_class.new(@args).run
41
- rescue NameError
104
+ rescue NameError => e
105
+ # Only catch NameError for missing command class, not NoMethodError
106
+ raise unless e.is_a?(NameError) && !e.is_a?(NoMethodError)
42
107
  $stderr.puts "Command '#{command}' not yet implemented"
43
108
  exit 1
44
109
  end
45
110
 
46
111
  def print_help
47
- puts <<~HELP
48
- Usage: git pkgs <command> [options]
112
+ puts "Usage: git pkgs <command> [options]"
113
+ puts
49
114
 
50
- Commands:
51
- init Initialize the package database for this repository
52
- update Update the database with new commits
53
- hooks Manage git hooks for auto-updating
54
- info Show database size and row counts
55
- branch Manage tracked branches
56
- list List dependencies at a commit
57
- tree Show dependency tree grouped by type
58
- history Show the history of a package
59
- search Find a dependency across all history
60
- why Explain why a dependency exists
61
- blame Show who added each dependency
62
- stale Show dependencies that haven't been updated
63
- stats Show dependency statistics
64
- diff Show dependency changes between commits
65
- show Show dependency changes in a commit
66
- log List commits with dependency changes
67
- upgrade Upgrade database after git-pkgs update
68
- schema Show database schema
115
+ max_cmd_len = COMMANDS.map(&:length).max
69
116
 
70
- Options:
71
- -h, --help Show this help message
72
- -v, --version Show version
117
+ COMMAND_GROUPS.each do |group, commands|
118
+ puts "#{group}:"
119
+ commands.each do |cmd, desc|
120
+ puts " #{cmd.ljust(max_cmd_len)} #{desc}"
121
+ end
122
+ puts
123
+ end
73
124
 
74
- Run 'git pkgs <command> --help' for command-specific options.
75
- HELP
125
+ puts "Options:"
126
+ puts " -h, --help Show this help message"
127
+ puts " -v, --version Show version"
128
+ puts " -q, --quiet Suppress informational messages"
129
+ puts " --git-dir=<path> Path to the git directory"
130
+ puts " --work-tree=<path> Path to the working tree"
131
+ puts
132
+ puts "Run 'git pkgs <command> -h' for command-specific options."
76
133
  end
77
134
  end
78
135
  end
@@ -74,6 +74,7 @@ module Git
74
74
  def self.green(text) = colorize(text, :green)
75
75
  def self.yellow(text) = colorize(text, :yellow)
76
76
  def self.blue(text) = colorize(text, :blue)
77
+ def self.magenta(text) = colorize(text, :magenta)
77
78
  def self.cyan(text) = colorize(text, :cyan)
78
79
  def self.bold(text) = colorize(text, :bold)
79
80
  def self.dim(text) = colorize(text, :dim)
@@ -21,7 +21,7 @@ module Git
21
21
  branch_name = @options[:branch] || repo.default_branch
22
22
  branch = Models::Branch.find_by(name: branch_name)
23
23
 
24
- error "No analysis found for branch '#{branch_name}'" unless branch&.last_analyzed_sha
24
+ error "No analysis found for branch '#{branch_name}'. Run 'git pkgs init' first." unless branch&.last_analyzed_sha
25
25
 
26
26
  current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
27
27
  snapshots = current_commit&.dependency_snapshots&.includes(:manifest) || []
@@ -35,16 +35,34 @@ module Git
35
35
  return
36
36
  end
37
37
 
38
+ # Batch fetch all "added" changes for current dependencies
39
+ snapshot_keys = snapshots.map { |s| [s.name, s.manifest_id] }.to_set
40
+ manifest_ids = snapshots.map(&:manifest_id).uniq
41
+ names = snapshots.map(&:name).uniq
42
+
43
+ all_added_changes = Models::DependencyChange
44
+ .includes(:commit)
45
+ .added
46
+ .where(manifest_id: manifest_ids, name: names)
47
+ .to_a
48
+
49
+ # Group by (name, manifest_id) and find earliest by committed_at
50
+ added_by_key = {}
51
+ all_added_changes.each do |change|
52
+ key = [change.name, change.manifest_id]
53
+ next unless snapshot_keys.include?(key)
54
+
55
+ existing = added_by_key[key]
56
+ if existing.nil? || change.commit.committed_at < existing.commit.committed_at
57
+ added_by_key[key] = change
58
+ end
59
+ end
60
+
38
61
  # For each current dependency, find who added it
39
62
  blame_data = []
40
63
 
41
64
  snapshots.each do |snapshot|
42
- added_change = Models::DependencyChange
43
- .includes(:commit)
44
- .where(name: snapshot.name, manifest: snapshot.manifest)
45
- .added
46
- .order("commits.committed_at ASC")
47
- .first
65
+ added_change = added_by_key[[snapshot.name, snapshot.manifest_id]]
48
66
 
49
67
  next unless added_change
50
68
 
@@ -101,7 +119,7 @@ module Git
101
119
  def parse_coauthors(message)
102
120
  return [] unless message
103
121
 
104
- message.scan(/^Co-authored-by:\s*(.+?)\s*<[^>]+>/i).flatten
122
+ message.scan(/^Co-authored-by:([^<]+)<[^>]+>/i).flatten.map(&:strip)
105
123
  end
106
124
 
107
125
  def bot_author?(name)
@@ -6,8 +6,16 @@ module Git
6
6
  class Branch
7
7
  include Output
8
8
 
9
- BATCH_SIZE = 100
10
- SNAPSHOT_INTERVAL = 20
9
+ DEFAULT_BATCH_SIZE = 500
10
+ DEFAULT_SNAPSHOT_INTERVAL = 50
11
+
12
+ def batch_size
13
+ Git::Pkgs.batch_size || DEFAULT_BATCH_SIZE
14
+ end
15
+
16
+ def snapshot_interval
17
+ Git::Pkgs.snapshot_interval || DEFAULT_SNAPSHOT_INTERVAL
18
+ end
11
19
 
12
20
  def initialize(args)
13
21
  @args = args
@@ -40,12 +48,12 @@ module Git
40
48
 
41
49
  Database.connect(repo.git_dir)
42
50
 
43
- error "Branch '#{branch_name}' not found" unless repo.branch_exists?(branch_name)
51
+ error "Branch '#{branch_name}' not found. Check 'git branch -a' for available branches." unless repo.branch_exists?(branch_name)
44
52
 
45
53
  existing = Models::Branch.find_by(name: branch_name)
46
54
  if existing
47
- puts "Branch '#{branch_name}' already tracked (#{existing.commits.count} commits)"
48
- puts "Use 'git pkgs update' to refresh"
55
+ info "Branch '#{branch_name}' already tracked (#{existing.commits.count} commits)"
56
+ info "Use 'git pkgs update' to refresh"
49
57
  return
50
58
  end
51
59
 
@@ -54,11 +62,15 @@ module Git
54
62
  branch = Models::Branch.create!(name: branch_name)
55
63
  analyzer = Analyzer.new(repo)
56
64
 
57
- puts "Analyzing branch: #{branch_name}"
65
+ info "Analyzing branch: #{branch_name}"
58
66
 
67
+ print "Loading commits..." unless Git::Pkgs.quiet
59
68
  walker = repo.walk(branch_name)
60
69
  commits = walker.to_a
61
70
  total = commits.size
71
+ print "\rPrefetching diffs..." unless Git::Pkgs.quiet
72
+ repo.prefetch_blob_paths(commits)
73
+ print "\r#{' ' * 20}\r" unless Git::Pkgs.quiet
62
74
 
63
75
  stats = bulk_process_commits(commits, branch, analyzer, total, repo)
64
76
 
@@ -66,10 +78,10 @@ module Git
66
78
 
67
79
  Database.optimize_for_reads
68
80
 
69
- puts "\rDone!#{' ' * 20}"
70
- puts "Analyzed #{total} commits"
71
- puts "Found #{stats[:dependency_commits]} commits with dependency changes"
72
- puts "Stored #{stats[:snapshots_stored]} snapshots"
81
+ info "\rDone!#{' ' * 20}"
82
+ info "Analyzed #{total} commits"
83
+ info "Found #{stats[:dependency_commits]} commits with dependency changes"
84
+ info "Stored #{stats[:snapshots_stored]} snapshots"
73
85
  end
74
86
 
75
87
  def list_branches
@@ -104,14 +116,14 @@ module Git
104
116
  Database.connect(repo.git_dir)
105
117
 
106
118
  branch = Models::Branch.find_by(name: branch_name)
107
- error "Branch '#{branch_name}' not tracked" unless branch
119
+ error "Branch '#{branch_name}' not tracked. Run 'git pkgs branch list' to see tracked branches." unless branch
108
120
 
109
121
  # Only delete branch_commits, keep shared commits
110
122
  count = branch.branch_commits.count
111
123
  branch.branch_commits.delete_all
112
124
  branch.destroy
113
125
 
114
- puts "Removed branch '#{branch_name}' (#{count} branch-commit links)"
126
+ info "Removed branch '#{branch_name}' (#{count} branch-commit links)"
115
127
  end
116
128
 
117
129
  def bulk_process_commits(commits, branch, analyzer, total, repo)
@@ -196,7 +208,7 @@ module Git
196
208
 
197
209
  commits.each do |rugged_commit|
198
210
  processed += 1
199
- print "\rProcessing commit #{processed}/#{total}..." if processed % 50 == 0 || processed == total
211
+ print "\rProcessing commit #{processed}/#{total}..." if !Git::Pkgs.quiet && (processed % 50 == 0 || processed == total)
200
212
 
201
213
  next if rugged_commit.parents.length > 1
202
214
 
@@ -247,7 +259,7 @@ module Git
247
259
 
248
260
  snapshot = result[:snapshot]
249
261
 
250
- if dependency_commit_count % SNAPSHOT_INTERVAL == 0
262
+ if dependency_commit_count % snapshot_interval == 0
251
263
  snapshot.each do |(manifest_path, name), dep_info|
252
264
  pending_snapshots << {
253
265
  sha: rugged_commit.oid,
@@ -262,7 +274,7 @@ module Git
262
274
  end
263
275
  end
264
276
 
265
- flush.call if pending_commits.size >= BATCH_SIZE
277
+ flush.call if pending_commits.size >= batch_size
266
278
  end
267
279
 
268
280
  if snapshot.any?