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 +7 -0
- data/.bundle/config.template +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +20 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +75 -0
- data/README.md +119 -0
- data/bin/rubocop +7 -0
- data/bin/yard +7 -0
- data/exe/skap +5 -0
- data/lib/skap/cli/help.rb +59 -0
- data/lib/skap/cli/init.rb +23 -0
- data/lib/skap/cli/sources.rb +59 -0
- data/lib/skap/cli/works.rb +175 -0
- data/lib/skap/cli.rb +40 -0
- data/lib/skap/command.rb +28 -0
- data/lib/skap/files/menu.rb +17 -0
- data/lib/skap/files/sources.rb +25 -0
- data/lib/skap/files/versions.rb +48 -0
- data/lib/skap/string_utils.rb +49 -0
- data/lib/skap/version.rb +5 -0
- data/lib/skap/yaml_file.rb +24 -0
- data/lib/skap.rb +4 -0
- data/menu.yaml +48 -0
- data/skap.gemspec +33 -0
- metadata +139 -0
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
|
data/.gitignore
ADDED
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
data/bin/yard
ADDED
data/exe/skap
ADDED
@@ -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"
|
data/lib/skap/command.rb
ADDED
@@ -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
|
data/lib/skap/version.rb
ADDED
@@ -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
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: []
|