subtrigger 0.2.7 → 0.3.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.
@@ -0,0 +1,82 @@
1
+ module Subtrigger
2
+ # Our own little implementation of the path, allowing us to look up
3
+ # the location of executable files. This is because Subversion hooks
4
+ # run in a clean environment, without any environment variables such as
5
+ # <tt>$PATH</tt>. We therefore need to run (for example)
6
+ # <tt>/usr/bin/svn update</tt> rather than <tt>svn update</tt>.
7
+ #
8
+ # There is a list of default locations that will be searched, but you
9
+ # may add your own if you want to. This is useful if you've got a custom
10
+ # installation on your machine you want to use.
11
+ #
12
+ # Note: testing whether an executable exists in a given path is done using
13
+ # the unix program <tt>test</tt>, which will most likely not work on
14
+ # windows machines (untested).
15
+ #
16
+ # @example Getting the path to an executable
17
+ # Path.new.to('svn') #=> '/usr/bin'
18
+ #
19
+ # @example Adding a preferred location
20
+ # path = Path.new
21
+ # path << '/opt/local'
22
+ # path.to('svn') => '/opt/local'
23
+ #
24
+ # @author Arjan van der Gaag
25
+ # @since 0.3.0
26
+ class Path
27
+
28
+ # The default list of paths to look in, covering most of the use cases.
29
+ DEFAULT_PATHS = %w{/opt/subversion/bin /usr/sbin /usr/bin}
30
+
31
+ # Custom exception raised when a program is not found in any of the
32
+ # locations known.
33
+ NotFound = Class.new(Exception)
34
+
35
+ # A list of absolute paths on te filesystems to where the svn executables
36
+ # might be located. These are scanned in order to find the executables
37
+ # to use.
38
+ attr_reader :locations
39
+
40
+ # Start a new list of paths, starting with the <tt>DEFAULT_PATHS</tt>
41
+ def initialize
42
+ @locations = DEFAULT_PATHS.dup # use a copy to prevent global state
43
+ @exists = Hash.new do |hash, p|
44
+ hash[p] = system('test -x ' + p)
45
+ end
46
+ end
47
+
48
+ # Add a new path to the stack before the existing ones.
49
+ #
50
+ # @param [String] new_path is a new possible location of executables
51
+ # @return [Array<String>] the total list of paths
52
+ def <<(new_path)
53
+ @locations.unshift(new_path)
54
+ end
55
+
56
+ # Scan all the known paths to find the given program.
57
+ #
58
+ # Note: this probably only works on unix-like systems.
59
+ #
60
+ # @todo implement memoization per argument
61
+ # @param [String] program is the name of the executable to find, like
62
+ # <tt>svn</tt>
63
+ # @return [String] the correct path to this program or nil
64
+ # @raise NotFoundException when the program is not found in any of the
65
+ # known locations
66
+ def to(program)
67
+ location = locations.find { |path| exists? File.join(path, program) }
68
+ raise NotFound.new(program) unless location
69
+ location
70
+ end
71
+
72
+ private
73
+
74
+ # Make the actual test if a given path points to an executable file.
75
+ #
76
+ # @param [String] path is the absolute path to test
77
+ # @return [Boolean] whether the path is an executable file
78
+ def exists?(path)
79
+ @exists[path]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,95 @@
1
+ module Subtrigger
2
+ # A simple wrapper around the output of Subversion's <tt>svnlook</tt>
3
+ # command.
4
+ #
5
+ # This class will let you make simple queries against the properties of a
6
+ # Subversion revision. It parses its output into keys and values so you can
7
+ # perform operations on them.
8
+ #
9
+ # == Attributes
10
+ #
11
+ # It knows about the following attributes:
12
+ #
13
+ # * Revision number
14
+ # * Author
15
+ # * Timestamp
16
+ # * Log message
17
+ # * changed directories
18
+ #
19
+ # This works by passing in the number of the revision to use, the raw
20
+ # output of <tt>svnlook info</tt> and the raw output of
21
+ # <tt>svnlook dirs-changed</tt>.
22
+ #
23
+ # == Special attributes
24
+ #
25
+ # Revision knows about changed projects. This is extracted from the list
26
+ # of changed directories. A project is a directory that is directly above
27
+ # a directory named <tt>trunk</tt>, <tt>branches</tt> or <tt>tags</tt>. So
28
+ # when a directory <tt>/internal/accounting/trunk</tt> is changed, the
29
+ # project <tt>/internal/accounting</tt> is reported.
30
+ #
31
+ # @example Example of raw input for <tt>info</tt>
32
+ # john
33
+ # 2010-07-05 17:00:00 +0200 (Mon, 01 Jan 2010)
34
+ # 215
35
+ # Description of log
36
+ #
37
+ # @example Usage
38
+ # @revision = Revision.new('...')
39
+ # @revision.author # => 'john'
40
+ # @revision.message # => 'Description of log'
41
+ # @revision.date # => (instance of Time)
42
+ # @revision.projects # => ['/project1', 'project2', ...]
43
+ #
44
+ # @author Arjan van der Gaag
45
+ # @since 0.3.0
46
+ class Revision
47
+ # The raw output of the svnlook command.
48
+ attr_reader :raw
49
+
50
+ # A list of all directories that were changed in this revision
51
+ attr_reader :dirs_changed
52
+
53
+ # the parsed Hash of attributes for this revision
54
+ attr_reader :attributes
55
+
56
+ def initialize(revision_number, info, dirs_changed)
57
+ @attributes = { :number => revision_number.to_i }
58
+ @raw = info
59
+ @dirs_changed = dirs_changed.split
60
+ parse
61
+ end
62
+
63
+ %w{author date message number}.each do |name|
64
+ define_method(name) do
65
+ attributes[name.to_sym]
66
+ end
67
+ end
68
+
69
+ # Creates a list of directory paths in the repository that have changes
70
+ # and contain a <tt>trunk</tt>, <tt>branches</tt> or <tt>tags</tt>
71
+ # directory.
72
+ #
73
+ # For example, a changed path in like <tt>/topdir/project_name/trunk</tt>
74
+ # would result in <tt>/topdir/project_name</tt>.
75
+ #
76
+ # @return [Array<String>] list of changed project paths
77
+ def projects
78
+ pattern = /\/(trunk|branches|tags)/
79
+ dirs_changed.grep(pattern).map do |dir|
80
+ dir.split(pattern, 2).first
81
+ end.uniq
82
+ end
83
+
84
+ private
85
+
86
+ # Parses the raw log of svnlook into a Hash of attributes.
87
+ def parse
88
+ raise ArgumentError, 'Could not parse Subversion info: expected at least 4 lines' if raw.split("\n").size < 4
89
+ author, timestamp, size, message = raw.split("\n", 4)
90
+ attributes[:author] = author
91
+ attributes[:date] = Time.parse(timestamp)
92
+ attributes[:message] = message.chomp
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,165 @@
1
+ module Subtrigger
2
+ # A <tt>Rule</tt> object knows when to fire some kind of action for some
3
+ # kind of revision. When the Subversion hook is fired, a Rule can inspect it
4
+ # and choose whether or not to fire its trigger (a piece code defined by the
5
+ # user).
6
+ #
7
+ # In the first example, the rule will output <tt>fired</tt> whenever a
8
+ # <tt>Revision</tt> comes along with a message containing <tt>foo</tt>.
9
+ #
10
+ # In the second example, we find all applicable rules for a given
11
+ # <tt>Revision</tt> object. We can then run each of them.
12
+ #
13
+ # @example 1: Define a simple Rule
14
+ # Rule.new(/foo/) { puts 'fired' }
15
+ #
16
+ # @example 2: Finding and firing Rules
17
+ # rev = Revision.new
18
+ # Rule.matching(rev).map { |rule| rule.run(rev) }
19
+ #
20
+ # @since 0.3.0
21
+ # @author Arjan van der Gaag
22
+ class Rule
23
+
24
+ # Exception for when trying to apply a rule to something other than an
25
+ # instance of Revision.
26
+ CannotCompare = Class.new(Exception)
27
+
28
+ # A hash of Revision attributes and regular expressions to match against
29
+ attr_reader :criteria
30
+
31
+ # The callback to run on a match
32
+ attr_reader :block
33
+
34
+ private
35
+
36
+ @rules = []
37
+
38
+ # Keep track of Rule objects that are created in a class instance variable
39
+ #
40
+ # @param [Rule] child is the new Rule object
41
+ # @return [Array<Rule>] the total list of children
42
+ def self.register(child)
43
+ @rules << child
44
+ end
45
+
46
+ public
47
+
48
+ # Return an array of all rules currently defined.
49
+ #
50
+ # @return [Array<Rule>]
51
+ def self.rules
52
+ @rules
53
+ end
54
+
55
+ # Reset the list of known rules, deleting all currently known rules.
56
+ #
57
+ # @return nil
58
+ def self.reset
59
+ @rules = []
60
+ end
61
+
62
+ # Return an array of all existing Rule objects that match the given
63
+ # revision.
64
+ #
65
+ # @param [Revision] revision is the revision to compare rules to.
66
+ # @return [Array<Rule>] list of all matching rules
67
+ def self.matching(revision)
68
+ @rules.select { |child| child === revision }
69
+ end
70
+
71
+ # Create a new Rule object with criteria for different properties of a
72
+ # Revision. The required block defines the callback to run. It will have
73
+ # the current Revision object yielded to it.
74
+ #
75
+ # Criteria are Ruby objects that should match (`===`) a Revision's
76
+ # attributes. These would usually be regular expressions, but they
77
+ # can be strings or custom objects if you want to.
78
+ #
79
+ # @overload initialize(pattern, &block)
80
+ # Define a rule with a pattern matching the log message
81
+ # @param [Regex] pattern is the regular expression to match against
82
+ # the revision's log message
83
+ # @overload initialize(options, &block)
84
+ # Define a rule with various criteria in a hash.
85
+ # @param [Hash] options defines matching criteria.
86
+ # @option options :author Criterium for Revision#author
87
+ # @option options :date Criterium for Revision#date
88
+ # @option options :number Criterium for Revision#number
89
+ # @option options :project Criterium for Revision#project
90
+ def initialize(pattern_or_options, &block)
91
+ raise ArgumentError, 'a Rule requires a block' unless block_given?
92
+
93
+ # If not given a hash, we build a hash defaulting on message
94
+ unless pattern_or_options.is_a?(Hash)
95
+ pattern_or_options = { :message => pattern_or_options }
96
+ end
97
+
98
+ @criteria, @block = pattern_or_options, block
99
+ @criteria.inspect
100
+ self.class.register self
101
+ end
102
+
103
+ # Call this Rule's callback method with the give Revision object.
104
+ # @return [nil]
105
+ def run(rev)
106
+ @rev = rev
107
+ block.call(@rev, collect_captures)
108
+ end
109
+
110
+ # Use {Rule#matches?} to see if this <tt>Rule</tt> matches the given
111
+ # <tt>Revision</tt>.
112
+ #
113
+ # @param [Object] the object to compare to
114
+ # @return [Boolean]
115
+ # @see Rule#matches?
116
+ def ===(other)
117
+ matches?(other)
118
+ rescue CannotCompare
119
+ super
120
+ end
121
+
122
+ # See if the current rule matches a given subversion revision.
123
+ #
124
+ # @param [Revision] revision the Revision object to compare to.
125
+ # @return [Boolean]
126
+ # @see Rule#===
127
+ # @raise Subtrigger::Rule::CannotCompare when comparing to something other
128
+ # than a revision.
129
+ def matches?(revision)
130
+ raise CannotCompare unless @criteria.keys.all? { |k| k == :all || revision.respond_to?(k) }
131
+ match = @criteria.any?
132
+ @criteria.each_pair do |key, value|
133
+ if key == :all
134
+ match = (value === revision)
135
+ else
136
+ match &= (value === revision.send(key.to_sym))
137
+ end
138
+ end
139
+ match
140
+ end
141
+
142
+ private
143
+
144
+ # When using regular expressions to match against string values, we
145
+ # want to be able to get to any captured groups. This method scans all
146
+ # string values with their Regex matchers and collects all captured
147
+ # groups into a namespaced hash.
148
+ #
149
+ # @example
150
+ # Rule.new /hello, (.+)!/ do |revision, matches|
151
+ # puts matches.inspect
152
+ # end
153
+ # # => { :message => ['world'] }
154
+ #
155
+ # @return [Hash] all captured groups per Revision attribute tested
156
+ # @todo this only passes on capture groups, not the entire match ($&)
157
+ def collect_captures
158
+ criteria.inject({}) do |output, (key, value)|
159
+ next if key == :all
160
+ output[key] = @rev.send(key.to_sym).scan(value).flatten if value.is_a?(Regexp)
161
+ output
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,96 @@
1
+ module Subtrigger
2
+ # Reads, parses and manages inline templates.
3
+ #
4
+ # When you define string templates at the end of your rules file, this class
5
+ # can parse and keep track of them. You can then easily retrieve them again
6
+ # and optionally format it like with <tt>String#%</tt>.
7
+ #
8
+ # You simply define a new template using <tt>@@</tt>, followed by a name and
9
+ # then the textual contents (see the example below).
10
+ #
11
+ # You can read the templates and use them, for example, in e-mails.
12
+ #
13
+ # @example Defining templates
14
+ # # at the end of your Ruby file:
15
+ # __END__
16
+ # @@ Template 1
17
+ # Foo
18
+ # @@ Template 2
19
+ # Hello, %s!
20
+ #
21
+ # @example Parsing templates
22
+ # Template.parse(__DATA__.read)
23
+ #
24
+ # @example Using templates
25
+ # Template.find('Template 1') # => 'Foo'
26
+ #
27
+ # @example Formatting templates
28
+ # Template.find('Template 2') # => 'Hello, %s!'
29
+ # Template.find('Template 2').format('world') # => 'Hello, world!'
30
+ #
31
+ # @author Arjan van der Gaag
32
+ # @since 0.3.0
33
+ class Template
34
+ # The unique identifier for this template
35
+ attr_reader :name
36
+
37
+ # The actual contents of the template
38
+ attr_reader :string
39
+
40
+ # List of defined templates the class tracks
41
+ @children = []
42
+
43
+ # The pattern that separates one template from another
44
+ TEMPLATE_DELIMITER = /^@@ (.*)\n/
45
+
46
+ # Exception raised when a string cannot be parsed into templates,
47
+ # because the delimiter cannot be found
48
+ Unparseable = Class.new(Exception)
49
+
50
+ # Parse the contents of a string and extract templates from it. These are
51
+ # tracked so you can use {Template#find} to retrieve them by name.
52
+ #
53
+ # @param [String] the contents of your rules file's <tt>__DATA__.read</tt>
54
+ # @return [nil]
55
+ def self.parse(string)
56
+ raise Unparseable, "Could not split into templates: #{string.inspect}" unless string =~ TEMPLATE_DELIMITER
57
+ string.split(TEMPLATE_DELIMITER).map(&:chomp).slice(1..-1).each_slice(2) do |name, content|
58
+ @children << new(name, content)
59
+ end
60
+ nil
61
+ end
62
+
63
+ # Finds and returns the content of the template by the given name.
64
+ #
65
+ # @param [String] name is the name of the template
66
+ # @return [String] is Template#content
67
+ def self.find(name)
68
+ @children.find { |child|
69
+ child.name == name
70
+ }
71
+ end
72
+
73
+ # Convert to string using the textual contents of the template
74
+ # @return [String]
75
+ def to_s
76
+ string
77
+ end
78
+
79
+ # Get the contents of the template and interpolate any given
80
+ # arguments into it.
81
+ #
82
+ # @example Getting a template and using interpolation
83
+ # template.to_s # => 'Dear %s...'
84
+ # template.format 'John' # => 'Dear John...'
85
+ # @return [String] the formatted template contents
86
+ def format(*args)
87
+ to_s % [*args]
88
+ end
89
+
90
+ # @param [String] name is the unique identifier of a template
91
+ # @param [String] string is the contents of the template.
92
+ def initialize(name, string)
93
+ @name, @string = name, string
94
+ end
95
+ end
96
+ end
data/subtrigger.gemspec CHANGED
@@ -5,65 +5,65 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{subtrigger}
8
- s.version = "0.2.7"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Arjan van der Gaag"]
12
- s.date = %q{2010-04-23}
13
- s.default_executable = %q{subtrigger}
12
+ s.date = %q{2010-07-16}
14
13
  s.description = %q{This gem allows you to create simple Ruby triggers for Subversion commit messages, responding to keywords in your log messages to send e-mails, deploy sites or do whatever you need.}
