rexer 0.1.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/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/bin/console +10 -0
- data/bin/dev +5 -0
- data/exe/rex +11 -0
- data/lib/rexer/cli.rb +50 -0
- data/lib/rexer/commands/install.rb +86 -0
- data/lib/rexer/commands/state.rb +55 -0
- data/lib/rexer/commands/switch.rb +33 -0
- data/lib/rexer/commands/uninstall.rb +46 -0
- data/lib/rexer/commands/update.rb +38 -0
- data/lib/rexer/definition/data.rb +27 -0
- data/lib/rexer/definition/diff.rb +56 -0
- data/lib/rexer/definition/dsl.rb +62 -0
- data/lib/rexer/definition/lock.rb +41 -0
- data/lib/rexer/definition.rb +32 -0
- data/lib/rexer/extension/plugin.rb +104 -0
- data/lib/rexer/extension/theme.rb +80 -0
- data/lib/rexer/source/base.rb +21 -0
- data/lib/rexer/source/git.rb +35 -0
- data/lib/rexer/source/github.rb +9 -0
- data/lib/rexer/version.rb +3 -0
- data/lib/rexer.rb +16 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 564e86a618538937d2e68d85f1b625a8fa3ebddc8e4891fb12a4598eb2f944a5
|
4
|
+
data.tar.gz: 4e21379d1dae8ee68c301ee0f4be43a1034b585af097a9713ae3b238a8795ffe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f542f39676156e026985ab85853133e0efe09681cf8671bf8f2370fa9f0c2301a5d0f7422cdc042219b7475f2e15b28ec35807ae12285c132dba8e4729fc9203
|
7
|
+
data.tar.gz: 4632bde598651690ce5b685d7d80b657ef6567fe074ce392575cf87f8b705a4ce6f3b75495a74b311d2a1b6949fca65b0a660c1b9c653b31a7086b7b4d3decf3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Katsuya Hidaka
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# Rexer: Redmine Extension manager
|
2
|
+
|
3
|
+
Rexer is a tool for managing Redmine Extension, which means Redmine [Plugin](https://www.redmine.org/projects/redmine/wiki/Plugins) and [Theme](https://www.redmine.org/projects/redmine/wiki/Themes) in this tool.
|
4
|
+
|
5
|
+
It is mainly aimed at helping with the development of Redmine and its plugins, allowing you to define extensions in a Ruby DSL and install, uninstall, update, and switch between different sets of the extensions.
|
6
|
+
|
7
|
+
[](https://github.com/hidakatsuya/rexer/actions/workflows/build.yml)
|
8
|
+
[](https://badge.fury.io/rb/rexer)
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Install the gem and add to the application's Gemfile by executing:
|
13
|
+
|
14
|
+
bundle add rexer
|
15
|
+
|
16
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
17
|
+
|
18
|
+
gem install rexer
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
### Quick Start
|
23
|
+
|
24
|
+
First, create a `.extensions.rb` file in the root directory of the Redmine application.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
theme :bleuclair, github: { repo: "farend/redmine_theme_farend_bleuclair", branch: "support-propshaft" }
|
28
|
+
|
29
|
+
plugin :view_customize, github: { repo: "onozaty/redmine-view-customize", tag: "v3.5.2" }
|
30
|
+
plugin :redmine_issues_panel, git: { url: "https://github.com/redmica/redmine_issues_panel", tag: "v1.0.2" }
|
31
|
+
```
|
32
|
+
|
33
|
+
Then, run the following command in the root directory of the Redmine application.
|
34
|
+
|
35
|
+
```
|
36
|
+
rex install
|
37
|
+
```
|
38
|
+
|
39
|
+
This command installs plugins and themes defined in the `.extensions.rb` file and generates the `.extensions.lock` file.
|
40
|
+
|
41
|
+
> [!NOTE]
|
42
|
+
> The `.extensions.lock` file is a file that locks the state of the installed extensions, but it's NOT a file that locks the version of the extensions.
|
43
|
+
|
44
|
+
If you want to uninstall the extensions, run the following command.
|
45
|
+
|
46
|
+
```
|
47
|
+
rex uninstall
|
48
|
+
```
|
49
|
+
|
50
|
+
This command uninstalls the extensions and deletes the `.extensions.lock` file.
|
51
|
+
|
52
|
+
### Commands
|
53
|
+
|
54
|
+
```
|
55
|
+
$ rex
|
56
|
+
Commands:
|
57
|
+
rex help [COMMAND] # Describe available commands or one specific command
|
58
|
+
rex install [ENV] # Install extensions for the specified environment
|
59
|
+
rex state # Show the current state of the installed extensions
|
60
|
+
rex switch [ENV] # Uninstall extensions for the currently installed environment and install extensions for the specified environment
|
61
|
+
rex uninstall # Uninstall extensions for the currently installed environment
|
62
|
+
rex update # Update extensions for the currently installed environment to the latest version
|
63
|
+
rex version # Show Rexer version
|
64
|
+
```
|
65
|
+
|
66
|
+
### Defining environments and extensions for the environment
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
theme :bleuclair, github: { repo: "farend/redmine_theme_farend_bleuclair" }
|
70
|
+
plugin :redmine_issues_panel, git: { url: "https://github.com/redmica/redmine_issues_panel" }
|
71
|
+
|
72
|
+
env :stable do
|
73
|
+
theme :bleuclair, github: { repo: "farend/redmine_theme_farend_bleuclair", branch: "support-propshaft" }
|
74
|
+
plugin :redmine_issues_panel, git: { url: "https://github.com/redmica/redmine_issues_panel", tag: "v1.0.2" }
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
In above example, the `bleuclair` theme and the `redmine_issues_panel` plugin are defined for the `default` environment. The `bleuclair` theme and the `redmine_issues_panel` plugin are defined for the `stable` environment.
|
79
|
+
|
80
|
+
If you want to install extensions for the `default` environment, run the following command.
|
81
|
+
|
82
|
+
```
|
83
|
+
rex install
|
84
|
+
or
|
85
|
+
rex install default
|
86
|
+
```
|
87
|
+
|
88
|
+
Similarly, if you want to install extensions for the `stable` environment, run the following command.
|
89
|
+
|
90
|
+
```
|
91
|
+
rex install stable
|
92
|
+
```
|
93
|
+
|
94
|
+
In addition, you can switch between environments.
|
95
|
+
|
96
|
+
```
|
97
|
+
rex switch stable
|
98
|
+
or
|
99
|
+
rex install stable
|
100
|
+
```
|
101
|
+
|
102
|
+
The above command uninstalls the extensions for the currently installed environment and installs the extensions for the `stable` environment.
|
103
|
+
|
104
|
+
### Defining hooks
|
105
|
+
|
106
|
+
You can define hooks for each extension.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
plugin :redmica_s3, github: { repo: "redmica/redmica_s3" } do
|
110
|
+
installed do
|
111
|
+
Pathname.new("config", "s3.yml").write <<~YAML
|
112
|
+
access_key_id: ...
|
113
|
+
YAML
|
114
|
+
end
|
115
|
+
|
116
|
+
uninstalled do
|
117
|
+
Pathname.new("config", "s3.yml").delete
|
118
|
+
end
|
119
|
+
|
120
|
+
updated do
|
121
|
+
puts "updated"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
## Developing
|
127
|
+
|
128
|
+
### Running integration tests
|
129
|
+
|
130
|
+
First, you need to build the docker image for the integration tests.
|
131
|
+
|
132
|
+
```
|
133
|
+
rake rexer:test:build_integration_test_image
|
134
|
+
```
|
135
|
+
|
136
|
+
Then, you can run the integration tests.
|
137
|
+
|
138
|
+
```
|
139
|
+
rake test
|
140
|
+
```
|
141
|
+
|
142
|
+
### Formatting and Linting code
|
143
|
+
|
144
|
+
This project uses [Standard](https://github.com/standardrb/standard) for code formatting and linting. You can format and check the code by running the following commands.
|
145
|
+
|
146
|
+
```
|
147
|
+
rake standard
|
148
|
+
rake standard:fix
|
149
|
+
```
|
150
|
+
|
151
|
+
## Contributing
|
152
|
+
|
153
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hidakatsuya/rexer.
|
154
|
+
|
155
|
+
## License
|
156
|
+
|
157
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rexer"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
require "irb"
|
10
|
+
IRB.start(__FILE__)
|
data/bin/dev
ADDED
data/exe/rex
ADDED
data/lib/rexer/cli.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module Rexer
|
4
|
+
class Cli < Thor
|
5
|
+
def self.exit_on_failure? = true
|
6
|
+
|
7
|
+
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed output"
|
8
|
+
|
9
|
+
desc "install [ENV]", "Install the definitions in .extensions.rb for the specified environment"
|
10
|
+
def install(env = "default")
|
11
|
+
Commands::Install.new.call(env&.to_sym)
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "uninstall", "Uninstall extensions for the currently installed environment based on the state in .extensions.lock and remove the lock file"
|
15
|
+
def uninstall
|
16
|
+
Commands::Uninstall.new.call
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "switch [ENV]", "Uninstall extensions for the currently installed environment and install extensions for the specified environment"
|
20
|
+
def switch(env = "default")
|
21
|
+
Commands::Switch.new.call(env&.to_sym)
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "update", "Update extensions for the currently installed environment to the latest version"
|
25
|
+
def update
|
26
|
+
Commands::Update.new.call
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "state", "Show the current state of the installed extensions"
|
30
|
+
def state
|
31
|
+
Commands::State.new.call
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "version", "Show Rexer version"
|
35
|
+
def version
|
36
|
+
puts Rexer::VERSION
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(*)
|
40
|
+
super
|
41
|
+
initialize_options
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def initialize_options
|
47
|
+
ENV["VERBOSE"] = "1" if options[:verbose]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Commands
|
3
|
+
class Install
|
4
|
+
def call(env)
|
5
|
+
definition = load_definition(env)
|
6
|
+
lock_definition = load_lock_definition
|
7
|
+
|
8
|
+
if lock_definition.nil?
|
9
|
+
install_initially(definition)
|
10
|
+
elsif lock_definition.env != definition.env
|
11
|
+
Switch.new.call(env)
|
12
|
+
else
|
13
|
+
apply_diff(lock_definition, definition)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def install_initially(definition)
|
20
|
+
install(definition.themes, definition.plugins)
|
21
|
+
|
22
|
+
create_lock_file(definition.env)
|
23
|
+
print_state
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply_diff(lock_definition, definition)
|
27
|
+
diff = lock_definition.diff(definition)
|
28
|
+
|
29
|
+
install(diff.added_themes, diff.added_plugins)
|
30
|
+
uninstall(diff.deleted_themes, diff.deleted_plugins)
|
31
|
+
update(diff.changed_themes, diff.changed_plugins)
|
32
|
+
|
33
|
+
create_lock_file(definition.env)
|
34
|
+
print_state
|
35
|
+
end
|
36
|
+
|
37
|
+
def load_definition(env)
|
38
|
+
Definition.load_data.tap { |data|
|
39
|
+
data.env = env
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_lock_definition
|
44
|
+
Definition::Lock.load_data if Definition::Lock.file.exist?
|
45
|
+
end
|
46
|
+
|
47
|
+
def install(themes, plugins)
|
48
|
+
themes.each do
|
49
|
+
Extension::Theme::Installer.new(_1).install
|
50
|
+
end
|
51
|
+
|
52
|
+
plugins.each do
|
53
|
+
Extension::Plugin::Installer.new(_1).install
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def uninstall(themes, plugins)
|
58
|
+
themes.each do
|
59
|
+
Extension::Theme::Uninstaller.new(_1).uninstall
|
60
|
+
end
|
61
|
+
|
62
|
+
plugins.each do
|
63
|
+
Extension::Plugin::Uninstaller.new(_1).uninstall
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def update(themes, plugins)
|
68
|
+
themes.each do
|
69
|
+
Extension::Theme::Updater.new(_1).update
|
70
|
+
end
|
71
|
+
|
72
|
+
plugins.each do
|
73
|
+
Extension::Plugin::Updater.new(_1).update
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_lock_file(env)
|
78
|
+
Definition::Lock.create_file(env)
|
79
|
+
end
|
80
|
+
|
81
|
+
def print_state
|
82
|
+
State.new.call
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Commands
|
3
|
+
class State
|
4
|
+
def initialize
|
5
|
+
@lock_definition = Definition::Lock.load_data
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
return if no_lock_file_found
|
10
|
+
|
11
|
+
puts "Rexer: #{lock_definition.version}"
|
12
|
+
puts "Env: #{lock_definition.env}"
|
13
|
+
|
14
|
+
print_themes
|
15
|
+
print_plugins
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :lock_definition
|
21
|
+
|
22
|
+
def print_plugins
|
23
|
+
plugins = lock_definition.plugins
|
24
|
+
return if plugins.empty?
|
25
|
+
|
26
|
+
puts "\nPlugins:"
|
27
|
+
plugins.each do
|
28
|
+
puts " * #{_1.name} (#{source_info(_1.source)})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def print_themes
|
33
|
+
themes = lock_definition.themes
|
34
|
+
return if themes.empty?
|
35
|
+
|
36
|
+
puts "\nThemes:"
|
37
|
+
themes.each do
|
38
|
+
puts " * #{_1.name} (#{source_info(_1.source)})"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def source_info(source_def)
|
43
|
+
source_def.then {
|
44
|
+
Source.const_get(_1.type.capitalize).new(**_1.options).info
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def no_lock_file_found
|
49
|
+
lock_definition.nil?.tap { |result|
|
50
|
+
puts "No lock file found" if result
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Commands
|
3
|
+
class Switch
|
4
|
+
def initialize
|
5
|
+
@lock_definition = Definition::Lock.load_data
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
return if no_lock_file_found
|
10
|
+
return if already_on(env)
|
11
|
+
|
12
|
+
Uninstall.new.call
|
13
|
+
Install.new.call(env)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :lock_definition
|
19
|
+
|
20
|
+
def no_lock_file_found
|
21
|
+
lock_definition.nil?.tap { |result|
|
22
|
+
puts "No lock file found" if result
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def already_on(env)
|
27
|
+
(lock_definition.env == env).tap do |result|
|
28
|
+
puts "Already on #{env} environment" if result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Commands
|
3
|
+
class Uninstall
|
4
|
+
def initialize
|
5
|
+
@lock_definition = Definition::Lock.load_data
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
return if no_lock_file_found
|
10
|
+
|
11
|
+
uninstall_themes
|
12
|
+
uninstall_plugins
|
13
|
+
|
14
|
+
delete_lock_file
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :lock_definition
|
20
|
+
|
21
|
+
def uninstall_plugins
|
22
|
+
lock_definition.plugins.each do
|
23
|
+
Extension::Plugin::Uninstaller.new(_1).uninstall
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def uninstall_themes
|
28
|
+
lock_definition.themes.each do
|
29
|
+
Extension::Theme::Uninstaller.new(_1).uninstall
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete_lock_file
|
34
|
+
Definition::Lock.file.then { |file|
|
35
|
+
file.delete if file.exist?
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def no_lock_file_found
|
40
|
+
lock_definition.nil?.tap { |result|
|
41
|
+
puts "No lock file found" if result
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Commands
|
3
|
+
class Update
|
4
|
+
def initialize
|
5
|
+
@lock_definition = Definition::Lock.load_data
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
return if no_lock_file_found
|
10
|
+
|
11
|
+
update_themes
|
12
|
+
update_plugins
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :lock_definition
|
18
|
+
|
19
|
+
def update_plugins
|
20
|
+
lock_definition.plugins.each do
|
21
|
+
Extension::Plugin::Updater.new(_1).update
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_themes
|
26
|
+
lock_definition.themes.each do
|
27
|
+
Extension::Theme::Updater.new(_1).update
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def no_lock_file_found
|
32
|
+
lock_definition.nil?.tap { |result|
|
33
|
+
puts "No lock file found" if result
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Definition
|
3
|
+
class Data
|
4
|
+
attr_accessor :env
|
5
|
+
attr_reader :version
|
6
|
+
|
7
|
+
def initialize(plugins, themes, env: nil, version: nil)
|
8
|
+
@plugins = plugins
|
9
|
+
@themes = themes
|
10
|
+
@env = env
|
11
|
+
@version = version
|
12
|
+
end
|
13
|
+
|
14
|
+
def plugins
|
15
|
+
env ? @plugins.select { _1.env == env } : @plugins
|
16
|
+
end
|
17
|
+
|
18
|
+
def themes
|
19
|
+
env ? @themes.select { _1.env == env } : @themes
|
20
|
+
end
|
21
|
+
|
22
|
+
def diff(other)
|
23
|
+
Definition::Diff.new(self, other)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Definition
|
3
|
+
class Diff
|
4
|
+
def initialize(old_data, new_data)
|
5
|
+
@old_data = old_data
|
6
|
+
@new_data = new_data
|
7
|
+
end
|
8
|
+
|
9
|
+
def added_plugins
|
10
|
+
new_data.plugins - old_data.plugins
|
11
|
+
end
|
12
|
+
|
13
|
+
def added_themes
|
14
|
+
new_data.themes - old_data.themes
|
15
|
+
end
|
16
|
+
|
17
|
+
def deleted_plugins
|
18
|
+
old_data.plugins - new_data.plugins
|
19
|
+
end
|
20
|
+
|
21
|
+
def deleted_themes
|
22
|
+
old_data.themes - new_data.themes
|
23
|
+
end
|
24
|
+
|
25
|
+
def changed_plugins
|
26
|
+
old_plugins = old_data.plugins
|
27
|
+
|
28
|
+
(new_data.plugins & old_plugins).select do |new_plugin|
|
29
|
+
old_plugin = old_plugins.find { _1.name == new_plugin.name }
|
30
|
+
plugin_changed?(old_plugin, new_plugin)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def changed_themes
|
35
|
+
old_themes = old_data.themes
|
36
|
+
|
37
|
+
(new_data.themes & old_themes).select do |new_theme|
|
38
|
+
old_theme = old_themes.find { _1.name == new_theme.name }
|
39
|
+
theme_changed?(old_theme, new_theme)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :old_data, :new_data
|
46
|
+
|
47
|
+
def plugin_changed?(old_plugin, new_plugin)
|
48
|
+
old_plugin.source != new_plugin.source
|
49
|
+
end
|
50
|
+
|
51
|
+
def theme_changed?(old_theme, new_theme)
|
52
|
+
old_theme.source != new_theme.source
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Definition
|
3
|
+
class Dsl
|
4
|
+
def initialize(env = :default)
|
5
|
+
@plugins = []
|
6
|
+
@themes = []
|
7
|
+
@env = env
|
8
|
+
end
|
9
|
+
|
10
|
+
def plugin(name, **opts, &hooks)
|
11
|
+
@plugins << Definition::Plugin.new(
|
12
|
+
name: name,
|
13
|
+
source: build_source(opts),
|
14
|
+
hooks: build_hooks(hooks, %i[installed uninstalled updated]),
|
15
|
+
env: @env
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def theme(name, **opts, &hooks)
|
20
|
+
@themes << Definition::Theme.new(
|
21
|
+
name: name,
|
22
|
+
source: build_source(opts),
|
23
|
+
hooks: build_hooks(hooks, %i[installed uninstalled updated]),
|
24
|
+
env: @env
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def env(env_name, &dsl)
|
29
|
+
data = self.class.new(env_name).tap { _1.instance_eval(&dsl) }.to_data
|
30
|
+
|
31
|
+
@plugins += data.plugins
|
32
|
+
@themes += data.themes
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_data
|
36
|
+
Definition::Data.new(@plugins, @themes)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_hooks(definition_hooks, availabe_hooks)
|
42
|
+
return nil if definition_hooks.nil?
|
43
|
+
|
44
|
+
hook_dsl = Class.new do
|
45
|
+
def hooks = @hooks ||= {}
|
46
|
+
|
47
|
+
availabe_hooks.each do |hook_name|
|
48
|
+
define_method(hook_name) { |&block| hooks[hook_name] = block }
|
49
|
+
end
|
50
|
+
end.new
|
51
|
+
|
52
|
+
hook_dsl.instance_eval(&definition_hooks)
|
53
|
+
hook_dsl.hooks
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_source(opts)
|
57
|
+
type = opts.keys.find { Rexer::Source::Base.source_names.include?(_1) }
|
58
|
+
Source.new(type, opts[type]) if type
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Definition
|
3
|
+
module Lock
|
4
|
+
def self.file
|
5
|
+
@file ||= Pathname.new(Rexer.definition_lock_file)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.load_data
|
9
|
+
return nil unless file.exist?
|
10
|
+
|
11
|
+
dsl = Dsl.new.tap { _1.instance_eval(file.read) }
|
12
|
+
dsl.to_data
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create_file(env)
|
16
|
+
dsl = <<~DSL
|
17
|
+
lock version: "#{Rexer::VERSION}", env: :#{env}
|
18
|
+
|
19
|
+
#{Definition.file.read}
|
20
|
+
DSL
|
21
|
+
file.write(dsl)
|
22
|
+
end
|
23
|
+
|
24
|
+
class Dsl < Definition::Dsl
|
25
|
+
def lock(env:, version:)
|
26
|
+
lock_state.update(env:, version:)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_data
|
30
|
+
Definition::Data.new(@plugins, @themes, **lock_state)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def lock_state
|
36
|
+
@lock_state ||= {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Definition
|
3
|
+
module ExtensionComparable
|
4
|
+
def eql?(other)
|
5
|
+
name == other.name
|
6
|
+
end
|
7
|
+
|
8
|
+
def hash
|
9
|
+
name.hash
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Source = ::Data.define(:type, :options)
|
14
|
+
|
15
|
+
Plugin = ::Data.define(:name, :source, :hooks, :env) do
|
16
|
+
include ExtensionComparable
|
17
|
+
end
|
18
|
+
|
19
|
+
Theme = ::Data.define(:name, :source, :hooks, :env) do
|
20
|
+
include ExtensionComparable
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.file
|
24
|
+
@file ||= Pathname.new(Rexer.definition_file)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.load_data
|
28
|
+
dsl = Dsl.new.tap { _1.instance_eval(file.read) }
|
29
|
+
dsl.to_data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
module Rexer
|
4
|
+
module Extension
|
5
|
+
module Plugin
|
6
|
+
def self.dir
|
7
|
+
Pathname.new("plugins")
|
8
|
+
end
|
9
|
+
|
10
|
+
class Base
|
11
|
+
def initialize(definition)
|
12
|
+
@definition = definition
|
13
|
+
@name = definition.name
|
14
|
+
@hooks = definition.hooks || {}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :name, :hooks, :definition
|
20
|
+
|
21
|
+
def plugin_dir
|
22
|
+
@plugin_dir ||= Plugin.dir.join(name.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def plugin_exists?
|
26
|
+
plugin_dir.exist? && !plugin_dir.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def needs_db_migration?
|
30
|
+
plugin_dir.join("db", "migrate").then {
|
31
|
+
_1.exist? && !_1.empty?
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_db_migrate(extra_envs = {})
|
36
|
+
return unless needs_db_migration?
|
37
|
+
|
38
|
+
envs = {"NAME" => name.to_s}.merge(extra_envs)
|
39
|
+
_, error, status = Open3.capture3(envs, "bin/rails redmine:plugins:migrate")
|
40
|
+
|
41
|
+
raise error unless status.success?
|
42
|
+
end
|
43
|
+
|
44
|
+
def source
|
45
|
+
@source ||= definition.source.then do |src|
|
46
|
+
Source.const_get(src.type.capitalize).new(**src.options)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Installer < Base
|
52
|
+
def install
|
53
|
+
return if plugin_exists?
|
54
|
+
|
55
|
+
load_from_source
|
56
|
+
run_db_migrate
|
57
|
+
hooks[:installed]&.call
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def load_from_source
|
63
|
+
source.load(plugin_dir.to_s)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Uninstaller < Base
|
68
|
+
def uninstall
|
69
|
+
return unless plugin_exists?
|
70
|
+
|
71
|
+
reset_db_migration
|
72
|
+
remove_plugin
|
73
|
+
hooks[:uninstalled]&.call
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def reset_db_migration
|
79
|
+
run_db_migrate("VERSION" => "0")
|
80
|
+
end
|
81
|
+
|
82
|
+
def remove_plugin
|
83
|
+
plugin_dir.rmtree
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Updater < Base
|
88
|
+
def update
|
89
|
+
return unless plugin_exists?
|
90
|
+
|
91
|
+
update_source
|
92
|
+
run_db_migrate
|
93
|
+
hooks[:updated]&.call
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def update_source
|
99
|
+
source.update(plugin_dir.to_s)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Extension
|
3
|
+
module Theme
|
4
|
+
def self.dir
|
5
|
+
Pathname.new("themes")
|
6
|
+
end
|
7
|
+
|
8
|
+
class Base
|
9
|
+
def initialize(definition)
|
10
|
+
@definition = definition
|
11
|
+
@name = definition.name
|
12
|
+
@hooks = definition.hooks || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :name, :hooks, :definition
|
18
|
+
|
19
|
+
def theme_dir
|
20
|
+
@theme_dir ||= Theme.dir.join(name.to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
def theme_exists?
|
24
|
+
theme_dir.exist? && !theme_dir.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def source
|
28
|
+
@source ||= definition.source.then do |src|
|
29
|
+
Source.const_get(src.type.capitalize).new(**src.options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Installer < Base
|
35
|
+
def install
|
36
|
+
return if theme_exists?
|
37
|
+
|
38
|
+
load_from_source
|
39
|
+
hooks[:installed]&.call
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def load_from_source
|
45
|
+
source.load(theme_dir.to_s)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Uninstaller < Base
|
50
|
+
def uninstall
|
51
|
+
return unless theme_exists?
|
52
|
+
|
53
|
+
remove_theme
|
54
|
+
hooks[:uninstalled]&.call
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def remove_theme
|
60
|
+
theme_dir.rmtree
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Updater < Base
|
65
|
+
def update
|
66
|
+
return unless theme_exists?
|
67
|
+
|
68
|
+
update_source
|
69
|
+
hooks[:updated]&.call
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def update_source
|
75
|
+
source.update(theme_dir.to_s)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rexer
|
2
|
+
module Source
|
3
|
+
class Base
|
4
|
+
def self.source_names = @source_names ||= []
|
5
|
+
|
6
|
+
def self.inherited(subclass)
|
7
|
+
source_names << subclass.name.split("::").last.downcase.to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
def load(_path)
|
11
|
+
raise "Not implemented"
|
12
|
+
end
|
13
|
+
|
14
|
+
def update(_path)
|
15
|
+
raise "Not implemented"
|
16
|
+
end
|
17
|
+
|
18
|
+
def info = ""
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "git"
|
2
|
+
|
3
|
+
module Rexer
|
4
|
+
module Source
|
5
|
+
class Git < Base
|
6
|
+
def initialize(url:, branch: nil, tag: nil, ref: nil)
|
7
|
+
@url = url
|
8
|
+
@branch = branch
|
9
|
+
@tag = tag
|
10
|
+
@ref = ref
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(path)
|
14
|
+
::Git.clone(url, path).then { checkout(_1) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def update(path)
|
18
|
+
FileUtils.rm_rf(path)
|
19
|
+
load(path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def info
|
23
|
+
branch || tag || ref || "master"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :url, :branch, :tag, :ref
|
29
|
+
|
30
|
+
def checkout(git)
|
31
|
+
(branch || tag || ref)&.then { git.checkout(_1) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rexer.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rexer
|
2
|
+
def self.definition_file
|
3
|
+
".extensions.rb"
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.definition_lock_file
|
7
|
+
".extensions.lock"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
require "zeitwerk"
|
13
|
+
|
14
|
+
loader = Zeitwerk::Loader.for_gem
|
15
|
+
loader.setup
|
16
|
+
loader.eager_load
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rexer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Katsuya Hidaka
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-08-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: git
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: zeitwerk
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.6'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.6'
|
55
|
+
description: Rexer is a tool for managing Redmine Extension (Plugin and Themes). It
|
56
|
+
allows you to define extensions in a Ruby DSL and install, uninstall, update, and
|
57
|
+
switch between different sets of the extensions.
|
58
|
+
email:
|
59
|
+
- hidakatsuya@gmail.com
|
60
|
+
executables:
|
61
|
+
- rex
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- bin/console
|
68
|
+
- bin/dev
|
69
|
+
- exe/rex
|
70
|
+
- lib/rexer.rb
|
71
|
+
- lib/rexer/cli.rb
|
72
|
+
- lib/rexer/commands/install.rb
|
73
|
+
- lib/rexer/commands/state.rb
|
74
|
+
- lib/rexer/commands/switch.rb
|
75
|
+
- lib/rexer/commands/uninstall.rb
|
76
|
+
- lib/rexer/commands/update.rb
|
77
|
+
- lib/rexer/definition.rb
|
78
|
+
- lib/rexer/definition/data.rb
|
79
|
+
- lib/rexer/definition/diff.rb
|
80
|
+
- lib/rexer/definition/dsl.rb
|
81
|
+
- lib/rexer/definition/lock.rb
|
82
|
+
- lib/rexer/extension/plugin.rb
|
83
|
+
- lib/rexer/extension/theme.rb
|
84
|
+
- lib/rexer/source/base.rb
|
85
|
+
- lib/rexer/source/git.rb
|
86
|
+
- lib/rexer/source/github.rb
|
87
|
+
- lib/rexer/version.rb
|
88
|
+
homepage: https://github.com/hidakatsuya/rexer
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata:
|
92
|
+
source_code_uri: https://github.com/hidakatsuya/rexer
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.0.0
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubygems_version: 3.5.11
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: A tool for managing Redmine Plugins and Themes
|
112
|
+
test_files: []
|