tetra 2.0.9 → 2.0.10
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 +4 -4
- data/CONTRIBUTING.md +39 -9
- data/README.md +6 -3
- data/bin/tetra +1 -3
- data/lib/tetra/constants.rb +1 -1
- data/lib/tetra/facades/ant.rb +1 -0
- data/lib/tetra/facades/bash.rb +2 -1
- data/lib/tetra/facades/git.rb +11 -15
- data/lib/tetra/facades/mvn.rb +2 -1
- data/lib/tetra/facades/process_runner.rb +50 -54
- data/lib/tetra/facades/tar.rb +2 -1
- data/lib/tetra/facades/unzip.rb +2 -1
- data/lib/tetra/maven_website.rb +7 -5
- data/lib/tetra/packages/scriptable.rb +1 -4
- data/lib/tetra/pom_getter.rb +4 -2
- data/lib/tetra/project.rb +16 -21
- data/lib/tetra/project_initer.rb +1 -1
- data/lib/tetra/ui/change_sources_subcommand.rb +8 -8
- data/lib/tetra/ui/generate_all_subcommand.rb +1 -1
- data/lib/tetra/ui/generate_spec_subcommand.rb +1 -1
- data/lib/tetra/ui/get_pom_subcommand.rb +5 -8
- data/lib/tetra/ui/init_subcommand.rb +2 -4
- data/lib/tetra/ui/subcommand.rb +14 -14
- data/lib/tetra/version.rb +2 -1
- data/lib/tetra/version_matcher.rb +1 -1
- metadata +6 -132
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44bc9f5ff042bf65fddacc23815cc68ec600a00ea27434b44af4bd548f459296
|
|
4
|
+
data.tar.gz: c1fcab0d1c9495ae6bba7fd22031627dbedfe0aa21a1837dc054ad2b9ec478dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67088cf14db8bd5ce2b488feb99878f142eed9630e66afce9cbaf8e19f706fbfe978f5fdfd9ed24a187b226d1649b86a015ded57926464cc22bb17625d3d8a9e
|
|
7
|
+
data.tar.gz: e7c38c4d2cd8182e9b9f9c0d35598b52f1a2a92cb1dec35c3fcb1bc79573acd5aa13aff220c0611768bf341cd51a2ce1e697cf3717ffb72c833758bda3d3ca50
|
data/CONTRIBUTING.md
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
To develop tetra, you will need:
|
|
6
6
|
|
|
7
|
-
* to install Bundler and Ruby development headers.
|
|
8
|
-
|
|
9
|
-
* to get development dependencies: `bundle install`;
|
|
7
|
+
* to install Bundler and Ruby development headers.
|
|
8
|
+
* to get development dependencies: `bundle install`.
|
|
10
9
|
|
|
11
10
|
To install a development version of tetra use:
|
|
12
11
|
|
|
@@ -14,20 +13,51 @@ To install a development version of tetra use:
|
|
|
14
13
|
sudo rake install
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
## Tests
|
|
17
|
+
|
|
18
|
+
To run tests, simply run `rake` or `bundle exec rake` which runs the full set of tests.
|
|
19
|
+
|
|
20
|
+
> [!NOTE]
|
|
21
|
+
> Tests are divided into fine (more similar to unit tests) and coarse (more similar to integration tests).
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# only run fine tests
|
|
25
|
+
bundle exec rspec spec/lib/fine/*
|
|
26
|
+
|
|
27
|
+
# only run coarse tests
|
|
28
|
+
bundle exec rspec spec/lib/coarse/*
|
|
29
|
+
|
|
30
|
+
# run all coarse tests with and show example names
|
|
31
|
+
bundle exec rspec spec/lib/coarse/* --format documentation
|
|
32
|
+
|
|
33
|
+
# only run selected tests
|
|
34
|
+
bundle exec rspec spec/lib/coarse/version_spec.rb
|
|
35
|
+
```
|
|
19
36
|
|
|
20
37
|
## Coding style
|
|
21
38
|
|
|
22
39
|
We use RuboCop to enforce a certain coding style for Ruby. With every Pull request and push, RuboCop checks the coding
|
|
23
|
-
style and reports any errors.
|
|
40
|
+
style and reports any errors. You can do this locally by simple executing `rubocop` if you have RuboCop installed. For
|
|
41
|
+
the exact RuboCop version used, take a look at `tetra.gemspec`.
|
|
24
42
|
|
|
25
43
|
## New features
|
|
26
44
|
|
|
27
45
|
In case you want to add new features, please be aware to also add the necessary fine grained or coarse grained tests
|
|
28
46
|
for it.
|
|
29
47
|
|
|
30
|
-
##
|
|
48
|
+
## Maintainers
|
|
49
|
+
|
|
50
|
+
### Release
|
|
51
|
+
|
|
52
|
+
This is an example for the version number 2.0.9.
|
|
31
53
|
|
|
32
|
-
Install
|
|
33
|
-
Bump
|
|
54
|
+
1. Install release helper gem `gem install gem-release`
|
|
55
|
+
1. Bump the version in `lib/tetra/version.rb`
|
|
56
|
+
1. Regenerate `Gemfile.lock` with `bundle install`
|
|
57
|
+
1. Build the gem with `gem build tetra.gemspec`
|
|
58
|
+
1. Run the local tests with `rake`
|
|
59
|
+
1. Add both files and commit them
|
|
60
|
+
1. Tag new version: `git tag -am "tag v2.0.9" v2.0.9`
|
|
61
|
+
1. Push to master: `git push upstream` and `git push --tags upstream`
|
|
62
|
+
1. (Optional: Take a look at the GitHub Action tests)
|
|
63
|
+
1. Release the gem on [rubygems.org](https://rubygems.org): `gem release --pretend` and `gem release`
|
data/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
[](https://github.com/uyuni-project/tetra/actions/workflows/test_fine.yml)
|
|
2
|
+
[](https://github.com/uyuni-project/tetra/actions/workflows/test_coarse.yml)
|
|
3
|
+
[](https://github.com/uyuni-project/tetra/actions/workflows/rubocop.yml)
|
|
3
4
|
[](https://codecov.io/github/uyuni-project/tetra)
|
|
4
5
|
[](https://sonarcloud.io/summary/new_code?id=uyuni-project_tetra)
|
|
6
|
+
[](https://www.bestpractices.dev/projects/6678)
|
|
7
|
+
[](https://www.bestpractices.dev/projects/6678)
|
|
5
8
|
|
|
6
9
|
# tetra – builds RPMs for Java software
|
|
7
10
|
|
|
@@ -14,7 +17,7 @@ See [MOTIVATION.md](MOTIVATION.md) for further information.
|
|
|
14
17
|
|
|
15
18
|
## Prerequisites
|
|
16
19
|
|
|
17
|
-
- [Ruby 3.
|
|
20
|
+
- [Ruby 3.4](https://www.ruby-lang.org) or later;
|
|
18
21
|
- [git](https://git-scm.com) with your credentials set in `~/.gitconfig` (name and email);
|
|
19
22
|
- some basic Unix commands: `bash`, `unzip`, `tar`;
|
|
20
23
|
- a JDK > 8 that can compile whatever software you need to package;
|
data/bin/tetra
CHANGED
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
# Copyright 2015 Silvio Moioli <moio@suse.de>
|
|
7
7
|
|
|
8
8
|
# Add lib/ to load path if running from a git clone (dev mode)
|
|
9
|
-
if File.exist?(File.join(__dir__, "..", ".git"))
|
|
10
|
-
$LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
|
|
11
|
-
end
|
|
9
|
+
$LOAD_PATH.unshift(File.join(__dir__, "..", "lib")) if File.exist?(File.join(__dir__, "..", ".git"))
|
|
12
10
|
|
|
13
11
|
begin
|
|
14
12
|
require "tetra"
|
data/lib/tetra/constants.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# base module for tetra
|
|
4
4
|
module Tetra
|
|
5
|
-
CCOLLECTIONS = "commons-collections4-4.5.0-M2-src"
|
|
5
|
+
CCOLLECTIONS = "commons-collections4-4.5.0-M2-src"
|
|
6
6
|
CHUNK_SEPARATOR_VERSION_MATCHER = /[.\-_ ~,]/ # Constant regex for splitting version chunks
|
|
7
7
|
LICENSE_MAP_PATH = File.join(__dir__, "data", "license_map.yml").freeze
|
|
8
8
|
LICENSE_MAPPINGS = {
|
data/lib/tetra/facades/ant.rb
CHANGED
data/lib/tetra/facades/bash.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Tetra
|
|
3
4
|
# runs Bash with tetra-specific options
|
|
4
5
|
class Bash
|
|
@@ -44,7 +45,7 @@ module Tetra
|
|
|
44
45
|
# The 'bash -c' flag normally takes a single string, so we must be careful.
|
|
45
46
|
# Ideally, passing it as a single array element ["bash", ..., "-c", command]
|
|
46
47
|
# is handled safely by ProcessRunner if it uses exec/system/spawn.
|
|
47
|
-
run(["bash", "--rcfile", bashrc_file.path, "-i", "-c", command])
|
|
48
|
+
run(["bash", "--rcfile", bashrc_file.path, "-i", "-c", command], echo: false, stdin_data: nil)
|
|
48
49
|
[command]
|
|
49
50
|
else
|
|
50
51
|
# Interactive mode
|
data/lib/tetra/facades/git.rb
CHANGED
|
@@ -17,11 +17,9 @@ module Tetra
|
|
|
17
17
|
# inits a repo
|
|
18
18
|
def init
|
|
19
19
|
Dir.chdir(@directory) do
|
|
20
|
-
if Dir.exist?(".git")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
git_cmd("init")
|
|
24
|
-
end
|
|
20
|
+
raise GitAlreadyInitedError if Dir.exist?(".git")
|
|
21
|
+
|
|
22
|
+
git_cmd("init")
|
|
25
23
|
end
|
|
26
24
|
end
|
|
27
25
|
|
|
@@ -127,14 +125,12 @@ module Tetra
|
|
|
127
125
|
"-L", "previously generated",
|
|
128
126
|
"-L", "user edited")
|
|
129
127
|
rescue ExecutionFailed => e
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
raise e
|
|
134
|
-
end
|
|
128
|
+
raise e unless e.status.positive?
|
|
129
|
+
|
|
130
|
+
conflict_count = e.status
|
|
135
131
|
ensure
|
|
136
132
|
# Clean up the temporary file
|
|
137
|
-
|
|
133
|
+
FileUtils.rm_f(temp_old_version)
|
|
138
134
|
end
|
|
139
135
|
conflict_count
|
|
140
136
|
end
|
|
@@ -167,12 +163,12 @@ module Tetra
|
|
|
167
163
|
# This streams stdout from git directly to stdin of xz without loading
|
|
168
164
|
# data into Ruby memory (which could be big).
|
|
169
165
|
git_command = ["git", "archive", "--format=tar", id, "--", directory]
|
|
170
|
-
xz_command = ["xz", "-
|
|
166
|
+
xz_command = ["xz", "-6", "-T0"]
|
|
171
167
|
|
|
172
168
|
statuses = Open3.pipeline(git_command, xz_command, out: destination_path)
|
|
173
169
|
|
|
174
170
|
unless statuses.all?(&:success?)
|
|
175
|
-
|
|
171
|
+
raise ExecutionFailed.new(
|
|
176
172
|
"git archive pipeline failed (exit codes: #{statuses.map(&:exitstatus)})",
|
|
177
173
|
statuses.last.exitstatus
|
|
178
174
|
)
|
|
@@ -193,11 +189,11 @@ module Tetra
|
|
|
193
189
|
|
|
194
190
|
# Automatically passes "git" as the first argument in an Array.
|
|
195
191
|
def git_cmd(*args)
|
|
196
|
-
run(["git"] + args)
|
|
192
|
+
run(["git"] + args, echo: false, stdin_data: nil)
|
|
197
193
|
end
|
|
198
194
|
|
|
199
195
|
def git_cmd_with_stdin(stdin_data, *args)
|
|
200
|
-
run(["git"] + args, false, stdin_data)
|
|
196
|
+
run(["git"] + args, echo: false, stdin_data: stdin_data)
|
|
201
197
|
end
|
|
202
198
|
end
|
|
203
199
|
|
data/lib/tetra/facades/mvn.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Tetra
|
|
3
4
|
# encapsulates tetra-specific Maven commandline options
|
|
4
5
|
class Mvn
|
|
@@ -19,7 +20,7 @@ module Tetra
|
|
|
19
20
|
"--strict-checksums"
|
|
20
21
|
]
|
|
21
22
|
|
|
22
|
-
"#{executable} #{options.join(
|
|
23
|
+
"#{executable} #{options.join(" ")}"
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -8,64 +8,21 @@ module Tetra
|
|
|
8
8
|
# runs a noninteractive executable and returns its output as a string
|
|
9
9
|
# raises ExecutionFailed if the exit status is not 0
|
|
10
10
|
# optionally echoes the executable's output/error to standard output/error
|
|
11
|
-
def run(commandline, echo
|
|
11
|
+
def run(commandline, echo: false, stdin_data: nil)
|
|
12
12
|
log.debug "running `#{commandline}`"
|
|
13
13
|
|
|
14
|
-
# Prepare buffers to capture output
|
|
15
14
|
out_buffer = StringIO.new
|
|
16
15
|
err_buffer = StringIO.new
|
|
17
|
-
exit_status = nil
|
|
18
16
|
# Flatten handles nested arrays, Compact removes nils
|
|
19
17
|
cmd_args = Array(commandline).flatten.compact.map(&:to_s)
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
# 1. Handle Input (if any)
|
|
23
|
-
stdin.write(stdin_data) if stdin_data
|
|
24
|
-
stdin.close # Close stdin so the process knows input is finished
|
|
25
|
-
|
|
26
|
-
# Use threads to read stdout and stderr simultaneously.
|
|
27
|
-
readers = []
|
|
28
|
-
|
|
29
|
-
readers << Thread.new do
|
|
30
|
-
stdout.each_line do |line|
|
|
31
|
-
print line if echo # Echo to real STDOUT if requested
|
|
32
|
-
out_buffer << line # Capture to memory
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
readers << Thread.new do
|
|
37
|
-
stderr.each_line do |line|
|
|
38
|
-
$stderr.print line if echo # Echo to real STDERR if requested
|
|
39
|
-
err_buffer << line # Capture to memory
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Wait for reading to finish
|
|
44
|
-
readers.each(&:join)
|
|
45
|
-
|
|
46
|
-
# Get the exit code
|
|
47
|
-
exit_status = wait_thr.value
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Check for failure
|
|
51
|
-
unless exit_status.success?
|
|
52
|
-
# Extract strings from buffers
|
|
53
|
-
out = out_buffer.string
|
|
54
|
-
err = err_buffer.string
|
|
55
|
-
|
|
56
|
-
log.warn("`#{commandline}` failed with status #{exit_status.exitstatus}")
|
|
57
|
-
|
|
58
|
-
if !out.empty? || !err.empty?
|
|
59
|
-
log.warn("Output follows:")
|
|
60
|
-
log.warn(out) unless out.empty?
|
|
61
|
-
log.warn(err) unless err.empty?
|
|
62
|
-
end
|
|
19
|
+
exit_status = spawn_and_capture(cmd_args, stdin_data, echo, out_buffer, err_buffer)
|
|
63
20
|
|
|
64
|
-
|
|
65
|
-
|
|
21
|
+
out = out_buffer.string
|
|
22
|
+
err = err_buffer.string
|
|
23
|
+
raise_execution_failed(commandline, exit_status, out, err) unless exit_status.success?
|
|
66
24
|
|
|
67
|
-
|
|
68
|
-
out_buffer.string
|
|
25
|
+
out
|
|
69
26
|
end
|
|
70
27
|
|
|
71
28
|
# runs an interactive executable in a subshell
|
|
@@ -78,16 +35,55 @@ module Tetra
|
|
|
78
35
|
|
|
79
36
|
log.debug "`#{command}` exited with success #{success}"
|
|
80
37
|
|
|
81
|
-
|
|
38
|
+
raise ExecutionFailed.new(command, $CHILD_STATUS.exitstatus, nil, nil) unless success
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# spawns commandline, writing stdin and reading stdout/stderr concurrently
|
|
44
|
+
# (writing stdin to completion before draining stdout/stderr can deadlock
|
|
45
|
+
# if the child fills its output pipe buffer before consuming all of stdin)
|
|
46
|
+
# returns the resulting Process::Status
|
|
47
|
+
def spawn_and_capture(cmd_args, stdin_data, echo, out_buffer, err_buffer)
|
|
48
|
+
Open3.popen3(*cmd_args) do |stdin, stdout, stderr, wait_thr|
|
|
49
|
+
threads = [
|
|
50
|
+
Thread.new { write_stdin(stdin, stdin_data) },
|
|
51
|
+
Thread.new { pipe_to_buffer(stdout, out_buffer, echo ? $stdout : nil) },
|
|
52
|
+
Thread.new { pipe_to_buffer(stderr, err_buffer, echo ? $stderr : nil) }
|
|
53
|
+
]
|
|
54
|
+
threads.each(&:join)
|
|
55
|
+
|
|
56
|
+
wait_thr.value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def write_stdin(stdin, stdin_data)
|
|
61
|
+
stdin.write(stdin_data) if stdin_data
|
|
62
|
+
stdin.close # Close stdin so the process knows input is finished
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pipe_to_buffer(io, buffer, echo_io)
|
|
66
|
+
io.each_line do |line|
|
|
67
|
+
echo_io&.print(line)
|
|
68
|
+
buffer << line
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def raise_execution_failed(commandline, exit_status, out, err)
|
|
73
|
+
log.warn("`#{commandline}` failed with status #{exit_status.exitstatus}")
|
|
74
|
+
if !out.empty? || !err.empty?
|
|
75
|
+
log.warn("Output follows:")
|
|
76
|
+
log.warn(out) unless out.empty?
|
|
77
|
+
log.warn(err) unless err.empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
raise ExecutionFailed.new(commandline, exit_status.exitstatus, out, err)
|
|
82
81
|
end
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
# raised when a command returns a non-zero status
|
|
86
85
|
class ExecutionFailed < StandardError
|
|
87
|
-
attr_reader :commandline
|
|
88
|
-
attr_reader :status
|
|
89
|
-
attr_reader :out
|
|
90
|
-
attr_reader :err
|
|
86
|
+
attr_reader :commandline, :status, :out, :err
|
|
91
87
|
|
|
92
88
|
def initialize(commandline, status, out, err)
|
|
93
89
|
@commandline = commandline
|
data/lib/tetra/facades/tar.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Tetra
|
|
3
4
|
# encapsulates tar
|
|
4
5
|
class Tar
|
|
@@ -7,7 +8,7 @@ module Tetra
|
|
|
7
8
|
# decompresses a file in a target directory
|
|
8
9
|
def decompress(tarfile, directory)
|
|
9
10
|
# Use Array format to prevent shell injection.
|
|
10
|
-
result = run(["tar", "xvf", tarfile, "--directory", directory])
|
|
11
|
+
result = run(["tar", "xvf", tarfile, "--directory", directory], echo: false, stdin_data: nil)
|
|
11
12
|
|
|
12
13
|
result&.strip
|
|
13
14
|
end
|
data/lib/tetra/facades/unzip.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Tetra
|
|
3
4
|
# encapsulates unzip
|
|
4
5
|
class Unzip
|
|
@@ -7,7 +8,7 @@ module Tetra
|
|
|
7
8
|
# decompresses a file in a target directory
|
|
8
9
|
def decompress(zipfile, directory)
|
|
9
10
|
# Use Array execution to prevent shell injection
|
|
10
|
-
result = run(["unzip", zipfile, "-d", directory])
|
|
11
|
+
result = run(["unzip", zipfile, "-d", directory], echo: false, stdin_data: nil)
|
|
11
12
|
|
|
12
13
|
result&.strip
|
|
13
14
|
end
|
data/lib/tetra/maven_website.rb
CHANGED
|
@@ -10,8 +10,8 @@ module Tetra
|
|
|
10
10
|
include Logging
|
|
11
11
|
|
|
12
12
|
# API Constants
|
|
13
|
-
SEARCH_API = "https://search.maven.org/solrsearch/select"
|
|
14
|
-
DOWNLOAD_API = "https://repo1.maven.org/maven2"
|
|
13
|
+
SEARCH_API = "https://search.maven.org/solrsearch/select"
|
|
14
|
+
DOWNLOAD_API = "https://repo1.maven.org/maven2"
|
|
15
15
|
|
|
16
16
|
def search_by_sha1(sha1)
|
|
17
17
|
search(q: "1:\"#{sha1}\"")
|
|
@@ -77,14 +77,16 @@ module Tetra
|
|
|
77
77
|
when Net::HTTPSuccess
|
|
78
78
|
response.body
|
|
79
79
|
when Net::HTTPRedirection
|
|
80
|
-
|
|
80
|
+
# Location may be relative (RFC 7231); resolve it against the
|
|
81
|
+
# current URI so relative redirects don't crash Net::HTTP.start.
|
|
82
|
+
location = (uri + response["location"]).to_s
|
|
81
83
|
log.info("Redirected to #{location}")
|
|
82
84
|
# Recursive call to follow redirect (params are usually part of the new URL)
|
|
83
85
|
fetch(location, {}, limit - 1)
|
|
84
86
|
when Net::HTTPNotFound
|
|
85
|
-
|
|
87
|
+
raise NotFoundOnMavenWebsiteError
|
|
86
88
|
else
|
|
87
|
-
|
|
89
|
+
raise "Remote error: #{response.code} #{response.message}"
|
|
88
90
|
end
|
|
89
91
|
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNRESET => e
|
|
90
92
|
if (retries += 1) <= 3
|
|
@@ -13,7 +13,7 @@ module Tetra
|
|
|
13
13
|
"cd #{project.latest_dry_run_directory}"
|
|
14
14
|
] + generate_aliases(project) + project.build_script_lines
|
|
15
15
|
|
|
16
|
-
new_content = script_lines.join("\n")
|
|
16
|
+
new_content = "#{script_lines.join("\n")}\n"
|
|
17
17
|
|
|
18
18
|
result_dir = File.join(project.packages_dir, project.name)
|
|
19
19
|
FileUtils.mkdir_p(result_dir)
|
|
@@ -44,8 +44,5 @@ module Tetra
|
|
|
44
44
|
|
|
45
45
|
cmds
|
|
46
46
|
end
|
|
47
|
-
|
|
48
|
-
# Alias for backward compatibility if other classes call 'aliases'
|
|
49
|
-
alias aliases generate_aliases
|
|
50
47
|
end
|
|
51
48
|
end
|
data/lib/tetra/pom_getter.rb
CHANGED
|
@@ -23,7 +23,9 @@ module Tetra
|
|
|
23
23
|
log.debug("Attempting unpack of #{file} to find a POM")
|
|
24
24
|
begin
|
|
25
25
|
Zip::File.foreach(file) do |entry|
|
|
26
|
-
#
|
|
26
|
+
# Defense in depth against malicious entry names. Note this method
|
|
27
|
+
# never extracts entries to disk (only reads pom.xml into memory),
|
|
28
|
+
# so Zip Slip path traversal is not actually reachable here.
|
|
27
29
|
next if entry.name.include?("..") || entry.name.start_with?("/")
|
|
28
30
|
|
|
29
31
|
# PERFORMANCE: end_with? is much faster than Regexp for simple suffixes
|
|
@@ -55,7 +57,7 @@ module Tetra
|
|
|
55
57
|
|
|
56
58
|
if found_doc
|
|
57
59
|
log.info("pom.xml for #{file} found on search.maven.org for sha1 #{sha1} " \
|
|
58
|
-
"(#{found_doc[
|
|
60
|
+
"(#{found_doc["g"]}:#{found_doc["a"]}:#{found_doc["v"]})")
|
|
59
61
|
|
|
60
62
|
group_id, artifact_id, version = site.get_maven_id_from(found_doc)
|
|
61
63
|
return site.download_pom(group_id, artifact_id, version), :found_via_sha1
|
data/lib/tetra/project.rb
CHANGED
|
@@ -31,11 +31,9 @@ module Tetra
|
|
|
31
31
|
# finds the project directory up in the tree, like git does
|
|
32
32
|
def self.find_project_dir(starting_dir)
|
|
33
33
|
result = starting_dir
|
|
34
|
-
while project?(result) == false && result != "/"
|
|
35
|
-
result = File.expand_path("..", result)
|
|
36
|
-
end
|
|
34
|
+
result = File.expand_path("..", result) while project?(result) == false && result != "/"
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
raise NoProjectDirectoryError, starting_dir if result == "/"
|
|
39
37
|
|
|
40
38
|
result
|
|
41
39
|
end
|
|
@@ -61,7 +59,7 @@ module Tetra
|
|
|
61
59
|
def dry_running?
|
|
62
60
|
latest_comment = @git.latest_comment("tetra: dry-run-")
|
|
63
61
|
# MODERNIZATION: Use match? instead of =~
|
|
64
|
-
!latest_comment.nil? && !latest_comment.
|
|
62
|
+
!latest_comment.nil? && !latest_comment.include?("tetra: dry-run-finished")
|
|
65
63
|
end
|
|
66
64
|
|
|
67
65
|
# ends a dry-run assuming a successful build:
|
|
@@ -127,7 +125,7 @@ module Tetra
|
|
|
127
125
|
|
|
128
126
|
previous_id = @git.latest_id(generated_comment)
|
|
129
127
|
|
|
130
|
-
File.
|
|
128
|
+
File.write(path, new_content)
|
|
131
129
|
log.debug "committing new content: #{comment}"
|
|
132
130
|
@git.commit_file(path, whole_comment)
|
|
133
131
|
|
|
@@ -136,7 +134,7 @@ module Tetra
|
|
|
136
134
|
conflict_count = @git.merge_with_id(path, "#{path}.tetra_user_edited", previous_id)
|
|
137
135
|
File.delete("#{path}.tetra_user_edited")
|
|
138
136
|
|
|
139
|
-
@git.commit_file(path, "User changes merged back") if conflict_count
|
|
137
|
+
@git.commit_file(path, "User changes merged back") if conflict_count.zero?
|
|
140
138
|
|
|
141
139
|
return conflict_count
|
|
142
140
|
end
|
|
@@ -145,10 +143,8 @@ module Tetra
|
|
|
145
143
|
end
|
|
146
144
|
|
|
147
145
|
# runs a block from the project directory or a subdirectory
|
|
148
|
-
def from_directory(subdirectory = "")
|
|
149
|
-
Dir.chdir(File.join(@full_path, subdirectory))
|
|
150
|
-
yield
|
|
151
|
-
end
|
|
146
|
+
def from_directory(subdirectory = "", &)
|
|
147
|
+
Dir.chdir(File.join(@full_path, subdirectory), &)
|
|
152
148
|
end
|
|
153
149
|
|
|
154
150
|
# returns the latest dry run start directory
|
|
@@ -159,18 +155,16 @@ module Tetra
|
|
|
159
155
|
# returns a list of files produced during the last dry-run
|
|
160
156
|
def produced_files
|
|
161
157
|
@git.latest_comment("tetra: dry-run-finished")
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.sort
|
|
158
|
+
.split("\n")
|
|
159
|
+
.filter_map { |line| line[%r{^tetra: file-changed: src/(.+)$}, 1] }
|
|
160
|
+
.sort
|
|
166
161
|
end
|
|
167
162
|
|
|
168
163
|
def build_script_lines
|
|
169
164
|
@git.latest_comment("tetra: dry-run-finished")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.sort
|
|
165
|
+
.split("\n")
|
|
166
|
+
.filter_map { |line| line[/^tetra: build-script-line: (.+)$/, 1] }
|
|
167
|
+
.sort
|
|
174
168
|
end
|
|
175
169
|
|
|
176
170
|
# archives a tarball of kit/ in packages/
|
|
@@ -216,8 +210,8 @@ module Tetra
|
|
|
216
210
|
|
|
217
211
|
# Calculate relative path for symlink
|
|
218
212
|
link_target = Pathname.new(new_location)
|
|
219
|
-
|
|
220
|
-
|
|
213
|
+
.relative_path_from(Pathname.new(file).dirname)
|
|
214
|
+
.to_s
|
|
221
215
|
|
|
222
216
|
File.symlink(link_target, file)
|
|
223
217
|
result << [file, new_location]
|
|
@@ -234,6 +228,7 @@ module Tetra
|
|
|
234
228
|
|
|
235
229
|
def initialize(directory)
|
|
236
230
|
@directory = directory
|
|
231
|
+
super("Not a tetra project directory: #{directory}")
|
|
237
232
|
end
|
|
238
233
|
end
|
|
239
234
|
end
|
data/lib/tetra/project_initer.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Tetra
|
|
|
5
5
|
class ChangeSourcesSubcommand < Tetra::Subcommand
|
|
6
6
|
parameter "[SOURCE_ARCHIVE]", "new source tarball or zipfile"
|
|
7
7
|
parameter "[MESSAGE]", "a short change description", default: "Sources changed"
|
|
8
|
-
option %w
|
|
8
|
+
option %w[-n --no-archive], :flag, "use current src/ contents instead of an archive (see SPECIAL_CASES.md)",
|
|
9
9
|
default: false
|
|
10
10
|
|
|
11
11
|
def execute
|
|
@@ -17,19 +17,19 @@ module Tetra
|
|
|
17
17
|
|
|
18
18
|
project = Tetra::Project.new(".")
|
|
19
19
|
ensure_dry_running(:is_not_in_progress, project) do
|
|
20
|
-
if
|
|
20
|
+
if no_archive?
|
|
21
|
+
# CASE 2: Updating from manual changes in src/
|
|
22
|
+
project.commit_sources(message, true)
|
|
23
|
+
|
|
24
|
+
puts "New sources committed."
|
|
25
|
+
puts "Please copy source archive(s) corresponding to src/ in packages/ before proceeding."
|
|
26
|
+
else
|
|
21
27
|
# CASE 1: Updating from a new archive file
|
|
22
28
|
full_archive_path = File.expand_path(source_archive)
|
|
23
29
|
project.commit_source_archive(full_archive_path, message)
|
|
24
30
|
|
|
25
31
|
puts "New sources committed."
|
|
26
32
|
puts "Please delete any stale source archives from packages/ before proceeding."
|
|
27
|
-
else
|
|
28
|
-
# CASE 2: Updating from manual changes in src/
|
|
29
|
-
project.commit_sources(message, true)
|
|
30
|
-
|
|
31
|
-
puts "New sources committed."
|
|
32
|
-
puts "Please copy source archive(s) corresponding to src/ in packages/ before proceeding."
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Tetra
|
|
4
4
|
# tetra generate-all
|
|
5
5
|
class GenerateAllSubcommand < Tetra::Subcommand
|
|
6
|
-
option %w
|
|
6
|
+
option %w[-f --filter], "FILTER", "filter files to be installed by this package spec", default: "*.jar"
|
|
7
7
|
parameter "[POM]", "a package pom file path", default: "pom.xml"
|
|
8
8
|
|
|
9
9
|
def execute
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Tetra
|
|
4
4
|
# tetra generate-spec
|
|
5
5
|
class GenerateSpecSubcommand < Tetra::Subcommand
|
|
6
|
-
option %w
|
|
6
|
+
option %w[-f --filter], "FILTER", "filter files to be installed by this spec", default: "*.jar"
|
|
7
7
|
parameter "[POM]", "a pom file path", default: "pom.xml"
|
|
8
8
|
|
|
9
9
|
def execute
|
|
@@ -15,14 +15,11 @@ module Tetra
|
|
|
15
15
|
path, status = pom_getter.get_pom(name)
|
|
16
16
|
|
|
17
17
|
if path
|
|
18
|
-
text_status =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
when :found_via_heuristic
|
|
24
|
-
"found by heuristic search from search.maven.org"
|
|
25
|
-
end
|
|
18
|
+
text_status = {
|
|
19
|
+
found_in_jar: "was inside the jar",
|
|
20
|
+
found_via_sha1: "found by sha1 search from search.maven.org",
|
|
21
|
+
found_via_heuristic: "found by heuristic search from search.maven.org"
|
|
22
|
+
}[status]
|
|
26
23
|
|
|
27
24
|
puts "#{format_path(path, project)} written, #{text_status}"
|
|
28
25
|
else
|
|
@@ -5,7 +5,7 @@ module Tetra
|
|
|
5
5
|
class InitSubcommand < Tetra::Subcommand
|
|
6
6
|
parameter "PACKAGE_NAME", "name of the package to create"
|
|
7
7
|
parameter "[SOURCE_ARCHIVE]", "source tarball or zipfile"
|
|
8
|
-
option %w
|
|
8
|
+
option %w[-n --no-archive], :flag, "create a project without a source archive (see SPECIAL_CASES.md)",
|
|
9
9
|
default: false
|
|
10
10
|
|
|
11
11
|
def execute
|
|
@@ -16,9 +16,7 @@ module Tetra
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Ensure the file exists if provided
|
|
19
|
-
if source_archive && !File.readable?(source_archive)
|
|
20
|
-
signal_usage_error "#{source_archive} is not a file or it is not readable."
|
|
21
|
-
end
|
|
19
|
+
signal_usage_error "#{source_archive} is not a file or it is not readable." if source_archive && !File.readable?(source_archive)
|
|
22
20
|
|
|
23
21
|
Tetra::Project.init(package_name)
|
|
24
22
|
project = Tetra::Project.new(package_name)
|
data/lib/tetra/ui/subcommand.rb
CHANGED
|
@@ -25,15 +25,15 @@ module Tetra
|
|
|
25
25
|
|
|
26
26
|
# maps verbosity options to log level
|
|
27
27
|
def configure_log_level(verbose, very_verbose, very_very_verbose)
|
|
28
|
-
if very_very_verbose
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
log.level = if very_very_verbose
|
|
29
|
+
::Logger::DEBUG
|
|
30
|
+
elsif very_verbose
|
|
31
|
+
::Logger::INFO
|
|
32
|
+
elsif verbose
|
|
33
|
+
::Logger::WARN
|
|
34
|
+
else
|
|
35
|
+
::Logger::ERROR
|
|
36
|
+
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# override default option parsing to pass options to other commands
|
|
@@ -89,15 +89,15 @@ module Tetra
|
|
|
89
89
|
def checking_exceptions
|
|
90
90
|
yield
|
|
91
91
|
rescue Errno::EACCES, Errno::ENOENT, Errno::EEXIST => e
|
|
92
|
-
|
|
92
|
+
warn e
|
|
93
93
|
rescue Tetra::NoProjectDirectoryError => e
|
|
94
|
-
|
|
94
|
+
warn "#{e.directory} is not a tetra project directory, see \"tetra init\""
|
|
95
95
|
rescue Tetra::GitAlreadyInitedError
|
|
96
|
-
|
|
96
|
+
warn "This directory is already a tetra project"
|
|
97
97
|
rescue Tetra::ExecutionFailed => e
|
|
98
|
-
|
|
98
|
+
warn "Failed to run `#{e.commandline}` (exit status #{e.status})"
|
|
99
99
|
rescue Interrupt
|
|
100
|
-
|
|
100
|
+
warn "Execution interrupted by the user"
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
private
|
data/lib/tetra/version.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Tetra
|
|
|
28
28
|
def best_match(my_version, their_versions)
|
|
29
29
|
return nil if their_versions.nil? || their_versions.empty?
|
|
30
30
|
|
|
31
|
-
log.debug("version comparison: #{my_version} vs #{their_versions.join(
|
|
31
|
+
log.debug("version comparison: #{my_version} vs #{their_versions.join(", ")}")
|
|
32
32
|
|
|
33
33
|
my_chunks = my_version.split(Tetra::CHUNK_SEPARATOR_VERSION_MATCHER)
|
|
34
34
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tetra
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dominik Gedon
|
|
@@ -70,14 +70,14 @@ dependencies:
|
|
|
70
70
|
requirements:
|
|
71
71
|
- - "~>"
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '3.
|
|
73
|
+
version: '3.4'
|
|
74
74
|
type: :runtime
|
|
75
75
|
prerelease: false
|
|
76
76
|
version_requirements: !ruby/object:Gem::Requirement
|
|
77
77
|
requirements:
|
|
78
78
|
- - "~>"
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '3.
|
|
80
|
+
version: '3.4'
|
|
81
81
|
- !ruby/object:Gem::Dependency
|
|
82
82
|
name: text
|
|
83
83
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -92,132 +92,6 @@ dependencies:
|
|
|
92
92
|
- - "~>"
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
94
|
version: '1.3'
|
|
95
|
-
- !ruby/object:Gem::Dependency
|
|
96
|
-
name: aruba
|
|
97
|
-
requirement: !ruby/object:Gem::Requirement
|
|
98
|
-
requirements:
|
|
99
|
-
- - "~>"
|
|
100
|
-
- !ruby/object:Gem::Version
|
|
101
|
-
version: '2.4'
|
|
102
|
-
type: :development
|
|
103
|
-
prerelease: false
|
|
104
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
105
|
-
requirements:
|
|
106
|
-
- - "~>"
|
|
107
|
-
- !ruby/object:Gem::Version
|
|
108
|
-
version: '2.4'
|
|
109
|
-
- !ruby/object:Gem::Dependency
|
|
110
|
-
name: rake
|
|
111
|
-
requirement: !ruby/object:Gem::Requirement
|
|
112
|
-
requirements:
|
|
113
|
-
- - "~>"
|
|
114
|
-
- !ruby/object:Gem::Version
|
|
115
|
-
version: '13.4'
|
|
116
|
-
type: :development
|
|
117
|
-
prerelease: false
|
|
118
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
119
|
-
requirements:
|
|
120
|
-
- - "~>"
|
|
121
|
-
- !ruby/object:Gem::Version
|
|
122
|
-
version: '13.4'
|
|
123
|
-
- !ruby/object:Gem::Dependency
|
|
124
|
-
name: rspec
|
|
125
|
-
requirement: !ruby/object:Gem::Requirement
|
|
126
|
-
requirements:
|
|
127
|
-
- - "~>"
|
|
128
|
-
- !ruby/object:Gem::Version
|
|
129
|
-
version: '3.13'
|
|
130
|
-
type: :development
|
|
131
|
-
prerelease: false
|
|
132
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
133
|
-
requirements:
|
|
134
|
-
- - "~>"
|
|
135
|
-
- !ruby/object:Gem::Version
|
|
136
|
-
version: '3.13'
|
|
137
|
-
- !ruby/object:Gem::Dependency
|
|
138
|
-
name: rubocop
|
|
139
|
-
requirement: !ruby/object:Gem::Requirement
|
|
140
|
-
requirements:
|
|
141
|
-
- - "~>"
|
|
142
|
-
- !ruby/object:Gem::Version
|
|
143
|
-
version: '1.86'
|
|
144
|
-
type: :development
|
|
145
|
-
prerelease: false
|
|
146
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
147
|
-
requirements:
|
|
148
|
-
- - "~>"
|
|
149
|
-
- !ruby/object:Gem::Version
|
|
150
|
-
version: '1.86'
|
|
151
|
-
- !ruby/object:Gem::Dependency
|
|
152
|
-
name: rubocop-performance
|
|
153
|
-
requirement: !ruby/object:Gem::Requirement
|
|
154
|
-
requirements:
|
|
155
|
-
- - "~>"
|
|
156
|
-
- !ruby/object:Gem::Version
|
|
157
|
-
version: '1.26'
|
|
158
|
-
type: :development
|
|
159
|
-
prerelease: false
|
|
160
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
161
|
-
requirements:
|
|
162
|
-
- - "~>"
|
|
163
|
-
- !ruby/object:Gem::Version
|
|
164
|
-
version: '1.26'
|
|
165
|
-
- !ruby/object:Gem::Dependency
|
|
166
|
-
name: rubocop-rake
|
|
167
|
-
requirement: !ruby/object:Gem::Requirement
|
|
168
|
-
requirements:
|
|
169
|
-
- - "~>"
|
|
170
|
-
- !ruby/object:Gem::Version
|
|
171
|
-
version: '0.7'
|
|
172
|
-
type: :development
|
|
173
|
-
prerelease: false
|
|
174
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
175
|
-
requirements:
|
|
176
|
-
- - "~>"
|
|
177
|
-
- !ruby/object:Gem::Version
|
|
178
|
-
version: '0.7'
|
|
179
|
-
- !ruby/object:Gem::Dependency
|
|
180
|
-
name: rubocop-rspec
|
|
181
|
-
requirement: !ruby/object:Gem::Requirement
|
|
182
|
-
requirements:
|
|
183
|
-
- - "~>"
|
|
184
|
-
- !ruby/object:Gem::Version
|
|
185
|
-
version: 3.9.0
|
|
186
|
-
type: :development
|
|
187
|
-
prerelease: false
|
|
188
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
189
|
-
requirements:
|
|
190
|
-
- - "~>"
|
|
191
|
-
- !ruby/object:Gem::Version
|
|
192
|
-
version: 3.9.0
|
|
193
|
-
- !ruby/object:Gem::Dependency
|
|
194
|
-
name: simplecov
|
|
195
|
-
requirement: !ruby/object:Gem::Requirement
|
|
196
|
-
requirements:
|
|
197
|
-
- - "~>"
|
|
198
|
-
- !ruby/object:Gem::Version
|
|
199
|
-
version: '0.22'
|
|
200
|
-
type: :development
|
|
201
|
-
prerelease: false
|
|
202
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
203
|
-
requirements:
|
|
204
|
-
- - "~>"
|
|
205
|
-
- !ruby/object:Gem::Version
|
|
206
|
-
version: '0.22'
|
|
207
|
-
- !ruby/object:Gem::Dependency
|
|
208
|
-
name: simplecov-cobertura
|
|
209
|
-
requirement: !ruby/object:Gem::Requirement
|
|
210
|
-
requirements:
|
|
211
|
-
- - "~>"
|
|
212
|
-
- !ruby/object:Gem::Version
|
|
213
|
-
version: '3.1'
|
|
214
|
-
type: :development
|
|
215
|
-
prerelease: false
|
|
216
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
217
|
-
requirements:
|
|
218
|
-
- - "~>"
|
|
219
|
-
- !ruby/object:Gem::Version
|
|
220
|
-
version: '3.1'
|
|
221
95
|
description: |
|
|
222
96
|
Tetra simplifies the creation of spec files and archives
|
|
223
97
|
to distribute Java projects in RPM format.
|
|
@@ -2045,7 +1919,7 @@ licenses:
|
|
|
2045
1919
|
- MIT
|
|
2046
1920
|
metadata:
|
|
2047
1921
|
bug_tracker_uri: https://github.com/uyuni-project/tetra/issues
|
|
2048
|
-
homepage_uri: https://
|
|
1922
|
+
homepage_uri: https://rubygems.org/gems/tetra
|
|
2049
1923
|
source_code_uri: https://github.com/uyuni-project/tetra
|
|
2050
1924
|
rubygems_mfa_required: 'true'
|
|
2051
1925
|
allowed_push_host: https://rubygems.org
|
|
@@ -2057,14 +1931,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
2057
1931
|
requirements:
|
|
2058
1932
|
- - ">="
|
|
2059
1933
|
- !ruby/object:Gem::Version
|
|
2060
|
-
version: '3.
|
|
1934
|
+
version: '3.4'
|
|
2061
1935
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
2062
1936
|
requirements:
|
|
2063
1937
|
- - ">="
|
|
2064
1938
|
- !ruby/object:Gem::Version
|
|
2065
1939
|
version: '0'
|
|
2066
1940
|
requirements: []
|
|
2067
|
-
rubygems_version: 4.0.
|
|
1941
|
+
rubygems_version: 4.0.17
|
|
2068
1942
|
specification_version: 4
|
|
2069
1943
|
summary: A tool to package Java projects
|
|
2070
1944
|
test_files: []
|