automatic 14.12.2 → 26.09
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 +5 -5
- data/README.md +669 -83
- data/VERSION +1 -1
- data/assets/siteinfo/items_all.json +60300 -52138
- data/automatic.gemspec +110 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +542 -0
- data/doc/BASIC_DESIGN.md +547 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +830 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +2015 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +962 -0
- data/doc/QUICKSTART.md +260 -0
- data/doc/RELEASING.md +394 -0
- data/doc/REQUIREMENTS.md +556 -0
- data/doc/VERSIONS +266 -0
- data/lib/automatic/cli.rb +270 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +65 -25
- data/lib/automatic/feed_parser.rb +51 -35
- data/lib/automatic/http.rb +107 -0
- data/lib/automatic/log.rb +49 -18
- data/lib/automatic/opml.rb +3 -1
- data/lib/automatic/pipeline.rb +73 -32
- data/lib/automatic/recipe.rb +100 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +93 -20
- data/plugins/custom_feed/svn_log.rb +73 -32
- data/plugins/custom_feed/web.rb +348 -0
- data/plugins/filter/absolute_uri.rb +43 -27
- data/plugins/filter/accept.rb +38 -45
- data/plugins/filter/batch.rb +97 -0
- data/plugins/filter/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +66 -51
- data/plugins/filter/full_feed.rb +170 -52
- data/plugins/filter/gemini.rb +216 -0
- data/plugins/filter/github_feed.rb +38 -26
- data/plugins/filter/ignore.rb +33 -44
- data/plugins/filter/image.rb +36 -25
- data/plugins/filter/image_source.rb +75 -52
- data/plugins/filter/join.rb +105 -0
- data/plugins/filter/kimi.rb +216 -0
- data/plugins/filter/limit.rb +57 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -0
- data/plugins/filter/present.rb +75 -0
- data/plugins/filter/rand.rb +16 -17
- data/plugins/filter/sakura_ai.rb +205 -0
- data/plugins/filter/sanitize.rb +29 -34
- data/plugins/filter/sort.rb +20 -27
- data/plugins/filter/tumblr_resize.rb +31 -23
- data/plugins/notify/ikachan.rb +86 -48
- data/plugins/provide/fluentd.rb +43 -24
- data/plugins/publish/amazon_s3.rb +79 -40
- data/plugins/publish/console.rb +19 -16
- data/plugins/publish/console_link.rb +20 -17
- data/plugins/publish/eject.rb +48 -26
- data/plugins/publish/fluentd.rb +50 -30
- data/plugins/publish/hatena_bookmark.rb +88 -71
- data/plugins/publish/instapaper.rb +69 -59
- data/plugins/publish/markdown.rb +278 -0
- data/plugins/publish/memcached.rb +35 -29
- data/plugins/store/database.rb +50 -48
- data/plugins/store/digest.rb +212 -0
- data/plugins/store/file.rb +107 -68
- data/plugins/store/full_text.rb +31 -28
- data/plugins/store/permalink.rb +18 -22
- data/plugins/subscription/feed.rb +34 -21
- data/plugins/subscription/link.rb +31 -32
- data/plugins/subscription/text.rb +40 -45
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +112 -321
- data/Gemfile +0 -38
- data/Rakefile +0 -59
- data/doc/ChangeLog +0 -303
- data/doc/PLUGINS +0 -750
- data/doc/PLUGINS.ja +0 -753
- data/doc/README +0 -511
- data/doc/README.ja +0 -519
- data/plugins/filter/google_news.rb +0 -50
- data/plugins/publish/google_calendar.rb +0 -84
- data/plugins/publish/hipchat.rb +0 -46
- data/plugins/publish/pocket.rb +0 -45
- data/plugins/publish/twitter.rb +0 -58
- data/plugins/subscription/chan_toru.rb +0 -57
- data/plugins/subscription/g_guide.rb +0 -57
- data/plugins/subscription/pocket.rb +0 -51
- data/plugins/subscription/twitter.rb +0 -69
- data/plugins/subscription/twitter_search.rb +0 -50
- data/plugins/subscription/weather.rb +0 -33
- data/script/build +0 -84
- data/spec/fixtures/sampleFeeds.tsv +0 -1
- data/spec/fixtures/sampleFeeds2.tsv +0 -2
- data/spec/fixtures/sampleRecipe.yml +0 -24
- data/spec/lib/automatic/log_spec.rb +0 -32
- data/spec/lib/automatic/pipeline_spec.rb +0 -68
- data/spec/lib/automatic/recipe_spec.rb +0 -40
- data/spec/lib/automatic_spec.rb +0 -99
- data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
- data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
- data/spec/plugins/filter/accept_spec.rb +0 -331
- data/spec/plugins/filter/clear_spec.rb +0 -49
- data/spec/plugins/filter/description_link_spec.rb +0 -138
- data/spec/plugins/filter/full_feed_spec.rb +0 -129
- data/spec/plugins/filter/github_feed_spec.rb +0 -55
- data/spec/plugins/filter/google_news_spec.rb +0 -69
- data/spec/plugins/filter/ignore_spec.rb +0 -328
- data/spec/plugins/filter/image_source_spec.rb +0 -89
- data/spec/plugins/filter/image_spec.rb +0 -65
- data/spec/plugins/filter/one_spec.rb +0 -71
- data/spec/plugins/filter/rand_spec.rb +0 -52
- data/spec/plugins/filter/sanitize_spec.rb +0 -153
- data/spec/plugins/filter/sort_spec.rb +0 -189
- data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
- data/spec/plugins/notify/ikachan_spec.rb +0 -58
- data/spec/plugins/provide/fluentd_spec.rb +0 -49
- data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
- data/spec/plugins/publish/console_spec.rb +0 -30
- data/spec/plugins/publish/eject_spec.rb +0 -40
- data/spec/plugins/publish/fluentd_spec.rb +0 -40
- data/spec/plugins/publish/google_calendar_spec.rb +0 -83
- data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
- data/spec/plugins/publish/hipchat_spec.rb +0 -69
- data/spec/plugins/publish/instapaper_spec.rb +0 -82
- data/spec/plugins/publish/memcached_spec.rb +0 -63
- data/spec/plugins/publish/pocket_spec.rb +0 -51
- data/spec/plugins/publish/twitter_spec.rb +0 -73
- data/spec/plugins/store/file_spec.rb +0 -58
- data/spec/plugins/store/full_text_spec.rb +0 -152
- data/spec/plugins/store/permalink_spec.rb +0 -206
- data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
- data/spec/plugins/subscription/feed_spec.rb +0 -71
- data/spec/plugins/subscription/g_guide_spec.rb +0 -82
- data/spec/plugins/subscription/link_spec.rb +0 -72
- data/spec/plugins/subscription/pocket_spec.rb +0 -57
- data/spec/plugins/subscription/text_spec.rb +0 -84
- data/spec/plugins/subscription/tumblr_spec.rb +0 -74
- data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
- data/spec/plugins/subscription/twitter_spec.rb +0 -73
- data/spec/plugins/subscription/weather_spec.rb +0 -44
- data/spec/plugins/subscription/xml_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -106
- data/spec/user_dir/plugins/store/mock.rb +0 -16
- data/test/fixtures/sampleOPML.xml +0 -11
- data/test/integration/test_absoluteurl.yml +0 -25
- data/test/integration/test_activerecord.yml +0 -24
- data/test/integration/test_add_pocket.yml +0 -26
- data/test/integration/test_chan_toru.yml +0 -21
- data/test/integration/test_descriptionlink.yml +0 -21
- data/test/integration/test_fluentd.yml +0 -22
- data/test/integration/test_fulltext.yml +0 -30
- data/test/integration/test_google_news.yml +0 -21
- data/test/integration/test_googlealert.yml +0 -21
- data/test/integration/test_hatenabookmark.yml +0 -30
- data/test/integration/test_ignore.yml +0 -25
- data/test/integration/test_ignore2.yml +0 -22
- data/test/integration/test_image2local.yml +0 -33
- data/test/integration/test_instapaper.yml +0 -26
- data/test/integration/test_link2local.yml +0 -34
- data/test/integration/test_one.yml +0 -23
- data/test/integration/test_pocket.yml +0 -22
- data/test/integration/test_rand.yml +0 -21
- data/test/integration/test_sanitize.yml +0 -23
- data/test/integration/test_sort.yml +0 -36
- data/test/integration/test_svnlog.yml +0 -15
- data/test/integration/test_text2feed.yml +0 -36
- data/test/integration/test_tumblr2local.yml +0 -43
- data/test/integration/test_twitter_search.yml +0 -22
- data/test/integration/test_weather.yml +0 -19
- data/test/integration/test_xml2fluentd.yml +0 -21
- data/vendor/.gitkeep +0 -0
data/lib/automatic/recipe.rb
CHANGED
|
@@ -1,37 +1,120 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
2
|
+
# Name:: Automatic::Recipe
|
|
3
|
+
# Author: ainame
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 18, 2012
|
|
8
|
+
# Updated:: Sep 6, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# Turns a Recipe file into something the pipeline can iterate. The format is
|
|
12
|
+
# specified in doc/PLUGINS.md section 2.
|
|
13
|
+
#
|
|
14
|
+
# Each plugin entry's documented shape -- a mapping naming a module, with an
|
|
15
|
+
# optional config mapping -- is validated here, at load time, rather than
|
|
16
|
+
# left to surface as whatever internal exception a malformed entry happens to
|
|
17
|
+
# raise once the pipeline reaches it.
|
|
9
18
|
|
|
19
|
+
require 'date'
|
|
10
20
|
require 'hashie'
|
|
11
21
|
require 'yaml'
|
|
12
22
|
|
|
13
23
|
module Automatic
|
|
14
24
|
class Recipe
|
|
25
|
+
# A Recipe is trusted local configuration, but it is still loaded safely:
|
|
26
|
+
# the document may name none of its own Ruby classes to instantiate. That
|
|
27
|
+
# costs nothing, since no Recipe needs it. It is a second line of defence
|
|
28
|
+
# and not the trust boundary; see doc/REQUIREMENTS.md section 17.
|
|
29
|
+
PERMITTED_CLASSES = [Date, Time, Symbol].freeze
|
|
30
|
+
|
|
15
31
|
attr_reader :procedure
|
|
16
32
|
|
|
17
|
-
def initialize(path =
|
|
33
|
+
def initialize(path = '')
|
|
18
34
|
load_recipe(path)
|
|
19
35
|
end
|
|
20
36
|
|
|
21
37
|
def load_recipe(path)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
resolved = resolve_path(path)
|
|
39
|
+
document = parse(resolved)
|
|
40
|
+
|
|
41
|
+
unless document.is_a?(Hash)
|
|
42
|
+
raise InvalidRecipeError,
|
|
43
|
+
"recipe #{resolved} is not a mapping"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@procedure = Hashie::Mash.new(document)
|
|
47
|
+
|
|
48
|
+
unless @procedure.plugins.is_a?(Array)
|
|
49
|
+
raise InvalidRecipeError,
|
|
50
|
+
"recipe #{resolved} has no plugins sequence"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
validate_plugins(resolved)
|
|
54
|
+
|
|
55
|
+
Automatic::Log.level(@procedure.global&.log&.level)
|
|
56
|
+
Automatic::Log.puts('info', "Loading Recipe: #{resolved}")
|
|
28
57
|
@procedure
|
|
29
58
|
end
|
|
30
59
|
|
|
31
60
|
def each_plugin
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
61
|
+
return enum_for(:each_plugin) unless block_given?
|
|
62
|
+
|
|
63
|
+
@procedure.plugins.each { |plugin| yield plugin }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# A bare filename is looked for in the user's config directory first;
|
|
69
|
+
# anything not found there is used as a path exactly as given.
|
|
70
|
+
def resolve_path(path)
|
|
71
|
+
in_user_dir = File.join(Automatic.user_config_dir, path.to_s)
|
|
72
|
+
File.exist?(in_user_dir) ? in_user_dir : path.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse(path)
|
|
76
|
+
YAML.safe_load(
|
|
77
|
+
File.read(path),
|
|
78
|
+
permitted_classes: PERMITTED_CLASSES,
|
|
79
|
+
aliases: true
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Only the shape doc/PLUGINS.md documents is checked here: a mapping
|
|
84
|
+
# naming a module, with an optional config mapping. What a plugin does
|
|
85
|
+
# with its own config is that plugin's concern, not this one's.
|
|
86
|
+
def validate_plugins(resolved)
|
|
87
|
+
@procedure.plugins.each_with_index do |plugin, index|
|
|
88
|
+
validate_plugin_entry(resolved, plugin, index)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_plugin_entry(resolved, plugin, index)
|
|
93
|
+
unless plugin.is_a?(Hash)
|
|
94
|
+
raise InvalidRecipeError,
|
|
95
|
+
"recipe #{resolved} plugins[#{index}] is not a mapping"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
validate_module_name(resolved, plugin, index)
|
|
99
|
+
validate_plugin_config(resolved, plugin, index)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_module_name(resolved, plugin, index)
|
|
103
|
+
name = plugin['module']
|
|
104
|
+
|
|
105
|
+
return if name.is_a?(String) && !name.strip.empty?
|
|
106
|
+
|
|
107
|
+
raise InvalidRecipeError,
|
|
108
|
+
"recipe #{resolved} plugins[#{index}] has no module name"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_plugin_config(resolved, plugin, index)
|
|
112
|
+
config = plugin['config']
|
|
113
|
+
|
|
114
|
+
return if config.nil? || config.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
raise InvalidRecipeError,
|
|
117
|
+
"recipe #{resolved} plugins[#{index}] has a config that is not a mapping"
|
|
35
118
|
end
|
|
36
119
|
end
|
|
37
120
|
end
|
data/lib/automatic/version.rb
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Name:: Automatic::VERSION
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 18, 2012
|
|
8
|
+
# Updated:: Aug 25, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
|
|
1
11
|
module Automatic
|
|
2
|
-
VERSION
|
|
12
|
+
# The release version. The VERSION file at the repository root carries the
|
|
13
|
+
# same number and the gemspec reads it from there; a spec asserts that the
|
|
14
|
+
# two agree. See doc/POLICY.md section 10.
|
|
15
|
+
VERSION = '26.09'
|
|
3
16
|
end
|
data/lib/automatic.rb
CHANGED
|
@@ -1,54 +1,127 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Ruby
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 18, 2012
|
|
8
|
+
# Updated:: Sep 6, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# The framework module: the two directories everything else resolves paths
|
|
12
|
+
# against, and the one method that runs a Recipe. See doc/BASIC_DESIGN.md
|
|
13
|
+
# section 4.3.
|
|
8
14
|
|
|
9
15
|
module Automatic
|
|
10
16
|
require 'automatic/environment'
|
|
17
|
+
require 'automatic/log'
|
|
11
18
|
require 'automatic/recipe'
|
|
12
19
|
require 'automatic/pipeline'
|
|
13
|
-
require 'automatic/
|
|
20
|
+
require 'automatic/http'
|
|
14
21
|
require 'automatic/feed_parser'
|
|
15
22
|
require 'automatic/feed_maker'
|
|
16
23
|
require 'automatic/version'
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
# The user directory, relative to the home directory.
|
|
26
|
+
USER_DIR = '/.automatic'
|
|
27
|
+
|
|
28
|
+
# Base class for every failure the framework raises itself, so that a caller
|
|
29
|
+
# can rescue these without rescuing everything.
|
|
30
|
+
class Error < StandardError; end
|
|
31
|
+
|
|
32
|
+
# Pipeline.run was given no Recipe.
|
|
33
|
+
class NoRecipeError < Error; end
|
|
34
|
+
|
|
35
|
+
# A Recipe named a module for which no plugin file could be found.
|
|
36
|
+
class NoPluginError < Error; end
|
|
37
|
+
|
|
38
|
+
# A Recipe parsed, but is not a document this framework can run.
|
|
39
|
+
class InvalidRecipeError < Error; end
|
|
40
|
+
|
|
41
|
+
# Raised by require_optional when, and only when, the exact feature it was
|
|
42
|
+
# asked to require could not be found. A LoadError subtype rather than an
|
|
43
|
+
# Error subtype, so that a caller already rescuing LoadError still catches
|
|
44
|
+
# it; distinct from plain LoadError so that a caller can tell "the optional
|
|
45
|
+
# gem itself is missing" apart from a LoadError raised from inside that
|
|
46
|
+
# gem's own load. See doc/POLICY.md section 9.1.
|
|
47
|
+
class OptionalDependencyError < LoadError; end
|
|
19
48
|
|
|
20
49
|
class << self
|
|
21
50
|
attr_accessor :root_dir
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
# Require a gem that only one plugin, or one optional path, needs, and turn
|
|
53
|
+
# its absence into a message naming the gem, what wanted it and how to get
|
|
54
|
+
# it. Nothing here resolves, installs or tracks a dependency: the `require`
|
|
55
|
+
# is the plugin's own, made where the plugin makes it, and this only
|
|
56
|
+
# replaces `cannot load such file -- dalli` with a sentence an operator can
|
|
57
|
+
# act on. See doc/POLICY.md section 9.1 and doc/PLUGINS.md section 3.8.
|
|
58
|
+
#
|
|
59
|
+
# Automatic.require_optional('sanitize', needed_by: 'FilterSanitize')
|
|
60
|
+
#
|
|
61
|
+
# `gem_name` is given where it differs from the path required, as
|
|
62
|
+
# `active_record` does from the `activerecord` gem.
|
|
63
|
+
def require_optional(feature, needed_by:, gem_name: feature)
|
|
64
|
+
require feature
|
|
65
|
+
rescue LoadError => e
|
|
66
|
+
# e.path is the argument require failed to find. When it matches
|
|
67
|
+
# feature, this require itself is what failed, and the gem naming this
|
|
68
|
+
# feature is what is missing. When it does not, the failure happened
|
|
69
|
+
# somewhere inside feature's own load -- a different missing file -- and
|
|
70
|
+
# is not this gem's absence; it is re-raised unconverted.
|
|
71
|
+
raise e unless e.path == feature
|
|
72
|
+
|
|
73
|
+
raise OptionalDependencyError,
|
|
74
|
+
"The `#{gem_name}` gem is not installed. It is needed by #{needed_by}. " \
|
|
75
|
+
"Install it with `gem install #{gem_name}`, or in a source checkout add " \
|
|
76
|
+
'its group to the bundle; see the optional plugin dependencies in ' \
|
|
77
|
+
"doc/DEPLOYMENT.md. (#{e.message})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Run one Recipe. root_dir is the installation root; user_dir is honoured
|
|
81
|
+
# only under AUTOMATIC_RUBY_ENV=test, which is how the specs point the
|
|
82
|
+
# plugin loader at a fixture directory.
|
|
83
|
+
def run(args = {})
|
|
24
84
|
self.root_dir = args[:root_dir]
|
|
25
85
|
self.user_dir = args[:user_dir]
|
|
26
86
|
Automatic::Pipeline.run(args[:recipe])
|
|
27
87
|
end
|
|
28
88
|
|
|
29
89
|
def plugins_dir
|
|
30
|
-
File.join(@root_dir,
|
|
90
|
+
File.join(@root_dir.to_s, 'plugins')
|
|
31
91
|
end
|
|
32
92
|
|
|
33
93
|
def config_dir
|
|
34
|
-
File.join(@root_dir,
|
|
94
|
+
File.join(@root_dir.to_s, 'config')
|
|
35
95
|
end
|
|
36
96
|
|
|
97
|
+
# Defaults to ~/.automatic when nothing has set it, so that a caller which
|
|
98
|
+
# never went through .run still resolves the user directory.
|
|
37
99
|
def user_dir
|
|
38
|
-
@user_dir
|
|
100
|
+
@user_dir ||= default_user_dir
|
|
39
101
|
end
|
|
40
102
|
|
|
41
|
-
def user_dir=(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
103
|
+
def user_dir=(dir)
|
|
104
|
+
@user_dir =
|
|
105
|
+
if ENV['AUTOMATIC_RUBY_ENV'] == 'test' && !dir.nil?
|
|
106
|
+
dir
|
|
107
|
+
else
|
|
108
|
+
default_user_dir
|
|
109
|
+
end
|
|
47
110
|
end
|
|
48
111
|
|
|
49
112
|
def user_plugins_dir
|
|
50
|
-
File.join(
|
|
113
|
+
File.join(user_dir.to_s, 'plugins')
|
|
51
114
|
end
|
|
52
|
-
end
|
|
53
115
|
|
|
116
|
+
# Where a Recipe named by a bare filename is looked for.
|
|
117
|
+
def user_config_dir
|
|
118
|
+
File.join(user_dir.to_s, 'config')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def default_user_dir
|
|
124
|
+
File.expand_path('~') + USER_DIR
|
|
125
|
+
end
|
|
126
|
+
end
|
|
54
127
|
end
|
|
@@ -1,55 +1,96 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Plugin::CustomFeed::SVNLog
|
|
3
|
+
# Author: kzgs
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 29, 2012
|
|
8
|
+
# Updated:: Aug 15, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
8
10
|
|
|
11
|
+
require 'rexml/document'
|
|
9
12
|
require 'rss/maker'
|
|
13
|
+
require 'time'
|
|
10
14
|
|
|
11
15
|
module Automatic::Plugin
|
|
12
16
|
class CustomFeedSVNLog
|
|
13
|
-
|
|
17
|
+
DEFAULT_FETCH_ITEMS = 30
|
|
14
18
|
|
|
15
|
-
def initialize(config, pipeline=[])
|
|
16
|
-
@config
|
|
19
|
+
def initialize(config, pipeline = [])
|
|
20
|
+
@config = config || {}
|
|
17
21
|
@pipeline = pipeline
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def run
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return @pipeline
|
|
25
|
+
entries = revisions
|
|
26
|
+
if entries.empty?
|
|
27
|
+
# RSS 1.0 has no representation for a channel with no items, and a
|
|
28
|
+
# repository with no revisions in the window asked for is an ordinary
|
|
29
|
+
# answer rather than a failure.
|
|
30
|
+
Automatic::Log.puts('warn', "No revisions returned by svn log for #{base_url}")
|
|
31
|
+
return @pipeline
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@pipeline << feed(entries)
|
|
35
|
+
@pipeline
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
private
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
-
|
|
40
|
+
def feed(revisions)
|
|
41
|
+
RSS::Maker.make('1.0') do |maker|
|
|
42
|
+
maker.channel.title = @config['title'].to_s
|
|
43
|
+
maker.channel.about = ''
|
|
44
|
+
maker.channel.description = ''
|
|
45
|
+
maker.channel.link = base_url
|
|
46
|
+
|
|
47
|
+
revisions.each do |revision|
|
|
48
|
+
item = maker.items.new_item
|
|
49
|
+
item.title = "#{revision['msg']} by #{revision['author']}"
|
|
50
|
+
item.link = "#{base_url}/!svn/bc/#{revision['revision']}"
|
|
51
|
+
item.date = Time.parse(revision['date'])
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `svn log --xml` as REXML sees it. REXML is a runtime dependency of this
|
|
57
|
+
# framework already -- the OPML parser uses it -- so this plugin needs the
|
|
58
|
+
# svn command and no gem of its own.
|
|
59
|
+
def revisions
|
|
60
|
+
REXML::Document.new(svn_log).elements.to_a('/log/logentry').map do |entry|
|
|
61
|
+
{
|
|
62
|
+
'revision' => entry.attributes['revision'].to_s,
|
|
63
|
+
'author' => text(entry, 'author'),
|
|
64
|
+
'msg' => text(entry, 'msg'),
|
|
65
|
+
'date' => text(entry, 'date')
|
|
66
|
+
}
|
|
67
|
+
end
|
|
41
68
|
end
|
|
42
69
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
def text(entry, name)
|
|
71
|
+
element = entry.elements[name]
|
|
72
|
+
element.nil? ? '' : element.text.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The command is run as an argument vector rather than through a shell, so
|
|
76
|
+
# a repository URL cannot become part of a command line. Point `target` at
|
|
77
|
+
# a repository you control regardless: svn itself will do what the URL
|
|
78
|
+
# tells it to.
|
|
79
|
+
def svn_log
|
|
80
|
+
output = IO.popen(['svn', 'log', base_url, '--xml', "--limit=#{limit}"],
|
|
81
|
+
err: File::NULL, &:read)
|
|
82
|
+
raise "svn log failed for #{base_url}" unless $?.success?
|
|
83
|
+
|
|
84
|
+
output
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def base_url
|
|
88
|
+
@base_url ||= @config['target'].to_s.sub(%r{/\z}, '')
|
|
49
89
|
end
|
|
50
90
|
|
|
51
91
|
def limit
|
|
52
|
-
|
|
92
|
+
value = @config['fetch_items'].to_i
|
|
93
|
+
value.positive? ? value : DEFAULT_FETCH_ITEMS
|
|
53
94
|
end
|
|
54
95
|
end
|
|
55
96
|
end
|