rexer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []