subtrigger 0.2.7 → 0.3.0

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