15
14
  s.email = %q{arjan@arjanvandergaag.nl}
16
- s.executables = ["subtrigger"]
17
15
  s.extra_rdoc_files = [
18
- "LICENSE",
19
- "README.rdoc"
16
+ "README.md"
20
17
  ]
21
18
  s.files = [
22
19
  ".document",
23
20
  ".gitignore",
24
- "LICENSE",
25
- "README.rdoc",
21
+ "README.md",
26
22
  "Rakefile",
27
23
  "VERSION",
28
- "bin/subtrigger",
29
24
  "lib/subtrigger.rb",
30
- "lib/subtrigger/email.rb",
31
- "lib/subtrigger/repository.rb",
32
- "lib/subtrigger/trigger.rb",
25
+ "lib/subtrigger/dsl.rb",
26
+ "lib/subtrigger/path.rb",
27
+ "lib/subtrigger/revision.rb",
28
+ "lib/subtrigger/rule.rb",
29
+ "lib/subtrigger/template.rb",
33
30
  "subtrigger.gemspec",
34
- "test/helper.rb",
35
- "test/test_email.rb",
36
- "test/test_repository.rb",
37
- "test/test_subtrigger.rb",
38
- "test/test_trigger.rb"
31
+ "test/test_helper.rb",
32
+ "test/test_path.rb",
33
+ "test/test_revision.rb",
34
+ "test/test_rule.rb",
35
+ "test/test_template.rb"
39
36
  ]
