git-pkgs 0.7.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f9ff177f3dd7cbb4f5591a0c0f2b936d5fb98629329294dae9f57380a69709e
4
- data.tar.gz: e8760e948aea3b7252176fc6a0b4bb0d74432ad05e4fa20693468b9440b126bd
3
+ metadata.gz: e23d09227a873e67670fa403a6c81c4f93a301cfda036f62d8b02d4d7f1e0ce5
4
+ data.tar.gz: ae4c878fa0cca631cb5d0485ab4a5caa999300ba1afc564c6d23461527a487cf
5
5
  SHA512:
6
- metadata.gz: a670a1b046b7d0953aefe8bd57a6a03fa69624eb3bdb4e8867a647521a88c936e559973b6470989cca45714e79de19c0f6d2a2c85fa7340c3b677e07923cb97a
7
- data.tar.gz: dcbb08683dfe053e70801ae36fe74f3d1f33ce0a6f74a7df3b6cb1a6d5df70d5a7c5db9ca9c7fc8efb6ee94463fbc96a68ef7e09f20808fd6b41b1c2d2e6b873
6
+ metadata.gz: 5a675b67669d345dc39419219adfe51d2c36b1167be446b7f1b937eab42b92c0d935ebd8102e534585760c5944de6059ac722818558090c504758fc4ca7d9388
7
+ data.tar.gz: 7cb84ae314e29ed3cbf0c360ddcd79f43174ce153fca35af476693a6f37a98c84e297b8b3bf3543150e546df2459e69fbcae371959cd4dac04d2aa7d139f1969
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.0] - 2026-01-14
4
+
5
+ - `git pkgs outdated` command to find dependencies with newer versions available in registries
6
+ - `git pkgs licenses` command to check dependency licenses with compliance options (--permissive, --allow, --deny)
7
+ - ecosyste.ms client for fetching package metadata (latest versions, licenses)
8
+ - Package and Version models for storing enrichment data
9
+ - Spinner utility for progress feedback during network operations
10
+ - PURL helper for standardized package URLs
11
+ - `outdated` is no longer an alias for `stale` (now a separate command)
12
+
3
13
  ## [0.7.0] - 2026-01-09
4
14
 
5
15
  - `git pkgs vulns` subcommand for vulnerability scanning via OSV API
data/Formula/git-pkgs.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  class GitPkgs < Formula
2
2
  desc "Track package dependencies across git history"
3
3
  homepage "https://github.com/andrew/git-pkgs"
4
- url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.6.2.tar.gz"
5
- sha256 "ccd7a8a5b9cb21c52cc488923ed1318387a9fefa4baff2057bd96b27591577aa"
4
+ url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.7.0.tar.gz"
5
+ sha256 "5c5aebf75e9570945b324777e5fa33cd5e35d31f6172c2415a4bd91db02477cc"
6
6
  license "AGPL-3.0"
7
7
 
8
8
  depends_on "cmake" => :build
data/README.md CHANGED
@@ -10,7 +10,9 @@ Your lockfile shows what dependencies you have, but it doesn't show how you got
10
10
 
11
11
  For best results, commit your lockfiles. Manifests show version ranges but lockfiles show what actually got installed, including transitive dependencies.
12
12
 
