swhid 0.2.1 → 0.3.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: 78a2795583924a21b4aae1a173e528af68805dd89ad44e6c3db32b534ca55653
4
- data.tar.gz: 1a65c99bade2ae1c4f00c69d2b49a31708da9fc8f15ee5c63d7d9806c876b7e5
3
+ metadata.gz: 17e5d6a13e7f6d44bed90224e6ff6b1418bd9501b7bd14c1ef00d5d7adbbeb3c
4
+ data.tar.gz: 712d0c4136e0ac2c5d11b38b73cbf77d75bbf87b188fc998a3f716e4f9e8ff98
5
5
  SHA512:
6
- metadata.gz: e9d04af133af2dca2b749ce7259a792dbed4622b7bfc587cc08be99d87e109d50be5926e0db2b653b3da7d0e74454fac3e4a9709b12fcd9306f4be20583eab97
7
- data.tar.gz: 24affb1c2fa578ab1211b54540a3d2a51497e583b70804cf837c8999a2c8beb3c3546af6f0de35fe2743e26918fc775baf9f6ec0e75fad0d2d116b10730a412b
6
+ metadata.gz: 7fa9985d872d26ade840cdb509319fe27795dbd7bbac18b9a4141eb291dcee55cdeb5be31ad94a2fd557b2d676124016c955303bee9a7b4dac1ff698fc22ddc9
7
+ data.tar.gz: e99b1253233fd0862a7b57da14ca6367036e5cb50b4d153ffe154165be2635bf8f7579a5a8d3ae10757972dfc511d591c9485bf66cca001db8336b5dc08d4349
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-11-23
4
+
5
+ - `directory` CLI command - Generate SWHID for directory from filesystem
6
+ - `revision` CLI command - Generate SWHID for git commit/revision
7
+ - `release` CLI command - Generate SWHID for git tag/release
8
+ - `snapshot` CLI command - Generate SWHID for git repository snapshot
9
+
3
10
  ## [0.2.1] - 2025-11-09
4
11
 
5
12
  - Package manager tests for PyPI, RubyGems, Maven, Cargo, and NPM artifacts (content and extracted directories)
data/README.md CHANGED
@@ -151,12 +151,13 @@ swhid = Swhid::Identifier.new(
151
151
  visit: "swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9",
152
152
  anchor: "swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0",
153
153
  path: "/src/main.rb",
154
- lines: "10-20"
154
+ lines: "10-20",
155
+ bytes: "0-100"
155
156
  }
156
157
  )
157
158
 
158
159
  puts swhid.to_s
159
- # => "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2;origin=https://github.com/example/repo;visit=swh:1:snp:...;anchor=swh:1:rev:...;path=/src/main.rb;lines=10-20"
160
+ # => "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2;origin=https://github.com/example/repo;visit=swh:1:snp:...;anchor=swh:1:rev:...;path=/src/main.rb;lines=10-20;bytes=0-100"
160
161
  ```
161
162
 
162
163
  ### CLI Usage
@@ -183,6 +184,40 @@ $ echo "Hello, World!" | swhid content
183
184
  swh:1:cnt:96898574d1b88e619be24fd90bb4cd399acbc5ca
184
185
  ```
185
186
 
187
+ **Generate SWHID from directory**
188
+
189
+ ```bash
190
+ $ swhid directory /path/to/directory
191
+ swh:1:dir:4b825dc642cb6eb9a060e54bf8d69288fbee4904
192
+ ```
193
+
194
+ **Generate SWHID from git commit**
195
+
196
+ ```bash
197
+ $ swhid revision /path/to/repo
198
+ swh:1:rev:bc0195aad0daa2ad5b0d76cce22b167bc3435590
199
+
200
+ $ swhid revision /path/to/repo main
201
+ swh:1:rev:bc0195aad0daa2ad5b0d76cce22b167bc3435590
202
+
203
+ $ swhid revision /path/to/repo abc123
204
+ swh:1:rev:bc0195aad0daa2ad5b0d76cce22b167bc3435590
205
+ ```
206
+
207
+ **Generate SWHID from git tag**
208
+
209
+ ```bash
210
+ $ swhid release /path/to/repo v1.0.0
211
+ swh:1:rel:2b10839e32c4c476e9d94492756bb1a3e1ec4aa8
212
+ ```
213
+
214
+ **Generate SWHID from git snapshot**
215
+
216
+ ```bash
217
+ $ swhid snapshot /path/to/repo
218
+ swh:1:snp:6e65b86363953b780d92b0a928f3e8fcdd10db36
219
+ ```
220
+
186
221
  **Add qualifiers**
