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 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
+ [![Build](https://github.com/hidakatsuya/rexer/actions/workflows/build.yml/badge.svg)](https://github.com/hidakatsuya/rexer/actions/workflows/build.yml)
8
+ [![Gem Version](https://badge.fury.io/rb/rexer.svg)](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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/rexer"
4
+
5
+ Rexer::Cli.start(ARGV)
data/exe/rex ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rexer"
4
+
5
+ begin
6
+ Rexer::Cli.start(ARGV)
7
+ rescue => e
8
+ puts "ERROR (#{e.class}): #{e.message}"
9
+ puts e.backtrace if ENV["VERBOSE"]
10
+ exit 1
11
+ end
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
@@ -0,0 +1,9 @@
1
+ module Rexer
2
+ module Source
3
+ class Github < Git
4
+ def initialize(repo:, branch: nil, tag: nil, ref: nil)
5
+ super(url: "https://github.com/#{repo}", branch: branch, tag: tag, ref: ref)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Rexer
2
+ VERSION = "0.1.0"
3
+ 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: []