skap 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b24eb7212466fb9d393d5d3847d279bf502c6ea798d5b6169443df4c704949e8
4
+ data.tar.gz: 5c45d5fd262e0b9ef739f8d4145ca6aab80ada327e2c211b6d7aa0449a2d19c7
5
+ SHA512:
6
+ metadata.gz: cf53061aa0925dbc45a03068bf4565f7c6de5fa1033ea946a209667cec3194e16791bd0af89001f1bf66281b811eebe5515bf31eecd605f6071dea4ed05977f8
7
+ data.tar.gz: 4dff3e1661ea6a77720be3c600add6a5af121c25490f579b1c66f34e7a129a627d4e44928b7ee75bd91da14459d43fb339afde31a7e16b1f0f035fbd0f612cc7
@@ -0,0 +1,7 @@
1
+ ---
2
+ BUNDLE_GLOBAL_GEM_CACHE: true
3
+ BUNDLE_IGNORE_FUNDING_REQUESTS: true
4
+ BUNDLE_IGNORE_MESSAGES: true
5
+ BUNDLE_SHEBANG: ruby
6
+ BUNDLE_SILENCE_DEPRECATIONS: false
7
+ BUNDLE_SILENCE_ROOT_WARNING: false
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .bundle/config
2
+ .yardoc
3
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ inherit_gem:
2
+ rubocop-configs:
3
+ - _all_cops.yml
4
+ - _ruby.yml
5
+ - gemspec.yml
6
+ - performance.yml
7
+
8
+ AllCops:
9
+ TargetRubyVersion: 3.3
10
+
11
+ Gemspec/DevelopmentDependencies:
12
+ Enabled: false
13
+
14
+ Lint/Debugger:
15
+ DebuggerMethods:
16
+ # Exclude "puts" from this list.
17
+ Kernel: [warn, binding.irb, p, Kernel.binding.irb]
18
+
19
+ Style/ClassAndModuleChildren:
20
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gemspec
5
+
6
+ # Turn off warnging: %{gem} was loaded from the standard library,
7
+ # but will no longer be part of the default gems starting from Ruby 3.5.0
8
+ gem "rdoc"
9
+
10
+ gem "prism" # For parser_prism in Rubocop.
11
+ gem "rubocop-configs", require: false, git: "https://github.com/crosspath/rubocop-configs.git"
data/Gemfile.lock ADDED
@@ -0,0 +1,75 @@
1
+ GIT
2
+ remote: https://github.com/crosspath/rubocop-configs.git
3
+ revision: ef433c9f20720610194773a31ca1b89a7888e5e7
4
+ specs:
5
+ rubocop-configs (0.17.0)
6
+ rake (>= 13.0)
7
+
8
+ PATH
9
+ remote: .
10
+ specs:
11
+ skap (1.0.0)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ ast (2.4.2)
17
+ diff-lcs (1.5.1)
18
+ json (2.7.2)
19
+ language_server-protocol (3.17.0.3)
20
+ parallel (1.26.3)
21
+ parser (3.3.4.2)
22
+ ast (~> 2.4.1)
23
+ racc
24
+ prism (1.0.0)
25
+ psych (5.1.2)
26
+ stringio
27
+ racc (1.8.1)
28
+ rainbow (3.1.1)
29
+ rake (13.2.1)
30
+ rdoc (6.7.0)
31
+ psych (>= 4.0.0)
32
+ regexp_parser (2.9.2)
33
+ rspec-core (3.13.1)
34
+ rspec-support (~> 3.13.0)
35
+ rspec-expectations (3.13.2)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.13.0)
38
+ rspec-support (3.13.1)
39
+ rubocop (1.66.1)
40
+ json (~> 2.3)
41
+ language_server-protocol (>= 3.17.0)
42
+ parallel (~> 1.10)
43
+ parser (>= 3.3.0.2)
44
+ rainbow (>= 2.2.2, < 4.0)
45
+ regexp_parser (>= 2.4, < 3.0)
46
+ rubocop-ast (>= 1.32.2, < 2.0)
47
+ ruby-progressbar (~> 1.7)
48
+ unicode-display_width (>= 2.4.0, < 3.0)
49
+ rubocop-ast (1.32.2)
50
+ parser (>= 3.3.1.0)
51
+ rubocop-performance (1.22.1)
52
+ rubocop (>= 1.48.1, < 2.0)
53
+ rubocop-ast (>= 1.31.1, < 2.0)
54
+ ruby-progressbar (1.13.0)
55
+ stringio (3.1.1)
56
+ unicode-display_width (2.5.0)
57
+ yard (0.9.37)
58
+
59
+ PLATFORMS
60
+ ruby
61
+ x86_64-linux-gnu
62
+
63
+ DEPENDENCIES
64
+ prism
65
+ rdoc
66
+ rspec-core (~> 3.13)
67
+ rspec-expectations (~> 3.13)
68
+ rubocop (~> 1.66)
69
+ rubocop-configs!
70
+ rubocop-performance (~> 1.22)
71
+ skap!
72
+ yard (~> 0.9)
73
+
74
+ BUNDLED WITH
75
+ 2.5.19
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Skap — document management system
2
+
3
+ Word "skap" is a variation of Germanic words "skab"/"schap"/"schaf".
4
+ Here it means storage closet with documents or books.
5
+
6
+ Skap manages local copy of source documents published in git repositories.
7
+ You may use Skap to track changes in source documents and to store revisions (versions)
8
+ of your works based on these source documents, for example abstracts.
9
+
10
+ ## Available commands
11
+
12
+ ```plain
13
+ help
14
+ Show help message about supported commands.
15
+ init
16
+ init DIRECTORY_PATH
17
+ Create configuration files in current directory or in DIRECTORY_PATH.
18
+ sources
19
+ add DIRECTORY REPO BRANCH
20
+ Add git submodule into DIRECTORY from REPO and track BRANCH by default.
21
+ delete DIRECTORY
22
+ Delete git submodule from DIRECTORY.
23
+ update
24
+ update DIRECTORY ...
25
+ Update git submodule from upstream in DIRECTORY or in all git submodules if DIRECTORY
26
+ is not specified. You may pass one or more directory paths (DIRECTORY ...) to update their
27
+ contents.
28
+ works
29
+ covered
30
+ List files of sources which have been used for published works.
31
+ ignored
32
+ List ignored files.
33
+ outdated
34
+ List documents which may contain outdated information.
35
+ publish DOCUMENT
36
+ publish DOCUMENT FILE_PATH ...
37
+ Save record about abstract (DOCUMENT) and pass list of file paths of sources (FILE_PATH ...)
38
+ which relate to this abstract. You should prepend minus sign to file path to exclude it from
39
+ list of related sources. Examples:
40
+ works publish _/docker/compose.md docs.docker.com/content/manuals/compose/**/*.md
41
+ works publish _/docker/compose.md -docs.docker.com/**/*.md
42
+ uncovered
43
+ List files of sources which have NOT been used for published works.
44
+ unknown
45
+ List files of sources which may be used for works or ignored.
46
+ ```
47
+
48
+ ## Suggested workflow
49
+
50
+ 1. Install Skap: `gem install skap`
51
+ 2. Initialize storage: `skap init ~/docs` (pass any path to storage)
52
+ 3. Add sources: `skap sources add docker https://github.com/docker/docs.git main`
53
+ (see command description in "Available commands")
54
+ 4. Look into downloaded source files and fill entries in file "sources.yaml" in your storage.
55
+ 5. Create file with your text (here it's known as "work") that relates somehow to source documents.
56
+ 6. Save revision of source documents with current state of "work":
57
+ `skap works publish _/docker/overview.md docker/content/get-started/docker-overview.md`
58
+ (here "_/docker/overview.md" is path to your "work")
59
+ 7. Commit changes into current git repository in your storage: `git commit ...` (see manual for git)
60
+
61
+ Additionally you may push your changes into remote repository — see manual for git:
62
+ git remote, git push.
63
+
64
+ ## Additional files in storage
65
+
66
+ You should store changes in these files with "git commit".
67
+
68
+ Schema of file "sources.yaml":
69
+
70
+ ```yaml
71
+ %directory-name%:
72
+ file-extensions: [%ext%, %ext%]
73
+ ignored:
74
+ - %file-path-pattern-in-this-directory%
75
+ indexed:
76
+ - %file-path-pattern-in-this-directory%
77
+ ```
78
+
79
+ Example of file "sources.yaml":
80
+
81
+ ```yaml
82
+ docker:
83
+ file-extensions: [md, yaml]
84
+ ignored:
85
+ - "*.md"
86
+ - compose.yaml
87
+ indexed:
88
+ - content/get-started/**/*.md
89
+ ```
90
+
91
+ Schema of file "versions.yaml":
92
+
93
+ ```yaml
94
+ %work-file-path%:
95
+ date: %iso-date%
96
+ sources:
97
+ %source-file-path%:
98
+ date: %iso-date%
99
+ sha: %commit-sha%
100
+ ```
101
+
102
+ Example of file "versions.yaml":
103
+
104
+ ```yaml
105
+ _/docker/overview.md:
106
+ date: 2024-12-31
107
+ sources:
108
+ docker/content/get-started/docker-overview.md:
109
+ date: 2024-12-31
110
+ sha: fc77b05ffe69070796a6a8630802e62b75304455
111
+ ```
112
+
113
+ ## Development
114
+
115
+ ```shell
116
+ bin/rubocop -A --only Style/FrozenStringLiteralComment,Layout/EmptyLineAfterMagicComment
117
+ bin/rubocop -a
118
+ bin/build
119
+ ```
data/bin/rubocop ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ load(Gem.bin_path("rubocop", "rubocop"))
data/bin/yard ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ load(Gem.bin_path("yard", "yard"))
data/exe/skap ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/skap"
5
+ Skap::CLI.start
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module CLI::Help
5
+ include Command
6
+ extend self
7
+
8
+ # @return [void]
9
+ def start
10
+ menu = Files::Menu.new
11
+
12
+ puts "Usage:", ""
13
+
14
+ show_menu(menu, $DEFAULT_OUTPUT.winsize.last)
15
+ end
16
+
17
+ private
18
+
19
+ # @param text [String]
20
+ # @param indent [String]
21
+ # @param max_width [Integer]
22
+ # @return [void]
23
+ def output_menu_item_text(text, indent, max_width)
24
+ res = []
25
+
26
+ if text.is_a?(Array)
27
+ text.each { |paragraph| res += StringUtils.break_by_words(paragraph, max_width) }
28
+ else
29
+ res += StringUtils.break_by_words(text, max_width)
30
+ end
31
+
32
+ res.each { |line| puts "#{indent}#{line}" }
33
+ end
34
+
35
+ # @param menu [Files::Menu]
36
+ # @param width [Integer]
37
+ # @return [void]
38
+ def show_menu(menu, width) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
39
+ section_indent = " " * 4
40
+ command_indent = " " * 8
41
+ within_section = width - 4
42
+ within_command = width - 8
43
+
44
+ menu.each do |item|
45
+ puts item["cmd"]
46
+
47
+ output_menu_item_text(item["text"], section_indent, within_section) if item.key?("text")
48
+
49
+ next unless item.key?("children")
50
+
51
+ item["children"].each do |subitem|
52
+ [*subitem["cmd"]].each { |cmd| puts "#{section_indent}#{cmd}" }
53
+
54
+ output_menu_item_text(subitem["text"], command_indent, within_command)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module CLI::Init
5
+ include Command
6
+ extend self
7
+
8
+ # @param dir [String]
9
+ # @param args [Array<String>]
10
+ # @return [void]
11
+ def start(dir, args)
12
+ assert_empty_options(args)
13
+
14
+ FileUtils.mkdir_p(dir)
15
+
16
+ shell("git init", dir:)
17
+ shell("echo '---\n' > #{Files::Sources.file_name}", dir:)
18
+ shell("echo '---\n' > #{Files::Versions.file_name}", dir:)
19
+
20
+ puts "Git repo initialized in #{dir}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module CLI::Sources
5
+ include Command
6
+ extend self
7
+
8
+ # @param command [String]
9
+ # @param args [Array<String>]
10
+ # @return [void]
11
+ def start(command, args)
12
+ assert_cwd
13
+
14
+ case command
15
+ when "add" then add(*args)
16
+ when "delete" then delete(*args)
17
+ when "update" then update(*args)
18
+ else
19
+ raise ArgumentError, "Unknown command: #{command}"
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # @param dir [String]
26
+ # @param repo [String]
27
+ # @param branch [String]
28
+ # @param rest [Array<String>]
29
+ # @return [void]
30
+ def add(dir, repo, branch, *rest)
31
+ assert_empty_options(rest)
32
+
33
+ return unless shell("git submodule add -b #{branch} --depth 3 -- #{repo} #{dir}")
34
+
35
+ sources_data = load_file(SOURCES)
36
+ sources_data[dir] = {"file-extensions" => [], "ignored" => [], "indexed" => []}
37
+
38
+ sources_data = sources_data.sort_by(&:first).to_h
39
+ File.write(SOURCES, Psych.dump(sources_data, line_width: 100))
40
+ end
41
+
42
+ # @param dir [String]
43
+ # @param rest [Array<String>]
44
+ # @return [void]
45
+ def delete(dir, *rest)
46
+ assert_empty_options(rest)
47
+
48
+ shell("git submodule deinit -f -- #{dir} && git rm -f #{dir} && rm -rf .git/modules/#{dir}")
49
+ end
50
+
51
+ # @param dirs [Array<String>]
52
+ # @return [void]
53
+ def update(*dirs)
54
+ path_arg = dirs.empty? ? "" : "-- #{dirs.join(" ")}"
55
+
56
+ shell("git submodule update --checkout --single-branch --recursive #{path_arg}")
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module CLI::Works
5
+ include Command
6
+ extend self
7
+
8
+ # @param command [String]
9
+ # @param args [Array<String>]
10
+ # @return [void]
11
+ def start(command, args) # rubocop:disable Metrics/MethodLength
12
+ assert_cwd
13
+
14
+ case command
15
+ when "covered" then covered(*args)
16
+ when "ignored" then ignored(*args)
17
+ when "publish" then publish(*args)
18
+ when "outdated" then outdated(*args)
19
+ when "uncovered" then uncovered(*args)
20
+ when "unknown" then unknown(*args)
21
+ else
22
+ raise ArgumentError, "Unknown command: #{command}"
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @param path_patterns_as_hash [Hash<String, Array<String>>]
29
+ # @return [Array<String>]
30
+ def collect_files(path_patterns_as_hash)
31
+ flatten_path_patterns(path_patterns_as_hash).flat_map do |path_pattern|
32
+ find_files_by_pattern(path_pattern)
33
+ end
34
+ end
35
+
36
+ # @param path [String]
37
+ # @return [String]
38
+ def commit_sha(path)
39
+ dir, file = File.split(path)
40
+
41
+ shell("git rev-parse HEAD -- #{file}", dir:).split("\n").first
42
+ end
43
+
44
+ # @param dirs [Array<String>]
45
+ # @return [void]
46
+ def covered(*dirs)
47
+ sources = Files::Sources.new.extract("indexed", dir_names_without_slash(dirs))
48
+
49
+ puts (Files::Versions.new.covered_sources & collect_files(sources)).sort
50
+ end
51
+
52
+ # @param dirs [Array<String>]
53
+ # @return [Array<String>]
54
+ def dir_names_with_slash(dirs)
55
+ dirs.map { |x| x.end_with?("/") ? x : "#{x}/" }
56
+ end
57
+
58
+ # @param dirs [Array<String>]
59
+ # @return [Array<String>]
60
+ def dir_names_without_slash(dirs)
61
+ dirs.map { |x| x.end_with?("/") ? x.chop : x }
62
+ end
63
+
64
+ # @param path_pattern [String]
65
+ # @return [Array<String>]
66
+ def find_files_by_pattern(path_pattern)
67
+ if path_pattern.include?("*")
68
+ result = Dir.glob(path_pattern, base: CURRENT_DIR)
69
+ raise ArgumentError, "No files found for path pattern \"#{path_pattern}\"" if result.empty?
70
+
71
+ result
72
+ else
73
+ raise ArgumentError, "File \"#{path_pattern}\" doesn't exist" if !File.exist?(path_pattern)
74
+
75
+ path_pattern
76
+ end
77
+ end
78
+
79
+ # @param hash [Hash<String, Array<String>>]
80
+ # @return [Array<String>]
81
+ def flatten_path_patterns(hash)
82
+ hash.flat_map { |dir, paths| paths.map { |x| File.join(dir, x) } }
83
+ end
84
+
85
+ # @param dirs [Array<String>]
86
+ # @return [void]
87
+ def ignored(*dirs)
88
+ sources = Files::Sources.new.extract("ignored", dir_names_without_slash(dirs))
89
+
90
+ puts collect_files(sources).sort
91
+ end
92
+
93
+ # @param dirs [Array<String>]
94
+ # @return [void]
95
+ def outdated(*dirs)
96
+ dirs = dir_names_with_slash(dirs)
97
+
98
+ outdated_documents =
99
+ Files::Versions.new.outdated_documents do |source_path|
100
+ dirs.empty? || source_path.start_with?(*dirs) ? commit_sha(source_path) : nil
101
+ end
102
+
103
+ outdated_documents.each do |(doc_path, outdated_sources)|
104
+ puts doc_path, outdated_sources.map { |x| "* #{x}" }, ""
105
+ end
106
+ end
107
+
108
+ # @param document_path [String]
109
+ # @param sources_paths [Array<String>]
110
+ # @return [void]
111
+ def publish(document_path, *sources_paths)
112
+ excluded_file_paths = sources_paths.select { |x| x.start_with?("-") }
113
+ added_file_paths = sources_paths - excluded_file_paths
114
+
115
+ excluded_file_paths.map! { |x| x[1..] }
116
+
117
+ versions = Files::Versions.new
118
+ doc = versions.find_document(document_path) || {}
119
+
120
+ update_document_info(doc, added_file_paths, excluded_file_paths)
121
+ versions.add_document(document_path, doc)
122
+
123
+ puts "Version updated for #{document_path} in #{Files::Versions.file_name}"
124
+ end
125
+
126
+ # @param path_patterns_as_hash [Hash<String, Hash<String, Array<String>>>]
127
+ # @return [Array<String>]
128
+ def trackable_files(sources_data)
129
+ sources_data
130
+ .extract("file-extensions")
131
+ .transform_values { |v| v.join(",") }
132
+ .reduce([]) { |a, (dir, ext)| a + Dir.glob("#{dir}/**/*.{#{ext}}") }
133
+ end
134
+
135
+ # @param dirs [Array<String>]
136
+ # @return [void]
137
+ def uncovered(*dirs)
138
+ sources = Files::Sources.new.extract("indexed", dir_names_without_slash(dirs))
139
+
140
+ puts (collect_files(sources) - Files::Versions.new.covered_sources.to_a).sort
141
+ end
142
+
143
+ # @param dirs [Array<String>]
144
+ # @return [void]
145
+ def unknown(*dirs)
146
+ sources_data = Files::Sources.new
147
+ sources_data.select_directories!(dir_names_without_slash(dirs)) if !dirs.empty?
148
+
149
+ all_files = trackable_files(sources_data)
150
+ ignored_files = collect_files(sources_data.extract("ignored"))
151
+ indexed_files = collect_files(sources_data.extract("indexed"))
152
+
153
+ puts (all_files - ignored_files - indexed_files).sort
154
+ end
155
+
156
+ # @param doc [Hash<String, Object>]
157
+ # @param added_file_paths [Array<String>]
158
+ # @param excluded_file_paths [Array<String>]
159
+ # @return [void]
160
+ def update_document_info(doc, added_file_paths, excluded_file_paths)
161
+ today = Time.now.strftime("%F")
162
+
163
+ doc["date"] = today
164
+ doc["sources"] ||= {}
165
+
166
+ added_file_paths.each do |path|
167
+ entry = (doc["sources"][path] ||= {})
168
+ entry["date"] = today
169
+ entry["sha"] = commit_sha(path)
170
+ end
171
+
172
+ excluded_file_paths.each { |x| doc["sources"].delete(x) }
173
+ end
174
+ end
175
+ end
data/lib/skap/cli.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "fileutils"
5
+ require "io/console"
6
+ require "psych"
7
+
8
+ require_relative "command"
9
+ require_relative "yaml_file"
10
+
11
+ require_relative "files/menu"
12
+ require_relative "files/sources"
13
+ require_relative "files/versions"
14
+
15
+ module Skap
16
+ module CLI
17
+ # @param argv [Array<String>]
18
+ # @return [void]
19
+ def self.start(argv = ARGV)
20
+ section, command, *rest = argv
21
+
22
+ case section
23
+ when "works" then CLI::Works.start(command, rest)
24
+ when "help", "--help", "-h", nil then CLI::Help.start
25
+ when "init" then CLI::Init.start(command, rest)
26
+ when "sources" then CLI::Sources.start(command, rest)
27
+ else
28
+ raise ArgumentError, "Unknown section: #{section}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # TODO: Command "clone" - it calls "git clone" & initializes git submodules.
35
+ # git clone --recurse <URL> <directory>
36
+
37
+ require_relative "cli/help"
38
+ require_relative "cli/init"
39
+ require_relative "cli/sources"
40
+ require_relative "cli/works"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module Command
5
+ CURRENT_DIR = Dir.pwd.freeze
6
+
7
+ private
8
+
9
+ # @return [void]
10
+ def assert_cwd
11
+ raise "Current dir isn't a repo for sources" if !File.exist?(Files::Sources.file_name)
12
+ end
13
+
14
+ # @param args [Array<String>]
15
+ # @return [void]
16
+ def assert_empty_options(args)
17
+ raise ArgumentError, "Unknown options: #{args.inspect}" if !args.empty?
18
+ end
19
+
20
+ # @param cmd [String]
21
+ # @param dir [String]
22
+ # @return [String]
23
+ def shell(cmd, dir: "")
24
+ dir = dir == "~" ? Dir.home : File.absolute_path(dir, CURRENT_DIR)
25
+ `cd #{dir} && #{cmd}`.strip
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Skap
6
+ module Files
7
+ class Menu < YAMLFile
8
+ extend Forwardable
9
+
10
+ SKAP_DIR = File.expand_path("../../..", __dir__).freeze
11
+
12
+ self.file_name = File.join(SKAP_DIR, "menu.yaml")
13
+
14
+ def_delegators :file, :each
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module Files
5
+ class Sources < YAMLFile
6
+ self.file_name = "sources.yaml"
7
+
8
+ # @param key [String]
9
+ # @param dirs [Array<String>]
10
+ # @return [Hash<String, Array<String>>]
11
+ def extract(key, dirs = [])
12
+ sources = dirs.empty? ? file : file.slice(*dirs)
13
+ sources
14
+ .transform_values { |v| v[key] }
15
+ .reject { |_, v| v.nil? || v.empty? }
16
+ end
17
+
18
+ # @param dirs [Array<String>]
19
+ # @return [Hash<String, Hash<String, Array<String>>>]
20
+ def select_directories!(dirs)
21
+ @file = file.slice(*dirs)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module Files
5
+ class Versions < YAMLFile
6
+ self.file_name = "versions.yaml"
7
+
8
+ # @param document_path [String]
9
+ # @param document [Hash<String, Object>]
10
+ # @return [void]
11
+ def add_document(document_path, document)
12
+ file[document_path] = document
13
+ @file = file.sort_by(&:first).to_h
14
+
15
+ File.write(self.class.file_name, Psych.dump(file, line_width: 100))
16
+ end
17
+
18
+ # @return [Set<String>]
19
+ def covered_sources
20
+ result = Set.new
21
+
22
+ file.each_value { |value| result.merge(value["sources"].keys) }
23
+
24
+ result
25
+ end
26
+
27
+ # @param document_path [String]
28
+ # @return [Hash<String, Object>]
29
+ def find_document(document_path)
30
+ file[document_path]
31
+ end
32
+
33
+ # @return [Array<Array(String, Array<String>)>]
34
+ def outdated_documents
35
+ sources_sha = {}
36
+
37
+ file.filter_map do |doc_path, hash|
38
+ outdated_sources =
39
+ hash["sources"].filter_map do |source_path, source_data|
40
+ sha = (sources_sha[source_path] ||= yield(source_path))
41
+ source_path if !sha.nil? && sha != source_data["sha"]
42
+ end
43
+ [doc_path, outdated_sources] if !outdated_sources.empty?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ module StringUtils
5
+ extend self
6
+
7
+ # @param paragraph [String]
8
+ # @param max_width [Integer]
9
+ # @return [Array<String>]
10
+ def break_by_words(paragraph, max_width)
11
+ words = paragraph.split
12
+ parts = [[]]
13
+ line_width = 0
14
+
15
+ while !words.empty?
16
+ word = words.shift
17
+ new_line_width = (line_width == 0 ? line_width : line_width + 1) + word.size
18
+ line_width = add_word(word, parts, new_line_width, max_width)
19
+ end
20
+
21
+ parts.pop if parts.last.empty?
22
+
23
+ parts.map { |line| line.join(" ") }
24
+ end
25
+
26
+ private
27
+
28
+ # @param word [String]
29
+ # @param parts [Array<String>]
30
+ # @param new_line_width [Integer]
31
+ # @param max_width [Integer]
32
+ # @return [Integer] Current line width
33
+ def add_word(word, parts, new_line_width, max_width) # rubocop:disable Metrics/MethodLength
34
+ if new_line_width <= max_width
35
+ parts.last << word
36
+
37
+ if new_line_width == max_width
38
+ parts << []
39
+ 0
40
+ else
41
+ new_line_width
42
+ end
43
+ else
44
+ parts << [word]
45
+ word.size
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skap
4
+ class YAMLFile
5
+ class << self
6
+ attr_accessor :file_name
7
+
8
+ private :file_name=
9
+ end
10
+
11
+ def initialize
12
+ @file = load_file
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :file
18
+
19
+ # @return [Hash<String, Object>]
20
+ def load_file
21
+ Psych.load_file(self.class.file_name, symbolize_names: false) || {}
22
+ end
23
+ end
24
+ end
data/lib/skap.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "skap/cli"
4
+ require_relative "skap/version"
data/menu.yaml ADDED
@@ -0,0 +1,48 @@
1
+ ---
2
+ - cmd: help
3
+ text: "Show help message about supported commands."
4
+ - cmd: ["init", "init DIRECTORY_PATH"]
5
+ text: "Create configuration files in current directory or in DIRECTORY_PATH."
6
+ - cmd: sources
7
+ children:
8
+ - cmd: "add DIRECTORY REPO BRANCH"
9
+ text: "Add git submodule into DIRECTORY from REPO and track BRANCH by default."
10
+ - cmd: "delete DIRECTORY"
11
+ text: "Delete git submodule from DIRECTORY."
12
+ - cmd: ["update", "update DIRECTORY ..."]
13
+ text: >
14
+ Update git submodule from upstream in DIRECTORY or in all git submodules if DIRECTORY
15
+ is not specified. You may pass one or more directory paths (DIRECTORY ...) to update
16
+ their contents.
17
+ - cmd: works
18
+ children:
19
+ - cmd: ["covered", "covered DIRECTORY ..."]
20
+ text: >
21
+ List files of sources which have been used for published works. Pass one or more directory
22
+ paths (DIRECTORY ...) to show files only in these directories.
23
+ - cmd: ["ignored", "ignored DIRECTORY ..."]
24
+ text: >
25
+ List ignored files. Pass one or more directory paths (DIRECTORY ...) to show files only in
26
+ these directories.
27
+ - cmd: ["outdated", "outdated DIRECTORY ..."]
28
+ text: >
29
+ List documents which may contain outdated information. Pass one or more directory paths
30
+ (DIRECTORY ...) to show files only in these directories.
31
+ - cmd: ["publish DOCUMENT", "publish DOCUMENT FILE_PATH ..."]
32
+ text:
33
+ - >
34
+ Save record about abstract (DOCUMENT) and pass list of file paths of sources
35
+ (FILE_PATH ...) which relate to this abstract. You should prepend minus sign to file path
36
+ to exclude it from list of related sources. Examples:
37
+ - >
38
+ works publish _/docker/compose.md docs.docker.com/content/manuals/compose/**/*.md
39
+ - >
40
+ works publish _/docker/compose.md -docs.docker.com/**/*.md
41
+ - cmd: ["uncovered", "uncovered DIRECTORY ..."]
42
+ text: >
43
+ List files of sources which have NOT been used for published works. Pass one or more
44
+ directory paths (DIRECTORY ...) to show files only in these directories.
45
+ - cmd: ["unknown", "unknown DIRECTORY ..."]
46
+ text: >
47
+ List files of sources which may be used for works or ignored. Pass one or more directory
48
+ paths (DIRECTORY ...) to show files only in these directories.
data/skap.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/skap/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "skap"
7
+ spec.version = Skap::VERSION
8
+ spec.summary = ""
9
+ # spec.description = ""
10
+ spec.authors = ["Evgeniy Nochevnov"]
11
+ spec.homepage = "https://github.com/crosspath/skap"
12
+ spec.license = "MIT"
13
+
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
15
+
16
+ spec.add_development_dependency("rspec-core", "~> 3.13")
17
+ spec.add_development_dependency("rspec-expectations", "~> 3.13")
18
+ spec.add_development_dependency("rubocop", "~> 1.66")
19
+ spec.add_development_dependency("rubocop-performance", "~> 1.22")
20
+ spec.add_development_dependency("yard", "~> 0.9")
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+
25
+ spec.files =
26
+ Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").grep_v(%r{^spec/})
28
+ end
29
+
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skap
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Evgeniy Nochevnov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-expectations
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.66'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.66'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-performance
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.22'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.22'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.9'
83
+ description:
84
+ email:
85
+ executables:
86
+ - skap
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".bundle/config.template"
91
+ - ".gitignore"
92
+ - ".rubocop.yml"
93
+ - Gemfile
94
+ - Gemfile.lock
95
+ - README.md
96
+ - bin/rubocop
97
+ - bin/yard
98
+ - exe/skap
99
+ - lib/skap.rb
100
+ - lib/skap/cli.rb
101
+ - lib/skap/cli/help.rb
102
+ - lib/skap/cli/init.rb
103
+ - lib/skap/cli/sources.rb
104
+ - lib/skap/cli/works.rb
105
+ - lib/skap/command.rb
106
+ - lib/skap/files/menu.rb
107
+ - lib/skap/files/sources.rb
108
+ - lib/skap/files/versions.rb
109
+ - lib/skap/string_utils.rb
110
+ - lib/skap/version.rb
111
+ - lib/skap/yaml_file.rb
112
+ - menu.yaml
113
+ - skap.gemspec
114
+ homepage: https://github.com/crosspath/skap
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ homepage_uri: https://github.com/crosspath/skap
119
+ source_code_uri: https://github.com/crosspath/skap
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 3.3.0
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.5.19
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: ''
139
+ test_files: []