187
222
 
188
223
  ```bash
data/exe/swhid CHANGED
@@ -24,6 +24,14 @@ class SwhidCLI
24
24
  parse_swhid
25
25
  when "content"
26
26
  compute_content_swhid
27
+ when "directory"
28
+ compute_directory_swhid
29
+ when "revision"
30
+ compute_revision_swhid
31
+ when "release"
32
+ compute_release_swhid
33
+ when "snapshot"
34
+ compute_snapshot_swhid
27
35
  when "help"
28
36
  show_help
29
37
  else
@@ -41,9 +49,13 @@ class SwhidCLI
41
49
  opts.banner = "Usage: swhid [command] [options]"
42
50
  opts.separator ""
43
51
  opts.separator "Commands:"
44
- opts.separator " parse <swhid> Parse and validate a SWHID"
45
- opts.separator " content Generate SWHID for content from stdin"
46
- opts.separator " help Show this help message"
52
+ opts.separator " parse <swhid> Parse and validate a SWHID"
53
+ opts.separator " content Generate SWHID for content from stdin"
54
+ opts.separator " directory <path> Generate SWHID for directory"
55
+ opts.separator " revision <repo> [ref] Generate SWHID for git revision/commit"
56
+ opts.separator " release <repo> <tag> Generate SWHID for git release/tag"
57
+ opts.separator " snapshot <repo> Generate SWHID for git snapshot"
58
+ opts.separator " help Show this help message"
47
59
  opts.separator ""
48
60
  opts.separator "Options:"
49
61
 
@@ -104,6 +116,124 @@ class SwhidCLI
104
116
  end
105
117
  end
106
118
 
119
+ def compute_directory_swhid
120
+ path = @args.shift
121
+ unless path
122
+ puts "Error: Directory path required"
123
+ exit 1
124
+ end
125
+
126
+ unless File.exist?(path)
127
+ puts "Error: Path does not exist: #{path}"
128
+ exit 1
129
+ end
130
+
131
+ unless File.directory?(path)
132
+ puts "Error: Path is not a directory: #{path}"
133
+ exit 1
134
+ end
135
+
136
+ swhid = Swhid::FromFilesystem.from_directory_path(path)
137
+
138
+ unless @options[:qualifiers].empty?
139
+ swhid = Swhid::Identifier.new(
140
+ object_type: swhid.object_type,
141
+ object_hash: swhid.object_hash,
142
+ qualifiers: @options[:qualifiers]
143
+ )
144
+ end
145
+
146
+ case @options[:format]
147
+ when "json"
148
+ output_json(swhid)
149
+ else
150
+ puts swhid.to_s
151
+ end
152
+ end
153
+
154
+ def compute_revision_swhid
155
+ repo_path = @args.shift
156
+ unless repo_path
157
+ puts "Error: Repository path required"
158
+ exit 1
159
+ end
160
+
161
+ ref = @args.shift || "HEAD"
162
+
163
+ swhid = Swhid::FromGit.from_revision(repo_path, ref)
164
+
165
+ unless @options[:qualifiers].empty?
166
+ swhid = Swhid::Identifier.new(
167
+ object_type: swhid.object_type,
168
+ object_hash: swhid.object_hash,
169
+ qualifiers: @options[:qualifiers]
170
+ )
171
+ end
172
+
173
+ case @options[:format]
174
+ when "json"
175
+ output_json(swhid)
176
+ else
177
+ puts swhid.to_s
178
+ end
179
+ end
180
+
181
+ def compute_release_swhid
182
+ repo_path = @args.shift
183
+ unless repo_path
184
+ puts "Error: Repository path required"
185
+ exit 1
186
+ end
187
+
188
+ tag_name = @args.shift
189
+ unless tag_name
190
+ puts "Error: Tag name required"
191
+ exit 1
192
+ end
193
+
194
+ swhid = Swhid::FromGit.from_release(repo_path, tag_name)
195
+
196
+ unless @options[:qualifiers].empty?
197
+ swhid = Swhid::Identifier.new(
198
+ object_type: swhid.object_type,
199
+ object_hash: swhid.object_hash,
200
+ qualifiers: @options[:qualifiers]
201
+ )
202
+ end
203
+
204
+ case @options[:format]
205
+ when "json"
206
+ output_json(swhid)
207
+ else
208
+ puts swhid.to_s
209
+ end
210
+ end
211
+
212
+ def compute_snapshot_swhid
213
+ repo_path = @args.shift
214
+ unless repo_path
215
+ puts "Error: Repository path required"
216
+ exit 1
217
+ end
218
+
219
+ swhid = Swhid::FromGit.from_snapshot(repo_path)
220
+
221
+ unless @options[:qualifiers].empty?
222
+ swhid = Swhid::Identifier.new(
223
+ object_type: swhid.object_type,
224
+ object_hash: swhid.object_hash,
225
+ qualifiers: @options[:qualifiers]
226
+ )
227
+ end
228
+
229
+ case @options[:format]
230
+ when "json"
231
+ output_json(swhid)
232
+ else
233
+ puts swhid.to_s
234
+ end
235
+ end
236
+
107
237
  def output_text(swhid)
108
238
  puts "SWHID: #{swhid.to_s}"
109
239
  puts "Core: #{swhid.core_swhid}"
@@ -134,8 +264,12 @@ class SwhidCLI
134
264
  swhid - Generate and parse SoftWare Hash IDentifiers
135
265
 
136
266
  Usage:
137
- swhid parse <swhid> Parse and validate a SWHID
138
- swhid content [options] Generate SWHID for content from stdin
267
+ swhid parse <swhid> Parse and validate a SWHID
268
+ swhid content [options] Generate SWHID for content from stdin
269
+ swhid directory <path> [options] Generate SWHID for directory
270
+ swhid revision <repo> [ref] [options] Generate SWHID for git revision/commit
271
+ swhid release <repo> <tag> [options] Generate SWHID for git release/tag
272
+ swhid snapshot <repo> [options] Generate SWHID for git snapshot
139
273
 
140
274
  Options:
141
275
  -f, --format FORMAT Output format (text, json)
@@ -149,6 +283,20 @@ class SwhidCLI
149
283
  # Generate SWHID from file content
150
284
  cat file.txt | swhid content
151
285
 
286
+ # Generate SWHID from directory
287
+ swhid directory /path/to/dir
288
+
289
+ # Generate SWHID from git commit
290
+ swhid revision /path/to/repo
291
+ swhid revision /path/to/repo main
292
+ swhid revision /path/to/repo abc123
293
+
294
+ # Generate SWHID from git tag
295
+ swhid release /path/to/repo v1.0.0
296
+
297
+ # Generate SWHID from git snapshot
298
+ swhid snapshot /path/to/repo
299
+
152
300
  # Generate SWHID with qualifiers
153
301
  cat file.txt | swhid content -q origin=https://github.com/example/repo
154
302
 
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rugged"
4
+
5
+ module Swhid
6
+ module FromGit
7
+ def self.from_revision(repo_path, ref = "HEAD")
8
+ repo = Rugged::Repository.new(repo_path)
9
+ commit = repo.rev_parse(ref)
10
+
11
+ raise ArgumentError, "Reference #{ref} is not a commit" unless commit.is_a?(Rugged::Commit)
12
+
13
+ metadata = {
14
+ directory: commit.tree.oid,
15
+ parents: commit.parents.map(&:oid),
16
+ author: format_person(commit.author),
17
+ author_timestamp: commit.author[:time].to_i,
18
+ author_timezone: format_timezone(commit.author[:time]),
19
+ committer: format_person(commit.committer),
20
+ committer_timestamp: commit.committer[:time].to_i,
21
+ committer_timezone: format_timezone(commit.committer[:time]),
22
+ message: commit.message
23
+ }
24
+
25
+ # Extract extra headers if present
26
+ extra_headers = extract_extra_headers(commit)
27
+ metadata[:extra_headers] = extra_headers unless extra_headers.empty?
28
+
29
+ Swhid.from_revision(metadata)
30
+ end
31
+
32
+ def self.from_release(repo_path, tag_name)
33
+ repo = Rugged::Repository.new(repo_path)
34
+ tag_ref = repo.references["refs/tags/#{tag_name}"]
35
+
36
+ raise ArgumentError, "Tag #{tag_name} not found" unless tag_ref
37
+
38
+ # Get the tag object
39
+ tag_obj = repo.lookup(tag_ref.target_id)
40
+
41
+ # Check if it's an annotated tag
42
+ if tag_obj.is_a?(Rugged::Tag::Annotation)
43
+ target_type = case tag_obj.target
44
+ when Rugged::Commit then "rev"
45
+ when Rugged::Tree then "dir"
46
+ when Rugged::Blob then "cnt"
47
+ else "rev"
48
+ end
49
+
50
+ metadata = {
51
+ name: tag_obj.name,
52
+ target: { hash: tag_obj.target.oid, type: target_type },
53
+ message: tag_obj.message
54
+ }
55
+
56
+ if tag_obj.tagger
57
+ metadata[:author] = format_person(tag_obj.tagger)
58
+ metadata[:author_timestamp] = tag_obj.tagger[:time].to_i
59
+ metadata[:author_timezone] = format_timezone(tag_obj.tagger[:time])
60
+ end
61
+
62
+ Swhid.from_release(metadata)
63
+ else
64
+ # Lightweight tag - points directly to commit
65
+ raise ArgumentError, "Lightweight tags are not supported for release SWHIDs"
66
+ end
67
+ end
68
+
69
+ def self.from_snapshot(repo_path)
70
+ repo = Rugged::Repository.new(repo_path)
71
+ branches = []
72
+
73
+ # Get all references (branches and tags)
74
+ repo.references.each do |ref|
75
+ ref_name = ref.name
76
+
77
+ if ref.type == :symbolic
78
+ # This is an alias (like HEAD pointing to refs/heads/main)
79
+ target_ref_name = ref.target
80
+ branches << {
81
+ name: ref_name,
82
+ target_type: "alias",
83
+ target: target_ref_name
84
+ }
85
+ else
86
+ # Direct reference
87
+ target_obj = ref.target
88
+
89
+ # Determine target type and OID
90
+ target_type, target_oid = case target_obj
91
+ when Rugged::Commit
92
+ ["revision", target_obj.oid]
93
+ when Rugged::Tag::Annotation
94
+ ["release", target_obj.oid]
95
+ when Rugged::Tree
96
+ ["directory", target_obj.oid]
97
+ when Rugged::Blob
98
+ ["content", target_obj.oid]
99
+ else
100
+ ["revision", target_obj.oid]
101
+ end
102
+
103
+ branches << {
104
+ name: ref_name,
105
+ target_type: target_type,
106
+ target: target_oid
107
+ }
108
+ end
109
+ end
110
+
111
+ Swhid.from_snapshot(branches)
112
+ end
113
+
114
+ private
115
+
116
+ def self.format_person(person)
117
+ "#{person[:name]} <#{person[:email]}>"
118
+ end
119
+
120
+ def self.format_timezone(time)
121
+ offset = time.utc_offset
122
+ sign = offset >= 0 ? "+" : "-"
123
+ hours = offset.abs / 3600
124
+ minutes = (offset.abs % 3600) / 60
125
+ format("%s%02d%02d", sign, hours, minutes)
126
+ end
127
+
128
+ def self.extract_extra_headers(commit)
129
+ # Rugged doesn't expose extra headers directly
130
+ # We would need to parse the raw commit object for this
131
+ # For now, return empty array
132
+ []
133
+ end
134
+ end
135
+ end
@@ -69,7 +69,10 @@ module Swhid
69
69
  sorted_entries = entries.sort_by(&:sort_key)
70
70
 
71
71
  sorted_entries.map do |entry|
72
- "#{entry.perms} #{entry.name}\0#{entry.target_hash}"
72
+ # Convert name to binary UTF-8 to match target_hash encoding
73
+ name_binary = entry.name.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)
74
+ perms_binary = entry.perms.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)
75
+ "#{perms_binary} #{name_binary}\0#{entry.target_hash}"
73
76
  end.join
74
77
  end
75
78
 
data/lib/swhid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Swhid
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/swhid.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "swhid/objects/revision"
8
8
  require_relative "swhid/objects/release"
9
9
  require_relative "swhid/objects/snapshot"
10
10
  require_relative "swhid/from_filesystem"
11
+ require_relative "swhid/from_git"
11
12
 
12
13
  module Swhid
13
14
  class Error < StandardError; end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swhid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rugged
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.9'
12
26
  description: A Ruby library and CLI for generating and parsing SoftWare Hash IDentifiers
13
27
  (SWHIDs). Supports all object types (content, directory, revision, release, snapshot)
14
28
  and qualifiers. Compatible with Git object hashing.
@@ -27,6 +41,7 @@ files:
27
41
  - exe/swhid
28
42
  - lib/swhid.rb
29
43
  - lib/swhid/from_filesystem.rb
44
+ - lib/swhid/from_git.rb
30
45
  - lib/swhid/identifier.rb
31
46
  - lib/swhid/objects/content.rb
32
47
  - lib/swhid/objects/directory.rb