40
37
  s.homepage = %q{http://github.com/avdgaag/subtrigger}
41
38
  s.rdoc_options = ["--charset=UTF-8"]
42
39
  s.require_paths = ["lib"]
43
- s.rubygems_version = %q{1.3.6}
40
+ s.rubygems_version = %q{1.3.7}
44
41
  s.summary = %q{Create post-commit triggers for Subversion commit messages}
45
42
  s.test_files = [
46
- "test/helper.rb",
47
- "test/test_email.rb",
48
- "test/test_repository.rb",
49
- "test/test_subtrigger.rb",
50
- "test/test_trigger.rb"
43
+ "test/test_helper.rb",
44
+ "test/test_path.rb",
45
+ "test/test_revision.rb",
46
+ "test/test_rule.rb",
47
+ "test/test_template.rb"
51
48
  ]
52
49
 
53
50
  if s.respond_to? :specification_version then
54
51
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
55
52
  s.specification_version = 3
56
53
 
57
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
58
55
  s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
59
56
  s.add_development_dependency(%q<mocha>, [">= 0"])
57
+ s.add_runtime_dependency(%q<pony>, [">= 0"])
60
58
  else
61
59
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
62
60
  s.add_dependency(%q<mocha>, [">= 0"])
61
+ s.add_dependency(%q<pony>, [">= 0"])
63
62
  end
64
63
  else
65
64
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
66
65
  s.add_dependency(%q<mocha>, [">= 0"])
66
+ s.add_dependency(%q<pony>, [">= 0"])
67
67
  end
68
68
  end
69
69