gitem 0.1.0 → 0.2.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: 5b9c3b4254111162f1b7f4c01d02a2bfaf7f6dd72d2b74917dd3afbb26da56c8
4
- data.tar.gz: 03d052e30428e19d8d8613a6fd0d413c3c2e758acb0fcb9de8e8d239539f582d
3
+ metadata.gz: 79d05206bb7ce01fa86ea94f2fc1342912200ce83e7a00650bc1393a427cbe8e
4
+ data.tar.gz: 5a370c3907e190b010fe694302f94004a28e6810ac4d85bd5cf76d6d1bac05c8
5
5
  SHA512:
6
- metadata.gz: 10a23da9cfb7d80aaf1ef0b18aeb1d23bc8ebb8524b53b03baec012c9d13025e1c593131ae23d46afccdd2a073644aff871ba5d0ed7c25486a16ef3fe45528bc
7
- data.tar.gz: 54475cf8d4c94e44b2f8c5aa9ffc231d54bfad666f371166aff2fd250c9a0d3aaa08b8a793b9027f0530259b34c879705603770a448d885bd7429f7c3c7b39c9
6
+ metadata.gz: 425331cceea1d9129d13c37587f3f37fa1dabeed77ed66c86119e9f0de4a7f4f93e1f64a031696d5e8ae2ef0001314bd52bd6b5d9a553bdfaef9bced5dff72c6
7
+ data.tar.gz: ce2f0b7ad472d6d4efa0686507ef7b1414aff3c21224effc4f6d206de3014afe9b380f2d62ec64360a40fc5ba6f09bfc06d6e247ab03d745199e9eb582c303f8
data/README.md CHANGED
@@ -1,39 +1,48 @@
1
1
  # Gitem
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/gitem`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ A static site generator for git repositories. Browse your git history locally with a GitHub-like interface.
6
4
 
7
5
  ## Installation
8
6
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
- Install the gem and add to the application's Gemfile by executing:
12
-
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
7
+ ```
8
+ gem install gitem
15
9
  ```
16
10
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
11
+ ## Usage
18
12
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
13
+ ```
14
+ gitem serve # Generate and serve (default)
15
+ gitem serve -p 3000 # Custom port
16
+ gitem serve --open # Open browser
17
+ gitem serve --no-generate # Serve only
18
+ gitem generate # Generate only
19
+ gitem generate -o ./out # Custom output
21
20
  ```
22
21
 
23
- ## Usage
22
+ ## Requirements
24
23
 
25
- TODO: Write usage instructions here
24
+ - Ruby >= 3.4.0
25
+ - libgit2
26
26
 
27
- ## Development
27
+ ### macOS
28
28
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
29
+ ```
30
+ brew install libgit2
31
+ ```
30
32
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
33
+ ### Ubuntu/Debian
34
+
35
+ ```
36
+ apt-get install libgit2-dev cmake
37
+ ```
32
38
 
33
- ## Contributing
39
+ ## Development
34
40
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/gitem.
41
+ ```
42
+ bin/setup
43
+ rake spec
44
+ ```
36
45
 
37
46
  ## License
38
47
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
48
+ [MIT](https://opensource.org/licenses/MIT)
data/exe/gitem CHANGED
@@ -2,20 +2,4 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "gitem"
5
-
6
- if ARGV.empty?
7
- puts "Usage: ruby #{$0} <path-to-git-repo> [output-directory]"
8
- puts "Default output: <repo>/.git/srv/"
9
- puts "Example: ruby #{$0} ."
10
- exit 1
11
- end
12
-
13
- repo_path = ARGV[0]
14
- output_dir = ARGV[1]
15
-
16
- unless File.exist?(File.join(repo_path, '.git')) || File.exist?(File.join(repo_path, 'HEAD'))
17
- puts "Error: #{repo_path} is not a valid git repository"
18
- exit 1
19
- end
20
-
21
- Gitem::GitToJson.new(repo_path, output_dir).export!
5
+ Gitem::CLI.new(ARGV).run
data/lib/gitem/cli.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitem
4
+ class CLI
5
+ def initialize(argv)
6
+ @argv = argv.dup
7
+ @options = { output: nil, port: 8000, generate: true, open: false }
8
+ end
9
+
10
+ def run
11
+ command = @argv.shift || "serve"
12
+ case command
13
+ when "generate", "g" then run_generate
14
+ when "serve", "s" then run_serve
15
+ when "help", "-h", "--help" then print_help
16
+ when "version", "-v", "--version" then puts "gitem #{VERSION}"
17
+ else
18
+ warn "Unknown command: #{command}"
19
+ print_help
20
+ exit 1
21
+ end
22
+ rescue Rugged::Error, Rugged::RepositoryError => e
23
+ warn "Git error: #{e.message}"
24
+ exit 1
25
+ rescue Error => e
26
+ warn "Error: #{e.message}"
27
+ exit 1
28
+ end
29
+
30
+ private
31
+
32
+ def run_generate
33
+ parse_generate_options!
34
+ validate_repo!
35
+ Generator.new(@options[:repo_path], @options[:output]).export!
36
+ end
37
+
38
+ def run_serve
39
+ parse_serve_options!
40
+ validate_repo!
41
+ generator = Generator.new(@options[:repo_path], @options[:output])
42
+ generator.export! if @options[:generate]
43
+ server = Server.new(generator.output_dir, @options[:port])
44
+ open_browser(server.url) if @options[:open]
45
+ server.start
46
+ end
47
+
48
+ def parse_generate_options!
49
+ OptionParser.new do |opts|
50
+ opts.banner = "Usage: gitem generate [REPO_PATH] [options]"
51
+ opts.on("-o", "--output DIR", "Output directory") { |v| @options[:output] = v }
52
+ opts.on("-h", "--help", "Show help") { puts opts; exit }
53
+ end.parse!(@argv)
54
+ @options[:repo_path] = @argv.shift || "."
55
+ end
56
+
57
+ def parse_serve_options!
58
+ OptionParser.new do |opts|
59
+ opts.banner = "Usage: gitem serve [REPO_PATH] [options]"
60
+ opts.on("-o", "--output DIR", "Output directory") { |v| @options[:output] = v }
61
+ opts.on("-p", "--port PORT", Integer, "Port (default: 8000)") { |v| @options[:port] = v }
62
+ opts.on("--[no-]generate", "Generate before serving") { |v| @options[:generate] = v }
63
+ opts.on("--open", "Open browser") { @options[:open] = true }
64
+ opts.on("-h", "--help", "Show help") { puts opts; exit }
65
+ end.parse!(@argv)
66
+ @options[:repo_path] = @argv.shift || "."
67
+ end
68
+
69
+ def validate_repo!
70
+ path = @options[:repo_path]
71
+ return if File.exist?(File.join(path, ".git")) || File.exist?(File.join(path, "HEAD"))
72
+ raise Error, "'#{path}' is not a valid git repository"
73
+ end
74
+
75
+ def open_browser(url)
76
+ cmd = case RbConfig::CONFIG["host_os"]
77
+ when /darwin/i then "open"
78
+ when /linux/i then "xdg-open"
79
+ when /mswin|mingw|cygwin/i then "start"
80
+ end
81
+ system("#{cmd} #{url} > /dev/null 2>&1 &") if cmd
82
+ end
83
+
84
+ def print_help
85
+ puts <<~HELP
86
+ Usage: gitem <command> [options]
87
+
88
+ Commands:
89
+ serve, s Generate and serve (default)
90
+ generate, g Generate JSON files only
91
+ help Show help
92
+ version Show version
93
+
94
+ Run 'gitem <command> --help' for options.
95
+ HELP
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitem
4
+ class Generator
5
+ TEMPLATE_PATH = File.expand_path("index.html", __dir__)
6
+ attr_reader :output_dir
7
+
8
+ def initialize(repo_path, output_dir = nil)
9
+ @repo = Rugged::Repository.new(repo_path)
10
+ @output_dir = output_dir || File.join(@repo.path, "srv")
11
+ @processed_trees = Set.new
12
+ @processed_blobs = Set.new
13
+ end
14
+
15
+ def export!
16
+ setup_directories
17
+ export_branches
18
+ export_tags
19
+ export_commits
20
+ export_repo_info
21
+ copy_template
22
+ puts "✓ Generated: #{@output_dir}"
23
+ end
24
+
25
+ private
26
+
27
+ def setup_directories
28
+ %w[commits trees blobs refs/heads refs/tags].each do |dir|
29
+ FileUtils.mkdir_p(File.join(@output_dir, dir))
30
+ end
31
+ end
32
+
33
+ def copy_template
34
+ FileUtils.cp(TEMPLATE_PATH, File.join(@output_dir, "index.html"))
35
+ end
36
+
37
+ def export_repo_info
38
+ branch = @repo.branches[default_branch_name]
39
+ readme_content, readme_name = extract_readme(branch)
40
+ write_json("repo.json", {
41
+ name: repo_name, default_branch: default_branch_name,
42
+ branches_count: local_branches.size, tags_count: @repo.tags.count,
43
+ readme: readme_content, readme_name: readme_name, generated_at: Time.now.iso8601
44
+ })
45
+ end
46
+
47
+ def repo_name
48
+ File.basename(@repo.workdir || @repo.path.chomp("/.git/").chomp(".git"))
49
+ end
50
+
51
+ def default_branch_name
52
+ @default_branch_name ||= %w[main master].find { |n| @repo.branches[n] } || local_branches.first&.name || "main"
53
+ end
54
+
55
+ def local_branches
56
+ @local_branches ||= @repo.branches.select { |b| b.name && !b.name.include?("/") }
57
+ end
58
+
59
+ def extract_readme(branch)
60
+ return [nil, nil] unless branch&.target
61
+ tree = branch.target.tree
62
+ %w[README.md README.markdown readme.md README.txt README].each do |name|
63
+ entry = tree.each.find { |e| e[:name].casecmp?(name) }
64
+ next unless entry&.dig(:type) == :blob
65
+ blob = @repo.lookup(entry[:oid])
66
+ next if blob.binary?
67
+ return [blob.content.encode("UTF-8", invalid: :replace, undef: :replace), entry[:name]]
68
+ end
69
+ [nil, nil]
70
+ end
71
+
72
+ def export_branches
73
+ branches = local_branches.filter_map do |branch|
74
+ target = branch.target
75
+ next unless target
76
+ { name: branch.name, sha: target.oid, is_head: branch.head?,
77
+ committed_at: target.committer[:time].iso8601, author: target.author[:name],
78
+ message: target.message.lines.first&.strip || "" }
79
+ end.sort_by { |b| b[:is_head] ? 0 : 1 }
80
+ write_json("branches.json", branches)
81
+ branches.each { |b| write_json("refs/heads/#{b[:name]}.json", b) }
82
+ end
83
+
84
+ def export_tags
85
+ tags = @repo.tags.filter_map do |tag|
86
+ target = tag.target
87
+ next unless target
88
+ commit = target.is_a?(Rugged::Tag::Annotation) ? target.target : target
89
+ { name: tag.name, sha: commit.oid, annotated: target.is_a?(Rugged::Tag::Annotation),
90
+ message: target.is_a?(Rugged::Tag::Annotation) ? target.message : nil,
91
+ committed_at: commit.committer[:time].iso8601 }
92
+ end.sort_by { |t| t[:committed_at] }.reverse
93
+ write_json("tags.json", tags)
94
+ tags.each { |t| write_json("refs/tags/#{t[:name]}.json", t) }
95
+ end
96
+
97
+ def export_commits
98
+ commits_list = []
99
+ walker = Rugged::Walker.new(@repo)
100
+ @repo.branches.each { |b| walker.push(b.target.oid) if b.target rescue nil }
101
+ walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_DATE)
102
+ walker.each_with_index do |commit, idx|
103
+ print "\r Processing commits: #{idx + 1}" if ((idx + 1) % 50).zero?
104
+ data = extract_commit(commit)
105
+ commits_list << data.slice(:sha, :short_sha, :message_headline, :author, :committed_at)
106
+ write_json("commits/#{commit.oid}.json", data)
107
+ export_tree(commit.tree, "")
108
+ end
109
+ commits_list.sort_by! { |c| c[:committed_at] }.reverse!
110
+ write_json("commits.json", commits_list)
111
+ puts "\r Processed #{commits_list.size} commits"
112
+ end
113
+
114
+ def extract_commit(commit)
115
+ stats = commit.parents.empty? ? initial_diff_stats(commit) : parent_diff_stats(commit)
116
+ { sha: commit.oid, short_sha: commit.oid[0, 7], message: commit.message,
117
+ message_headline: commit.message.lines.first&.strip || "",
118
+ author: { name: commit.author[:name], email: commit.author[:email], date: commit.author[:time].iso8601 },
119
+ committer: { name: commit.committer[:name], email: commit.committer[:email], date: commit.committer[:time].iso8601 },
120
+ committed_at: commit.committer[:time].iso8601,
121
+ parents: commit.parents.map { |p| { sha: p.oid, short_sha: p.oid[0, 7] } },
122
+ tree_sha: commit.tree.oid, stats: stats[:stats], files: stats[:files] }
123
+ end
124
+
125
+ def initial_diff_stats(commit)
126
+ files = []
127
+ collect_tree_files(commit.tree, "", files)
128
+ { stats: { additions: files.sum { |f| f[:additions] }, deletions: 0, changed: files.size }, files: files }
129
+ end
130
+
131
+ def collect_tree_files(tree, path, files)
132
+ tree.each do |entry|
133
+ full_path = path.empty? ? entry[:name] : "#{path}/#{entry[:name]}"
134
+ if entry[:type] == :blob
135
+ blob = @repo.lookup(entry[:oid])
136
+ files << { path: full_path, additions: blob.binary? ? 0 : blob.content.lines.count, deletions: 0, status: "added" }
137
+ elsif entry[:type] == :tree
138
+ collect_tree_files(@repo.lookup(entry[:oid]), full_path, files)
139
+ end
140
+ end
141
+ end
142
+
143
+ def parent_diff_stats(commit)
144
+ diff = commit.parents.first.diff(commit)
145
+ files, additions, deletions = [], 0, 0
146
+ diff.each_patch do |patch|
147
+ fa, fd = 0, 0
148
+ patch.each_hunk { |h| h.each_line { |l| fa += 1 if l.addition?; fd += 1 if l.deletion? } }
149
+ additions += fa; deletions += fd
150
+ status = { added: "added", deleted: "deleted", renamed: "renamed" }[patch.delta.status] || "modified"
151
+ files << { path: patch.delta.new_file[:path], additions: fa, deletions: fd, status: status }
152
+ end
153
+ { stats: { additions: additions, deletions: deletions, changed: files.size }, files: files }
154
+ end
155
+
156
+ def export_tree(tree, path)
157
+ return if @processed_trees.include?(tree.oid)
158
+ @processed_trees.add(tree.oid)
159
+ entries = tree.map do |entry|
160
+ entry_path = path.empty? ? entry[:name] : "#{path}/#{entry[:name]}"
161
+ export_tree(@repo.lookup(entry[:oid]), entry_path) if entry[:type] == :tree
162
+ export_blob(entry[:oid], entry_path) if entry[:type] == :blob
163
+ { name: entry[:name], path: entry_path, type: entry[:type].to_s, sha: entry[:oid], mode: entry[:filemode].to_s(8) }
164
+ end
165
+ write_json("trees/#{tree.oid}.json", { sha: tree.oid, path: path, entries: entries })
166
+ end
167
+
168
+ def export_blob(oid, path)
169
+ return if @processed_blobs.include?(oid)
170
+ @processed_blobs.add(oid)
171
+ blob = @repo.lookup(oid)
172
+ content = blob.binary? ? nil : blob.content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
173
+ content = "#{content[0, 100_000]}\n... [truncated]" if content && content.size > 100_000
174
+ write_json("blobs/#{oid}.json", { sha: oid, path: path, size: blob.size, binary: blob.binary?, content: content, truncated: !blob.binary? && blob.size > 100_000 })
175
+ end
176
+
177
+ def write_json(path, data)
178
+ File.write(File.join(@output_dir, path), JSON.generate(data))
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitem
4
+ class Server
5
+ attr_reader :url
6
+
7
+ def initialize(root, port = 8000)
8
+ @root = root
9
+ @port = port
10
+ @url = "http://localhost:#{port}"
11
+ end
12
+
13
+ def start
14
+ puts "🌐 Server running at #{@url}"
15
+ puts " Press Ctrl+C to stop\n\n"
16
+ server = WEBrick::HTTPServer.new(
17
+ Port: @port, DocumentRoot: @root,
18
+ Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN), AccessLog: []
19
+ )
20
+ trap("INT") { server.shutdown }
21
+ trap("TERM") { server.shutdown }
22
+ server.start
23
+ end
24
+ end
25
+ end
data/lib/gitem/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gitem
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/gitem.rb CHANGED
@@ -1,250 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fileutils'
4
- require 'json'
5
- require 'rugged'
6
- require 'time'
3
+ require "fileutils"
4
+ require "json"
5
+ require "optparse"
6
+ require "rbconfig"
7
+ require "rugged"
8
+ require "set"
9
+ require "time"
10
+ require "webrick"
7
11
 
8
12
  require_relative "gitem/version"
13
+ require_relative "gitem/generator"
14
+ require_relative "gitem/server"
15
+ require_relative "gitem/cli"
9
16
 
10
17
  module Gitem
11
18
  class Error < StandardError; end
12
-
13
- class GitToJson
14
- def initialize(repo_path, output_dir = nil)
15
- @repo = Rugged::Repository.new(repo_path)
16
- @output_dir = output_dir || File.join(@repo.path, 'srv')
17
- @processed_trees = Set.new
18
- @processed_blobs = Set.new
19
- end
20
-
21
- def export!
22
- setup_directories
23
- export_branches
24
- export_tags
25
- export_commits
26
- export_repo_info
27
- puts "\n✓ Export complete! Files written to #{@output_dir}"
28
- puts " Serve with: cd #{@output_dir} && ruby -run -e httpd . 8000"
29
- end
30
-
31
- private
32
-
33
- def setup_directories
34
- %w[commits trees blobs refs/heads refs/tags].each do |dir|
35
- FileUtils.mkdir_p(File.join(@output_dir, dir))
36
- end
37
- end
38
-
39
- def export_repo_info
40
- default_branch = default_branch_name
41
- branch = @repo.branches[default_branch]
42
- readme_content = nil
43
- readme_name = nil
44
-
45
- if branch&.target
46
- tree = branch.target.tree
47
- %w[README.md README.markdown readme.md README.txt README].each do |name|
48
- entry = tree.each.find { |e| e[:name].downcase == name.downcase }
49
- if entry && entry[:type] == :blob
50
- blob = @repo.lookup(entry[:oid])
51
- readme_content = blob.content.encode('UTF-8', invalid: :replace, undef: :replace) unless blob.binary?
52
- readme_name = entry[:name]
53
- break
54
- end
55
- end
56
- end
57
-
58
- info = {
59
- name: File.basename(@repo.workdir || @repo.path.chomp('/.git/').chomp('.git')),
60
- default_branch: default_branch,
61
- branches_count: @repo.branches.count { |b| b.name && !b.name.include?('/') },
62
- tags_count: @repo.tags.count,
63
- readme: readme_content,
64
- readme_name: readme_name,
65
- generated_at: Time.now.iso8601
66
- }
67
- write_json('repo.json', info)
68
- end
69
-
70
- def default_branch_name
71
- %w[main master].find { |name| @repo.branches[name] } || @repo.branches.first&.name || 'main'
72
- end
73
-
74
- def export_branches
75
- branches = @repo.branches.map do |branch|
76
- next if branch.name.nil? || branch.name.include?('/')
77
- target = branch.target rescue nil
78
- next unless target
79
- {
80
- name: branch.name,
81
- sha: target.oid,
82
- is_head: branch.head?,
83
- committed_at: target.committer[:time].iso8601,
84
- author: target.author[:name],
85
- message: target.message.lines.first&.strip || ''
86
- }
87
- end.compact.sort_by { |b| b[:is_head] ? 0 : 1 }
88
-
89
- write_json('branches.json', branches)
90
- branches.each { |b| write_json("refs/heads/#{b[:name]}.json", b) }
91
- end
92
-
93
- def export_tags
94
- tags = @repo.tags.map do |tag|
95
- target = tag.target rescue nil
96
- next unless target
97
- commit = target.is_a?(Rugged::Tag::Annotation) ? target.target : target
98
- {
99
- name: tag.name,
100
- sha: commit.oid,
101
- annotated: target.is_a?(Rugged::Tag::Annotation),
102
- message: target.is_a?(Rugged::Tag::Annotation) ? target.message : nil,
103
- committed_at: commit.committer[:time].iso8601
104
- }
105
- end.compact.sort_by { |t| t[:committed_at] }.reverse
106
-
107
- write_json('tags.json', tags)
108
- tags.each { |t| write_json("refs/tags/#{t[:name]}.json", t) }
109
- end
110
-
111
- def export_commits
112
- commits_list = []
113
- walker = Rugged::Walker.new(@repo)
114
-
115
- @repo.branches.each do |branch|
116
- next if branch.target.nil?
117
- walker.push(branch.target.oid) rescue nil
118
- end
119
-
120
- walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_DATE)
121
-
122
- walker.each_with_index do |commit, idx|
123
- print "\rProcessing commit #{idx + 1}..." if (idx + 1) % 10 == 0
124
-
125
- commit_data = extract_commit(commit)
126
- commits_list << commit_data.slice(:sha, :short_sha, :message_headline, :author, :committed_at)
127
-
128
- write_json("commits/#{commit.oid}.json", commit_data)
129
- export_tree(commit.tree, '')
130
- end
131
-
132
- commits_list.sort_by! { |c| c[:committed_at] }.reverse!
133
- write_json('commits.json', commits_list)
134
- puts "\rProcessed #{commits_list.size} commits"
135
- end
136
-
137
- def extract_commit(commit)
138
- parents = commit.parents.map { |p| { sha: p.oid, short_sha: p.oid[0..6] } }
139
- diff_stats = commit.parents.empty? ? initial_diff_stats(commit) : parent_diff_stats(commit)
140
-
141
- {
142
- sha: commit.oid,
143
- short_sha: commit.oid[0..6],
144
- message: commit.message,
145
- message_headline: commit.message.lines.first&.strip || '',
146
- author: { name: commit.author[:name], email: commit.author[:email], date: commit.author[:time].iso8601 },
147
- committer: { name: commit.committer[:name], email: commit.committer[:email], date: commit.committer[:time].iso8601 },
148
- committed_at: commit.committer[:time].iso8601,
149
- parents: parents,
150
- tree_sha: commit.tree.oid,
151
- stats: diff_stats[:stats],
152
- files: diff_stats[:files]
153
- }
154
- end
155
-
156
- def initial_diff_stats(commit)
157
- files = []
158
- collect_tree_files(commit.tree, '', files)
159
- { stats: { additions: files.sum { |f| f[:additions] }, deletions: 0, changed: files.size }, files: files }
160
- end
161
-
162
- def collect_tree_files(tree, path, files)
163
- tree.each do |entry|
164
- full_path = path.empty? ? entry[:name] : "#{path}/#{entry[:name]}"
165
- if entry[:type] == :blob
166
- blob = @repo.lookup(entry[:oid])
167
- lines = blob.binary? ? 0 : blob.content.lines.count
168
- files << { path: full_path, additions: lines, deletions: 0, status: 'added' }
169
- elsif entry[:type] == :tree
170
- collect_tree_files(@repo.lookup(entry[:oid]), full_path, files)
171
- end
172
- end
173
- end
174
-
175
- def parent_diff_stats(commit)
176
- diff = commit.parents.first.diff(commit)
177
- files = []
178
- additions = deletions = 0
179
-
180
- diff.each_patch do |patch|
181
- file_adds = file_dels = 0
182
- patch.each_hunk { |h| h.each_line { |l| l.addition? ? file_adds += 1 : (file_dels += 1 if l.deletion?) } }
183
- additions += file_adds
184
- deletions += file_dels
185
-
186
- status = case patch.delta.status
187
- when :added then 'added'
188
- when :deleted then 'deleted'
189
- when :renamed then 'renamed'
190
- else 'modified'
191
- end
192
-
193
- files << { path: patch.delta.new_file[:path], additions: file_adds, deletions: file_dels, status: status }
194
- end
195
-
196
- { stats: { additions: additions, deletions: deletions, changed: files.size }, files: files }
197
- end
198
-
199
- def export_tree(tree, path)
200
- return if @processed_trees.include?(tree.oid)
201
- @processed_trees.add(tree.oid)
202
-
203
- entries = tree.map do |entry|
204
- entry_data = {
205
- name: entry[:name],
206
- path: path.empty? ? entry[:name] : "#{path}/#{entry[:name]}",
207
- type: entry[:type].to_s,
208
- sha: entry[:oid],
209
- mode: entry[:filemode].to_s(8)
210
- }
211
-
212
- if entry[:type] == :tree
213
- export_tree(@repo.lookup(entry[:oid]), entry_data[:path])
214
- elsif entry[:type] == :blob
215
- export_blob(entry[:oid], entry_data[:path])
216
- end
217
-
218
- entry_data
219
- end
220
-
221
- write_json("trees/#{tree.oid}.json", { sha: tree.oid, path: path, entries: entries })
222
- end
223
-
224
- def export_blob(oid, path)
225
- return if @processed_blobs.include?(oid)
226
- @processed_blobs.add(oid)
227
-
228
- blob = @repo.lookup(oid)
229
- data = {
230
- sha: oid,
231
- path: path,
232
- size: blob.size,
233
- binary: blob.binary?,
234
- content: blob.binary? ? nil : safe_content(blob.content),
235
- truncated: !blob.binary? && blob.size > 100_000
236
- }
237
-
238
- write_json("blobs/#{oid}.json", data)
239
- end
240
-
241
- def safe_content(content)
242
- return content[0..100_000] + "\n... [truncated]" if content.size > 100_000
243
- content.encode('UTF-8', invalid: :replace, undef: :replace, replace: '�')
244
- end
245
-
246
- def write_json(path, data)
247
- File.write(File.join(@output_dir, path), JSON.pretty_generate(data))
248
- end
249
- end
250
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -37,20 +37,6 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
- - !ruby/object:Gem::Dependency
41
- name: open3
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '0.1'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '0.1'
54
40
  - !ruby/object:Gem::Dependency
55
41
  name: rugged
56
42
  requirement: !ruby/object:Gem::Requirement
@@ -66,20 +52,20 @@ dependencies:
66
52
  - !ruby/object:Gem::Version
67
53
  version: '1.0'
68
54
  - !ruby/object:Gem::Dependency
69
- name: time
55
+ name: webrick
70
56
  requirement: !ruby/object:Gem::Requirement
71
57
  requirements:
72
58
  - - "~>"
73
59
  - !ruby/object:Gem::Version
74
- version: '0.1'
60
+ version: '1.8'
75
61
  type: :runtime
76
62
  prerelease: false
77
63
  version_requirements: !ruby/object:Gem::Requirement
78
64
  requirements:
79
65
  - - "~>"
80
66
  - !ruby/object:Gem::Version
81
- version: '0.1'
82
- description: A static site generated for git repositories.
67
+ version: '1.8'
68
+ description: Browse your git history locally with a GitHub-like interface.
83
69
  email:
84
70
  - mo@mokhan.ca
85
71
  executables:
@@ -92,16 +78,20 @@ files:
92
78
  - Rakefile
93
79
  - exe/gitem
94
80
  - lib/gitem.rb
95
- - lib/gitem/index.html.erb
81
+ - lib/gitem/cli.rb
82
+ - lib/gitem/generator.rb
83
+ - lib/gitem/index.html
84
+ - lib/gitem/server.rb
96
85
  - lib/gitem/version.rb
97
86
  - sig/gitem.rbs
98
- homepage: https://mokhan.ca/xlgmokha/gitem
87
+ homepage: https://github.com/xlgmokha/gitem
99
88
  licenses:
100
89
  - MIT
101
90
  metadata:
102
91
  allowed_push_host: https://rubygems.org
103
- homepage_uri: https://mokhan.ca/xlgmokha/gitem
104
- source_code_uri: https://mokhan.ca/xlgmokha/gitem
92
+ homepage_uri: https://github.com/xlgmokha/gitem
93
+ source_code_uri: https://github.com/xlgmokha/gitem
94
+ changelog_uri: https://github.com/xlgmokha/gitem/blob/main/CHANGELOG.md
105
95
  rdoc_options: []
106
96
  require_paths:
107
97
  - lib
@@ -118,5 +108,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
108
  requirements: []
119
109
  rubygems_version: 4.0.1
120
110
  specification_version: 4
121
- summary: A static site generated for git repositories.
111
+ summary: A static site generator for git repositories.
122
112
  test_files: []
File without changes