swhid 0.2.1 → 0.3.1

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: ab5130c9128fd1942d8a4a01eecb6d5f403f38fc2ce53d72323b08350e30bdf4
4
+ data.tar.gz: a7c5c116fcd54c08f7d51b4125d4871f25b98aacc4f7ab818ebae670fc0972c9
5
5
  SHA512:
6
- metadata.gz: e9d04af133af2dca2b749ce7259a792dbed4622b7bfc587cc08be99d87e109d50be5926e0db2b653b3da7d0e74454fac3e4a9709b12fcd9306f4be20583eab97
7
- data.tar.gz: 24affb1c2fa578ab1211b54540a3d2a51497e583b70804cf837c8999a2c8beb3c3546af6f0de35fe2743e26918fc775baf9f6ec0e75fad0d2d116b10730a412b
6
+ metadata.gz: ad64d5ffad60fde8ec7399abcd63af7c34ae203c54f469df9866283864f1850c0f9b1c112ab89300c7ea52f56d5809189bbe6ec0bd2c9fc2ed85510daa3b2d37
7
+ data.tar.gz: a44ef79ecd7a5d64f4d29c56f470b749ab61eac0d3f3ddd29c8b4798f334fa3f43222b8b463819f221e5687ca1355f7c23b7754be43a739009aafa4adba3e3cd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-11-23
4
+
5
+ ### Fixed
6
+ - Snapshot implementation now includes HEAD symbolic reference
7
+ - Extra headers extraction for signed commits (gpgsig, mergetag, etc.)
8
+ - Tag-of-tag support (tags pointing to other tag objects)
9
+ - Extra headers extraction for signed tags
10
+
11
+ ## [0.3.0] - 2025-11-23
12
+
13
+ - `directory` CLI command - Generate SWHID for directory from filesystem
14
+ - `revision` CLI command - Generate SWHID for git commit/revision
15
+ - `release` CLI command - Generate SWHID for git tag/release
16
+ - `snapshot` CLI command - Generate SWHID for git repository snapshot
17
+
3
18
  ## [0.2.1] - 2025-11-09
4
19
 