13
- It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem. Everything runs locally and offline with no external services or network calls, and the database lives in your `.git` directory where you can use it in CI to catch dependency changes in pull requests.
13
+ It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem. The database lives in your `.git` directory where you can use it in CI to catch dependency changes in pull requests.
14
+
15
+ The core commands (`list`, `history`, `blame`, `diff`, `stale`, etc.) work entirely from your git history with no network access. Additional commands fetch external data: `vulns` checks [OSV](https://osv.dev) for known CVEs, while `outdated` and `licenses` query [ecosyste.ms](https://packages.ecosyste.ms/) for registry metadata. See [docs/enrichment.md](docs/enrichment.md) for details on external data.
14
16
 
15
17
  ## Installation
16
18
 
@@ -45,6 +47,8 @@ git pkgs history rails # track a specific package
45
47
  git pkgs why rails # why was this added?
46
48
  git pkgs diff --from=HEAD~10 # what changed recently?
47
49
  git pkgs diff --from=main --to=feature # compare branches
50
+ git pkgs vulns # scan for known CVEs
51
+ git pkgs vulns blame # who introduced each vulnerability
48
52
  ```
49
53
 
50
54
  ## Commands
@@ -254,22 +258,69 @@ This shows dependencies grouped by type (runtime, development, etc).
254
258
  git pkgs stale # list deps by how long since last touched
255
259
  git pkgs stale --days=365 # only show deps untouched for a year
256
260
  git pkgs stale --ecosystem=npm # filter by ecosystem
257
- git pkgs outdated # alias for stale
258
261
  ```
259
262
 
260
263
  Shows dependencies sorted by how long since they were last changed in your repo. Useful for finding packages that may have been forgotten or need review.
261
264
 
265
+ ### Find outdated dependencies
266
+
267
+ ```bash
268
+ git pkgs outdated # show packages with newer versions available
269
+ git pkgs outdated --major # only major version updates
270
+ git pkgs outdated --minor # minor and major updates (skip patch)
271
+ git pkgs outdated --stateless # no database needed
272
+ ```
273
+
274
+ Checks package registries (via [ecosyste.ms](https://packages.ecosyste.ms/)) to find dependencies with newer versions available. Major updates are shown in red, minor in yellow, patch in cyan.
275
+
276
+ ### Check licenses
277
+
278
+ ```bash
279
+ git pkgs licenses # show license for each dependency
280
+ git pkgs licenses --permissive # flag copyleft licenses
281
+ git pkgs licenses --allow=MIT,Apache-2.0 # explicit allow list
282
+ git pkgs licenses --group # group output by license
283
+ git pkgs licenses --stateless # no database needed
284
+ ```
285
+
286
+ Fetches license information from package registries. Exits with code 1 if violations are found, making it suitable for CI. See [docs/enrichment.md](docs/enrichment.md) for all options.
287
+
262
288
  ### Vulnerability scanning
263
289
 
290
+ Scan dependencies for known CVEs using the [OSV database](https://osv.dev). Because git-pkgs tracks the full history of every dependency change, it provides context that static scanners can't: who introduced a vulnerability, when it was fixed, and how long you were exposed.
291
+
264
292
  ```bash
265
- git pkgs vulns # scan current dependencies for known CVEs
293
+ git pkgs vulns # scan current dependencies
294
+ git pkgs vulns v1.0.0 # scan at a tag, branch, or commit
266
295
  git pkgs vulns -s high # only critical and high severity
296
+ git pkgs vulns -e npm # filter by ecosystem
297
+ git pkgs vulns -f sarif # output for GitHub code scanning
298
+ ```
299
+
300
+ Subcommands for historical analysis:
301
+
302
+ ```bash
267
303
  git pkgs vulns blame # who introduced each vulnerability
304
+ git pkgs vulns blame --all-time # include fixed vulnerabilities
268
305
  git pkgs vulns praise # who fixed vulnerabilities
269
- git pkgs vulns exposure --all-time --summary # remediation metrics
306
+ git pkgs vulns praise --summary # author leaderboard
307
+ git pkgs vulns exposure # remediation metrics (CRA compliance)
308
+ git pkgs vulns diff main feature # compare vulnerability state between refs
309
+ git pkgs vulns log # commits that introduced or fixed vulns
310
+ git pkgs vulns history lodash # vulnerability timeline for a package
311
+ git pkgs vulns show CVE-2024-1234 # details about a specific CVE
270
312
  ```
271
313
 
272
- Uses the [OSV database](https://osv.dev) to check your dependencies against known security advisories. Because git-pkgs tracks the full history, it can show who introduced and fixed each vulnerability. See [docs/vulns.md](docs/vulns.md) for full documentation.
314
+ Output formats: `text` (default), `json`, and `sarif`. SARIF integrates with GitHub Advanced Security:
315
+
316
+ ```yaml
317
+ - run: git pkgs vulns --stateless -f sarif > results.sarif
318
+ - uses: github/codeql-action/upload-sarif@v3
319
+ with:
320
+ sarif_file: results.sarif
321
+ ```
322
+
323
+ Vulnerability data is cached locally and refreshed automatically when stale (>24h). Use `git pkgs vulns sync --refresh` to force an update. See [docs/vulns.md](docs/vulns.md) for full documentation.
273
324
 
274
325
  ### Diff between commits
275
326
 
@@ -497,6 +548,7 @@ Git::Pkgs::Database.connect(repo_git_dir)
497
548
  Git::Pkgs::Models::DependencyChange.where(name: "rails").all
498
549
  ```
499
550
 
551
+
500
552
  ## Contributing
501
553
 
502
554
  Bug reports, feature requests, and pull requests are welcome. If you're unsure about a change, open an issue first to discuss it.
@@ -33,7 +33,7 @@ module Git
33
33
  REQUIRE Project.toml Manifest.toml
34
34
  shard.yml shard.lock
35
35
  elm-package.json elm_dependencies.json elm-stuff/exact-dependencies.json
36
- haxelib.json
36
+ haxelib.json stack.yaml stack.yaml.lock
37
37
  action.yml action.yaml .github/workflows/*.yml .github/workflows/*.yaml
38
38
  Dockerfile docker-compose*.yml docker-compose*.yaml
39
39
  dvc.yaml vcpkg.json _generated-vcpkg-list.json
data/lib/git/pkgs/cli.rb CHANGED
@@ -33,7 +33,9 @@ module Git
33
33
  },
34
34
  "Analysis" => {
35
35
  "stats" => "Show dependency statistics",
36
- "stale" => "Show dependencies that haven't been updated"
36
+ "stale" => "Show dependencies that haven't been updated",
37
+ "outdated" => "Show packages with newer versions available",
38
+ "licenses" => "Show licenses for dependencies"
37
39
  },
38
40
  "Security" => {
39
41
  "vulns" => "Scan for known vulnerabilities"
@@ -42,7 +44,7 @@ module Git
42
44
 
43
45
  COMMANDS = COMMAND_GROUPS.values.flat_map(&:keys).freeze
44
46
  COMMAND_DESCRIPTIONS = COMMAND_GROUPS.values.reduce({}, :merge).freeze
45
- ALIASES = { "praise" => "blame", "outdated" => "stale" }.freeze
47
+ ALIASES = { "praise" => "blame" }.freeze
46
48
 
47
49
  def self.run(args)
48
50
  new(args).run
@@ -24,18 +24,24 @@ module Git
24
24
  gems.locked
25
25
  glide.lock
26
26
  go.mod
27
+ go.sum
28
+ gradle.lockfile
27
29
  mix.lock
28
30
  npm-shrinkwrap.json
29
31
  package-lock.json
30
32
  packages.lock.json
31
33
  paket.lock
34
+ pdm.lock
32
35
  pnpm-lock.yaml
33
36
  poetry.lock
34
37
  project.assets.json
35
38
  pubspec.lock
36
39
  pylock.toml
40
+ renv.lock
37
41
  shard.lock
42
+ stack.yaml.lock
38
43
  uv.lock
44
+ verification-metadata.xml
39
45
  yarn.lock
40
46
  ].freeze
41
47
 
@@ -0,0 +1,378 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Git
6
+ module Pkgs
7
+ module Commands
8
+ class Licenses
9
+ include Output
10
+
11
+ PERMISSIVE = %w[
12
+ MIT Apache-2.0 BSD-2-Clause BSD-3-Clause ISC Unlicense CC0-1.0
13
+ 0BSD WTFPL Zlib BSL-1.0
14
+ ].freeze
15
+
16
+ COPYLEFT = %w[
17
+ GPL-2.0 GPL-3.0 LGPL-2.1 LGPL-3.0 AGPL-3.0 MPL-2.0
18
+ GPL-2.0-only GPL-2.0-or-later GPL-3.0-only GPL-3.0-or-later
19
+ LGPL-2.1-only LGPL-2.1-or-later LGPL-3.0-only LGPL-3.0-or-later
20
+ AGPL-3.0-only AGPL-3.0-or-later
21
+ ].freeze
22
+
23
+ def self.description
24
+ "Show licenses for dependencies"
25
+ end
26
+
27
+ def initialize(args)
28
+ @args = args.dup
29
+ @options = parse_options
30
+ end
31
+
32
+ def parse_options
33
+ options = { allow: [], deny: [] }
34
+
35
+ parser = OptionParser.new do |opts|
36
+ opts.banner = "Usage: git pkgs licenses [options]"
37
+ opts.separator ""
38
+ opts.separator "Show licenses for dependencies with optional compliance checks."
39
+ opts.separator ""
40
+ opts.separator "Options:"
41
+
42
+ opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
43
+ options[:ecosystem] = v
44
+ end
45
+
46
+ opts.on("-r", "--ref=REF", "Git ref to check (default: HEAD)") do |v|
47
+ options[:ref] = v
48
+ end
49
+
50
+ opts.on("-f", "--format=FORMAT", "Output format (text, json, csv)") do |v|
51
+ options[:format] = v
52
+ end
53
+
54
+ opts.on("--allow=LICENSES", "Comma-separated list of allowed licenses") do |v|
55
+ options[:allow] = v.split(",").map(&:strip)
56
+ end
57
+
58
+ opts.on("--deny=LICENSES", "Comma-separated list of denied licenses") do |v|
59
+ options[:deny] = v.split(",").map(&:strip)
60
+ end
61
+
62
+ opts.on("--permissive", "Only allow permissive licenses (MIT, Apache, BSD, etc.)") do
63
+ options[:permissive] = true
64
+ end
65
+
66
+ opts.on("--copyleft", "Flag copyleft licenses (GPL, AGPL, etc.)") do
67
+ options[:copyleft] = true
68
+ end
69
+
70
+ opts.on("--unknown", "Flag packages with unknown/missing licenses") do
71
+ options[:unknown] = true
72
+ end
73
+
74
+ opts.on("--group", "Group output by license") do
75
+ options[:group] = true
76
+ end
77
+
78
+ opts.on("--stateless", "Parse manifests directly without database") do
79
+ options[:stateless] = true
80
+ end
81
+
82
+ opts.on("-h", "--help", "Show this help") do
83
+ puts opts
84
+ exit
85
+ end
86
+ end
87
+
88
+ parser.parse!(@args)
89
+ options
90
+ end
91
+
92
+ def run
93
+ repo = Repository.new
94
+ use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
95
+
96
+ if use_stateless
97
+ Database.connect_memory
98
+ deps = get_dependencies_stateless(repo)
99
+ else
100
+ Database.connect(repo.git_dir)
101
+ deps = get_dependencies_with_database(repo)
102
+ end
103
+
104
+ if deps.empty?
105
+ empty_result "No dependencies found"
106
+ return
107
+ end
108
+
109
+ if @options[:ecosystem]
110
+ deps = deps.select { |d| d[:ecosystem].downcase == @options[:ecosystem].downcase }
111
+ end
112
+
113
+ deps = Analyzer.pair_manifests_with_lockfiles(deps)
114
+
115
+ if deps.empty?
116
+ empty_result "No dependencies found"
117
+ return
118
+ end
119
+
120
+ packages = deps.map do |dep|
121
+ purl = PurlHelper.build_purl(ecosystem: dep[:ecosystem], name: dep[:name]).to_s
122
+ {
123
+ purl: purl,
124
+ name: dep[:name],
125
+ ecosystem: dep[:ecosystem],
126
+ version: dep[:requirement],
127
+ manifest_path: dep[:manifest_path]
128
+ }
129
+ end.uniq { |p| p[:purl] }
130
+
131
+ enrich_packages(packages.map { |p| p[:purl] })
132
+
133
+ packages.each do |pkg|
134
+ db_pkg = Models::Package.first(purl: pkg[:purl])
135
+ pkg[:license] = db_pkg&.license
136
+ pkg[:violation] = check_violation(pkg[:license])
137
+ end
138
+
139
+ violations = packages.select { |p| p[:violation] }
140
+
141
+ case @options[:format]
142
+ when "json"
143
+ output_json(packages, violations)
144
+ when "csv"
145
+ output_csv(packages)
146
+ else
147
+ if @options[:group]
148
+ output_grouped(packages, violations)
149
+ else
150
+ output_text(packages, violations)
151
+ end
152
+ end
153
+
154
+ exit 1 if violations.any?
155
+ end
156
+
157
+ def check_violation(license)
158
+ return "unknown" if @options[:unknown] && (license.nil? || license.empty?)
159
+
160
+ return nil if license.nil? || license.empty?
161
+
162
+ if @options[:permissive]
163
+ return "copyleft" if COPYLEFT.any? { |l| license_matches?(license, l) }
164
+ return "not-permissive" unless PERMISSIVE.any? { |l| license_matches?(license, l) }
165
+ end
166
+
167
+ if @options[:copyleft]
168
+ return "copyleft" if COPYLEFT.any? { |l| license_matches?(license, l) }
169
+ end
170
+
171
+ if @options[:allow].any?
172
+ return "not-allowed" unless @options[:allow].any? { |l| license_matches?(license, l) }
173
+ end
174
+
175
+ if @options[:deny].any?
176
+ return "denied" if @options[:deny].any? { |l| license_matches?(license, l) }
177
+ end
178
+
179
+ nil
180
+ end
181
+
182
+ def license_matches?(license, pattern)
183
+ license.downcase.include?(pattern.downcase)
184
+ end
185
+
186
+ def enrich_packages(purls)
187
+ packages_by_purl = {}
188
+ purls.each do |purl|
189
+ parsed = Purl::PackageURL.parse(purl)
190
+ ecosystem = PurlHelper::ECOSYSTEM_TO_PURL_TYPE.invert[parsed.type] || parsed.type
191
+ pkg = Models::Package.find_or_create_by_purl(
192
+ purl: purl,
193
+ ecosystem: ecosystem,
194
+ name: parsed.name
195
+ )
196
+ packages_by_purl[purl] = pkg
197
+ end
198
+
199
+ stale_purls = packages_by_purl.select { |_, pkg| pkg.needs_enrichment? }.keys
200
+ return if stale_purls.empty?
201
+
202
+ client = EcosystemsClient.new
203
+ begin
204
+ results = Spinner.with_spinner("Fetching package metadata...") do
205
+ client.bulk_lookup(stale_purls)
206
+ end
207
+ results.each do |purl, data|
208
+ packages_by_purl[purl]&.enrich_from_api(data)
209
+ end
210
+ rescue EcosystemsClient::ApiError => e
211
+ $stderr.puts "Warning: Could not fetch package data: #{e.message}" unless Git::Pkgs.quiet
212
+ end
213
+ end
214
+
215
+ def output_text(packages, violations)
216
+ max_name = packages.map { |p| p[:name].length }.max || 20
217
+ max_license = packages.map { |p| (p[:license] || "").length }.max || 10
218
+ max_license = [max_license, 20].min
219
+
220
+ packages.sort_by { |p| [p[:license] || "zzz", p[:name]] }.each do |pkg|
221
+ name = pkg[:name].ljust(max_name)
222
+ license = (pkg[:license] || "unknown").ljust(max_license)[0, max_license]
223
+ ecosystem = pkg[:ecosystem]
224
+
225
+ line = "#{name} #{license} (#{ecosystem})"
226
+
227
+ colored = if pkg[:violation]
228
+ Color.red("#{line} [#{pkg[:violation]}]")
229
+ else
230
+ line
231
+ end
232
+
233
+ puts colored
234
+ end
235
+
236
+ output_summary(packages, violations)
237
+ end
238
+
239
+ def output_grouped(packages, violations)
240
+ by_license = packages.group_by { |p| p[:license] || "unknown" }
241
+
242
+ by_license.sort_by { |license, _| license.downcase }.each do |license, pkgs|
243
+ has_violation = pkgs.any? { |p| p[:violation] }
244
+ header = "#{license} (#{pkgs.size})"
245
+ puts has_violation ? Color.red(header) : Color.bold(header)
246
+
247
+ pkgs.sort_by { |p| p[:name] }.each do |pkg|
248
+ puts " #{pkg[:name]}"
249
+ end
250
+ puts ""
251
+ end
252
+
253
+ output_summary(packages, violations)
254
+ end
255
+
256
+ def output_summary(packages, violations)
257
+ return unless violations.any?
258
+
259
+ puts ""
260
+ puts Color.red("#{violations.size} license violation#{"s" if violations.size != 1} found")
261
+ end
262
+
263
+ def output_json(packages, violations)
264
+ require "json"
265
+ puts JSON.pretty_generate({
266
+ packages: packages,
267
+ summary: {
268
+ total: packages.size,
269
+ violations: violations.size,
270
+ by_license: packages.group_by { |p| p[:license] || "unknown" }.transform_values(&:size)
271
+ }
272
+ })
273
+ end
274
+
275
+ def output_csv(packages)
276
+ puts "name,ecosystem,version,license,violation"
277
+ packages.sort_by { |p| p[:name] }.each do |pkg|
278
+ puts [
279
+ pkg[:name],
280
+ pkg[:ecosystem],
281
+ pkg[:version],
282
+ pkg[:license] || "",
283
+ pkg[:violation] || ""
284
+ ].map { |v| csv_escape(v) }.join(",")
285
+ end
286
+ end
287
+
288
+ def csv_escape(value)
289
+ if value.to_s.include?(",") || value.to_s.include?('"')
290
+ "\"#{value.to_s.gsub('"', '""')}\""
291
+ else
292
+ value.to_s
293
+ end
294
+ end
295
+
296
+ def get_dependencies_stateless(repo)
297
+ ref = @options[:ref] || "HEAD"
298
+ commit_sha = repo.rev_parse(ref)
299
+ rugged_commit = repo.lookup(commit_sha)
300
+
301
+ error "Could not resolve '#{ref}'" unless rugged_commit
302
+
303
+ analyzer = Analyzer.new(repo)
304
+ analyzer.dependencies_at_commit(rugged_commit)
305
+ end
306
+
307
+ def get_dependencies_with_database(repo)
308
+ ref = @options[:ref] || "HEAD"
309
+ commit_sha = repo.rev_parse(ref)
310
+ target_commit = Models::Commit.first(sha: commit_sha)
311
+
312
+ return get_dependencies_stateless(repo) unless target_commit
313
+
314
+ branch_name = repo.default_branch
315
+ branch = Models::Branch.first(name: branch_name)
316
+ return [] unless branch
317
+
318
+ compute_dependencies_at_commit(target_commit, branch)
319
+ end
320
+
321
+ def compute_dependencies_at_commit(target_commit, branch)
322
+ snapshot_commit = branch.commits_dataset
323
+ .join(:dependency_snapshots, commit_id: :id)
324
+ .where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
325
+ .order(Sequel.desc(Sequel[:commits][:committed_at]))
326
+ .distinct
327
+ .first
328
+
329
+ deps = {}
330
+ if snapshot_commit
331
+ snapshot_commit.dependency_snapshots.each do |s|
332
+ key = [s.manifest.path, s.name]
333
+ deps[key] = {
334
+ manifest_path: s.manifest.path,
335
+ manifest_kind: s.manifest.kind,
336
+ name: s.name,
337
+ ecosystem: s.ecosystem,
338
+ requirement: s.requirement,
339
+ dependency_type: s.dependency_type
340
+ }
341
+ end
342
+ end
343
+
344
+ if snapshot_commit && snapshot_commit.id != target_commit.id
345
+ commit_ids = branch.commits_dataset.select_map(Sequel[:commits][:id])
346
+ changes = Models::DependencyChange
347
+ .join(:commits, id: :commit_id)
348
+ .where(Sequel[:commits][:id] => commit_ids)
349
+ .where { Sequel[:commits][:committed_at] > snapshot_commit.committed_at }
350
+ .where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
351
+ .order(Sequel[:commits][:committed_at])
352
+ .eager(:manifest)
353
+ .all
354
+
355
+ changes.each do |change|
356
+ key = [change.manifest.path, change.name]
357
+ case change.change_type
358
+ when "added", "modified"
359
+ deps[key] = {
360
+ manifest_path: change.manifest.path,
361
+ manifest_kind: change.manifest.kind,
362
+ name: change.name,
363
+ ecosystem: change.ecosystem,
364
+ requirement: change.requirement,
365
+ dependency_type: change.dependency_type
366
+ }
367
+ when "removed"
368
+ deps.delete(key)
369
+ end
370
+ end
371
+ end
372
+
373
+ deps.values
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end