Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [Unreleased]

- `git pkgs diff-driver` command for semantic lockfile diffs in `git diff`

## [0.3.0] - 2026-01-03

- Pager support for long output (respects `GIT_PAGER`, `core.pager`, `PAGER`)
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,30 @@ jobs:
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD
```

### Diff driver

Install a git textconv driver that shows semantic dependency changes instead of raw lockfile diffs:

```bash
git pkgs diff-driver --install
```

Now `git diff` on lockfiles shows a sorted dependency list instead of raw lockfile changes:

```diff
diff --git a/Gemfile.lock b/Gemfile.lock
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,3 @@
+kamal 1.0.0
-puma 5.0.0
+puma 6.0.0
rails 7.0.0
-sidekiq 6.0.0
```

Use `git diff --no-textconv` to see the raw lockfile diff. To remove: `git pkgs diff-driver --uninstall`

## Configuration

git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-config).
Expand Down
1 change: 1 addition & 0 deletions lib/git/pkgs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
require_relative "pkgs/commands/log"
require_relative "pkgs/commands/upgrade"
require_relative "pkgs/commands/schema"
require_relative "pkgs/commands/diff_driver"

module Git
module Pkgs
Expand Down
6 changes: 4 additions & 2 deletions lib/git/pkgs/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module Git
module Pkgs
class CLI
COMMANDS = %w[init update hooks info list tree history search why blame stale stats diff branch show log upgrade schema].freeze
COMMANDS = %w[init update hooks info list tree history search why blame stale stats diff branch show log upgrade schema diff-driver].freeze
ALIASES = { "praise" => "blame", "outdated" => "stale" }.freeze

def self.run(args)
Expand Down Expand Up @@ -36,7 +36,9 @@ def run

def run_command(command)
command = ALIASES.fetch(command, command)
command_class = Commands.const_get(command.capitalize.gsub(/_([a-z])/) { $1.upcase })
# Convert kebab-case or snake_case to PascalCase
class_name = command.split(/[-_]/).map(&:capitalize).join
command_class = Commands.const_get(class_name)
command_class.new(@args).run
rescue NameError
$stderr.puts "Command '#{command}' not yet implemented"
Expand Down
169 changes: 169 additions & 0 deletions lib/git/pkgs/commands/diff_driver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# frozen_string_literal: true

require "bibliothecary"

module Git
module Pkgs
module Commands
class DiffDriver
include Output

# Only lockfiles - manifests are human-readable and diff fine normally
LOCKFILE_PATTERNS = %w[
Brewfile.lock.json
Cargo.lock
Cartfile.resolved
Gemfile.lock
Gopkg.lock
Package.resolved
Pipfile.lock
Podfile.lock
Project.lock.json
bun.lock
composer.lock
gems.locked
glide.lock
go.sum
mix.lock
npm-shrinkwrap.json
package-lock.json
packages.lock.json
paket.lock
pnpm-lock.yaml
poetry.lock
project.assets.json
pubspec.lock
pylock.toml
shard.lock
uv.lock
yarn.lock
].freeze

def initialize(args)
@args = args
@options = parse_options
end

def run
if @options[:install]
install_driver
return
end

if @options[:uninstall]
uninstall_driver
return
end

# textconv mode: single file argument, output dependency list
if @args.length == 1
output_textconv(@args[0])
return
end

error "Usage: git pkgs diff-driver <file>"
end

def output_textconv(file_path)
content = read_file(file_path)
deps = parse_deps(file_path, content)

# Output sorted dependency list for git to diff
deps.keys.sort.each do |name|
dep = deps[name]
# Only show type if it's not runtime (the default)
type_suffix = dep[:type] && dep[:type] != "runtime" ? " [#{dep[:type]}]" : ""
puts "#{name} #{dep[:requirement]}#{type_suffix}"
end
end

def install_driver
# Set up git config for textconv
system("git", "config", "diff.pkgs.textconv", "git-pkgs diff-driver")

# Add to .gitattributes
gitattributes_path = File.join(Dir.pwd, ".gitattributes")
existing = File.exist?(gitattributes_path) ? File.read(gitattributes_path) : ""

new_entries = []
LOCKFILE_PATTERNS.each do |pattern|
entry = "#{pattern} diff=pkgs"
new_entries << entry unless existing.include?(entry)
end

if new_entries.any?
File.open(gitattributes_path, "a") do |f|
f.puts unless existing.end_with?("\n") || existing.empty?
f.puts "# git-pkgs textconv for lockfiles"
new_entries.each { |entry| f.puts entry }
end
end

puts "Installed textconv driver for lockfiles."
puts " git config: diff.pkgs.textconv = git-pkgs diff-driver"
puts " .gitattributes: #{new_entries.count} lockfile patterns added"
puts
puts "Now 'git diff' on lockfiles shows dependency changes."
puts "Use 'git diff --no-textconv' to see raw diff."
end

def uninstall_driver
system("git", "config", "--unset", "diff.pkgs.textconv")

gitattributes_path = File.join(Dir.pwd, ".gitattributes")
if File.exist?(gitattributes_path)
lines = File.readlines(gitattributes_path)
lines.reject! { |line| line.include?("diff=pkgs") || line.include?("# git-pkgs") }
File.write(gitattributes_path, lines.join)
end

puts "Uninstalled diff driver."
end

def read_file(path)
return "" if path == "/dev/null"
return "" unless File.exist?(path)

File.read(path)
end

def parse_deps(path, content)
return {} if content.empty?

result = Bibliothecary.analyse_file(path, content).first
return {} unless result

result[:dependencies].map { |d| [d[:name], d] }.to_h
rescue StandardError
{}
end

def parse_options
options = {}

parser = OptionParser.new do |opts|
opts.banner = "Usage: git pkgs diff-driver <file>"
opts.separator ""
opts.separator "Outputs dependency list for git textconv diffing."

opts.on("--install", "Install textconv driver for lockfiles") do
options[:install] = true
end

opts.on("--uninstall", "Uninstall textconv driver") do
options[:uninstall] = true
end

opts.on("-h", "--help", "Show this help") do
puts opts
exit
end
end

parser.parse!(@args)
options
end
end
end
end
end
Loading