5
20
  - 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,219 @@
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 (like gpgsig, svn headers, etc)
26
+ extra_headers = extract_extra_headers(repo, 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::Tag::Annotation then "rel"
46
+ when Rugged::Tree then "dir"
47
+ when Rugged::Blob then "cnt"
48
+ else "rev"
49
+ end
50
+
51
+ metadata = {
52
+ name: tag_obj.name,
53
+ target: { hash: tag_obj.target.oid, type: target_type },
54
+ message: tag_obj.message
55
+ }
56
+
57
+ if tag_obj.tagger
58
+ metadata[:author] = format_person(tag_obj.tagger)
59
+ metadata[:author_timestamp] = tag_obj.tagger[:time].to_i
60
+ metadata[:author_timezone] = format_timezone(tag_obj.tagger[:time])
61
+ end
62
+
63
+ # Extract extra headers if present (like gpgsig for signed tags)
64
+ extra_headers = extract_tag_extra_headers(repo, tag_obj)
65
+ metadata[:extra_headers] = extra_headers unless extra_headers.empty?
66
+
67
+ Swhid.from_release(metadata)
68
+ else
69
+ # Lightweight tag - points directly to commit
70
+ raise ArgumentError, "Lightweight tags are not supported for release SWHIDs"
71
+ end
72
+ end
73
+
74
+ def self.from_snapshot(repo_path)
75
+ repo = Rugged::Repository.new(repo_path)
76
+ branches = []
77
+
78
+ # Check for HEAD first
79
+ head_path = File.join(repo.path, "HEAD")
80
+ if File.exist?(head_path)
81
+ head_content = File.read(head_path).strip
82
+ if head_content.start_with?("ref:")
83
+ # HEAD is a symbolic ref
84
+ target_ref = head_content.sub("ref: ", "")
85
+ branches << {
86
+ name: "HEAD",
87
+ target_type: "alias",
88
+ target: target_ref
89
+ }
90
+ end
91
+ end
92
+
93
+ # Get all references (branches and tags)
94
+ repo.references.each do |ref|
95
+ ref_name = ref.name
96
+
97
+ if ref.type == :symbolic
98
+ # This is an alias (symbolic ref)
99
+ target_ref_name = ref.target
100
+ branches << {
101
+ name: ref_name,
102
+ target_type: "alias",
103
+ target: target_ref_name
104
+ }
105
+ else
106
+ # Direct reference
107
+ target_obj = ref.target
108
+
109
+ # Determine target type and OID
110
+ target_type, target_oid = case target_obj
111
+ when Rugged::Commit
112
+ ["revision", target_obj.oid]
113
+ when Rugged::Tag::Annotation
114
+ ["release", target_obj.oid]
115
+ when Rugged::Tree
116
+ ["directory", target_obj.oid]
117
+ when Rugged::Blob
118
+ ["content", target_obj.oid]
119
+ else
120
+ ["revision", target_obj.oid]
121
+ end
122
+
123
+ branches << {
124
+ name: ref_name,
125
+ target_type: target_type,
126
+ target: target_oid
127
+ }
128
+ end
129
+ end
130
+
131
+ Swhid.from_snapshot(branches)
132
+ end
133
+
134
+ private
135
+
136
+ def self.format_person(person)
137
+ "#{person[:name]} <#{person[:email]}>"
138
+ end
139
+
140
+ def self.format_timezone(time)
141
+ offset = time.utc_offset
142
+ sign = offset >= 0 ? "+" : "-"
143
+ hours = offset.abs / 3600
144
+ minutes = (offset.abs % 3600) / 60
145
+ format("%s%02d%02d", sign, hours, minutes)
146
+ end
147
+
148
+ def self.extract_extra_headers(repo, commit)
149
+ # Rugged doesn't expose extra headers directly
150
+ # We need to parse the raw commit object
151
+ raw_data = repo.read(commit.oid).data
152
+ lines = raw_data.split("\n")
153
+
154
+ extra_headers = []
155
+ in_headers = true
156
+
157
+ lines.each do |line|
158
+ # Stop when we hit the blank line before the message
159
+ if line.empty?
160
+ in_headers = false
161
+ next
162
+ end
163
+
164
+ next unless in_headers
165
+
166
+ # Skip standard headers
167
+ next if line.start_with?("tree ", "parent ", "author ", "committer ")
168
+
169
+ # Extract extra headers (like gpgsig, mergetag, svn-repo-uuid, etc)
170
+ if line.start_with?(" ")
171
+ # Continuation of previous header
172
+ if extra_headers.any?
173
+ extra_headers.last[1] += "\n#{line[1..]}"
174
+ end
175
+ elsif line.include?(" ")
176
+ key, value = line.split(" ", 2)
177
+ extra_headers << [key, value]
178
+ end
179
+ end
180
+
181
+ extra_headers
182
+ end
183
+
184
+ def self.extract_tag_extra_headers(repo, tag)
185
+ # Parse raw tag object for extra headers
186
+ raw_data = repo.read(tag.oid).data
187
+ lines = raw_data.split("\n")
188
+
189
+ extra_headers = []
190
+ in_headers = true
191
+
192
+ lines.each do |line|
193
+ # Stop when we hit the blank line before the message
194
+ if line.empty?
195
+ in_headers = false
196
+ next
197
+ end
198
+
199
+ next unless in_headers
200
+
201
+ # Skip standard tag headers
202
+ next if line.start_with?("object ", "type ", "tag ", "tagger ")
203
+
204
+ # Extract extra headers (like gpgsig for signed tags)
205
+ if line.start_with?(" ")
206
+ # Continuation of previous header
207
+ if extra_headers.any?
208
+ extra_headers.last[1] += "\n#{line[1..]}"
209
+ end
210
+ elsif line.include?(" ")
211
+ key, value = line.split(" ", 2)
212
+ extra_headers << [key, value]
213
+ end
214
+ end
215
+
216
+ extra_headers
217
+ end
218
+ end
219
+ 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
 
@@ -39,6 +39,11 @@ module Swhid
39
39
  lines << author_line
40
40
  end
41
41
 
42
+ extra_headers = metadata[:extra_headers] || []
43
+ extra_headers.each do |key, value|
44
+ lines << format_header_line(key, value)
45
+ end
46
+
42
47
  result = lines.join("\n") + "\n"
43
48
 
44
49
  if metadata[:message]
@@ -54,6 +59,11 @@ module Swhid
54
59
  "#{prefix} #{person_escaped} #{timestamp} #{tz}"
55
60
  end
56
61
 
62
+ def self.format_header_line(key, value)
63
+ value_escaped = value.gsub("\n", "\n ")
64
+ "#{key} #{value_escaped}"
65
+ end
66
+
57
67
  def self.extract_target(target)
58
68
  case target
59
69
  when Hash
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.1"
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.1
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