acts_as_ferret 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,8 @@
1
- module FerretMixin
2
- module Acts #:nodoc:
3
- module ARFerret #:nodoc:
1
+ module ActsAsFerret #:nodoc:
4
2
 
5
- module MoreLikeThis
3
+ module MoreLikeThis
6
4
 
7
- class DefaultAAFSimilarity
8
- def idf(doc_freq, num_docs)
9
- return 0.0 if num_docs == 0
10
- return Math.log(num_docs.to_f/(doc_freq+1)) + 1.0
11
- end
12
- end
5
+ module InstanceMethods
13
6
 
14
7
  # returns other instances of this class, which have similar contents
15
8
  # like this one. Basically works like this: find out n most interesting
@@ -25,18 +18,16 @@ module FerretMixin
25
18
  # :field_names : Array of field names to use for similarity search (mandatory)
26
19
  # :min_term_freq => 2, # Ignore terms with less than this frequency in the source doc.
27
20
  # :min_doc_freq => 5, # Ignore words which do not occur in at least this many docs
28
- # :min_word_length => nil, # Ignore words if less than this len (longer
29
- # words tend to be more characteristic for the document they occur in).
21
+ # :min_word_length => nil, # Ignore words shorter than this length (longer words tend to
22
+ # be more characteristic for the document they occur in).
30
23
  # :max_word_length => nil, # Ignore words if greater than this len.
31
24
  # :max_query_terms => 25, # maximum number of terms in the query built
32
- # :max_num_tokens => 5000, # maximum number of tokens to examine in a
33
- # single field
34
- # :boost => false, # when true, a boost according to the
35
- # relative score of a term is applied to this Term's TermQuery.
36
- # :similarity => Ferret::Search::Similarity.default, # the similarity
37
- # implementation to use
38
- # :analyzer => Ferret::Analysis::StandardAnalyzer.new # the analyzer to
39
- # use
25
+ # :max_num_tokens => 5000, # maximum number of tokens to examine in a single field
26
+ # :boost => false, # when true, a boost according to the relative score of
27
+ # a term is applied to this Term's TermQuery.
28
+ # :similarity => 'DefaultAAFSimilarity' # the similarity implementation to use (the default
29
+ # equals Ferret's internal similarity implementation)
30
+ # :analyzer => 'Ferret::Analysis::StandardAnalyzer' # class name of the analyzer to use
40
31
  # :append_to_query => nil # proc taking a query object as argument, which will be called after generating the query. can be used to further manipulate the query used to find related documents, i.e. to constrain the search to a given class in single table inheritance scenarios
41
32
  # find_options : options handed over to find_by_contents
42
33
  def more_like_this(options = {}, find_options = {})
@@ -49,30 +40,39 @@ module FerretMixin
49
40
  :max_query_terms => 25, # maximum number of terms in the query built
50
41
  :max_num_tokens => 5000, # maximum number of tokens to analyze when analyzing contents
51
42
  :boost => false,
52
- :similarity => DefaultAAFSimilarity.new,
53
- :analyzer => Ferret::Analysis::StandardAnalyzer.new,
43
+ :similarity => 'ActsAsFerret::MoreLikeThis::DefaultAAFSimilarity', # class name of the similarity implementation to use
44
+ :analyzer => 'Ferret::Analysis::StandardAnalyzer', # class name of the analyzer to use
54
45
  :append_to_query => nil,
55
46
  :base_class => self.class # base class to use for querying, useful in STI scenarios where BaseClass.find_by_contents can be used to retrieve results from other classes, too
56
47
  }.update(options)
57
- index = self.class.ferret_index
58
48
  #index.search_each('id:*') do |doc, score|
59
49
  # puts "#{doc} == #{index[doc][:description]}"
60
50
  #end
61
- index.synchronize do # avoid that concurrent writes close our reader
62
- index.send(:ensure_reader_open)
63
- reader = index.send(:reader)
64
- doc_number = self.document_number
65
- term_freq_map = retrieve_terms(document_number, reader, options)
51
+ clazz = options[:base_class]
52
+ options[:base_class] = clazz.name
53
+ query = clazz.aaf_index.build_more_like_this_query(self.id, self.class.name, options)
54
+ options[:append_to_query].call(query) if options[:append_to_query]
55
+ clazz.find_by_contents(query, find_options)
56
+ end
57
+
58
+ end
59
+
60
+ module IndexMethods
61
+
62
+ def build_more_like_this_query(id, class_name, options)
63
+ [:similarity, :analyzer].each { |sym| options[sym] = options[sym].constantize.new }
64
+ ferret_index.synchronize do # avoid that concurrent writes close our reader
65
+ ferret_index.send(:ensure_reader_open)
66
+ reader = ferret_index.send(:reader)
67
+ term_freq_map = retrieve_terms(id, class_name, reader, options)
66
68
  priority_queue = create_queue(term_freq_map, reader, options)
67
- query = create_query(priority_queue, options)
68
- logger.debug "morelikethis-query: #{query}"
69
- options[:append_to_query].call(query) if options[:append_to_query]
70
- options[:base_class].find_by_contents(query, find_options)
69
+ create_query(id, class_name, priority_queue, options)
71
70
  end
72
71
  end
73
72
 
73
+ protected
74
74
 
75
- def create_query(priority_queue, options={})
75
+ def create_query(id, class_name, priority_queue, options={})
76
76
  query = Ferret::Search::BooleanQuery.new
77
77
  qterms = 0
78
78
  best_score = nil
@@ -93,8 +93,8 @@ module FerretMixin
93
93
  qterms += 1
94
94
  break if options[:max_query_terms] > 0 && qterms >= options[:max_query_terms]
95
95
  end
96
- # exclude ourselves
97
- query.add_query(Ferret::Search::TermQuery.new(:id, self.id.to_s), :must_not)
96
+ # exclude the original record
97
+ query.add_query(query_for_record(id, class_name), :must_not)
98
98
  return query
99
99
  end
100
100
 
@@ -102,11 +102,13 @@ module FerretMixin
102
102
 
103
103
  # creates a term/term_frequency map for terms from the fields
104
104
  # given in options[:field_names]
105
- def retrieve_terms(doc_number, reader, options)
105
+ def retrieve_terms(id, class_name, reader, options)
106
+ document_number = document_number(id, class_name)
106
107
  field_names = options[:field_names]
107
108
  max_num_tokens = options[:max_num_tokens]
108
109
  term_freq_map = Hash.new(0)
109
110
  doc = nil
111
+ record = nil
110
112
  field_names.each do |field|
111
113
  #puts "field: #{field}"
112
114
  term_freq_vector = reader.term_vector(document_number, field)
@@ -125,7 +127,8 @@ module FerretMixin
125
127
  content = doc[field]
126
128
  unless content
127
129
  # no term vector, no stored content, so try content from this instance
128
- content = content_for_field_name(field.to_s)
130
+ record ||= options[:base_class].constantize.find(id)
131
+ content = record.content_for_field_name(field.to_s)
129
132
  end
130
133
  puts "have doc: #{doc[:id]} with #{field} == #{content}"
131
134
  token_count = 0
@@ -184,12 +187,16 @@ module FerretMixin
184
187
  )
185
188
  end
186
189
 
187
- def content_for_field_name(field)
188
- self[field] || self.instance_variable_get("@#{field.to_s}".to_sym) || self.send(field.to_sym)
189
- end
190
+ end
190
191
 
192
+ class DefaultAAFSimilarity
193
+ def idf(doc_freq, num_docs)
194
+ return 0.0 if num_docs == 0
195
+ return Math.log(num_docs.to_f/(doc_freq+1)) + 1.0
196
+ end
191
197
  end
192
198
 
199
+
193
200
  class FrequencyQueueItem
194
201
  attr_reader :word, :field, :score
195
202
  def initialize(word, field, score)
@@ -198,6 +205,5 @@ module FerretMixin
198
205
  end
199
206
 
200
207
  end
201
- end
202
208
  end
203
209
 
data/lib/multi_index.rb CHANGED
@@ -1,15 +1,14 @@
1
- module FerretMixin
2
- module Acts #:nodoc:
3
- module ARFerret #:nodoc:
4
- # not threadsafe
1
+ module ActsAsFerret #:nodoc:
2
+
3
+ # this class is not threadsafe
5
4
  class MultiIndex
6
5
 
7
- # todo: check for necessary index rebuilds in this place, too
8
- # idea - each class gets a create_reader method that does this
9
6
  def initialize(model_classes, options = {})
10
7
  @model_classes = model_classes
8
+ # ensure all models indexes exist
9
+ @model_classes.each { |m| m.aaf_index.ensure_index_exists }
11
10
  default_fields = @model_classes.inject([]) do |fields, c|
12
- fields + c.ferret_configuration[:default_field]
11
+ fields + c.aaf_configuration[:ferret][:default_field]
13
12
  end
14
13
  @options = {
15
14
  :default_field => default_fields
@@ -68,9 +67,9 @@ module FerretMixin
68
67
  unless latest?
69
68
  @sub_readers = @model_classes.map { |clazz|
70
69
  begin
71
- reader = Ferret::Index::IndexReader.new(clazz.class_index_dir)
70
+ reader = Ferret::Index::IndexReader.new(clazz.aaf_configuration[:index_dir])
72
71
  rescue Exception
73
- puts "error opening #{clazz.class_index_dir}: #{$!}"
72
+ puts "error opening #{clazz.aaf_configuration[:index_dir]}: #{$!}"
74
73
  end
75
74
  reader
76
75
  }
@@ -82,6 +81,4 @@ module FerretMixin
82
81
 
83
82
  end # of class MultiIndex
84
83
 
85
- end
86
- end
87
84
  end
@@ -0,0 +1,50 @@
1
+ require 'drb'
2
+ module ActsAsFerret
3
+
4
+ # This index implementation connects to a remote ferret server instance. It
5
+ # basically forwards all calls to the remote server.
6
+ class RemoteIndex < AbstractIndex
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @ferret_config = config[:ferret]
11
+ @server = DRbObject.new(nil, config[:remote])
12
+ end
13
+
14
+ def method_missing(method_name, *args)
15
+ args.unshift model_class_name
16
+ @server.send(method_name, *args)
17
+ end
18
+
19
+ def find_id_by_contents(q, options = {}, &proc)
20
+ total_hits, results = @server.find_id_by_contents(model_class_name, q, options)
21
+ block_given? ? yield_results(total_hits, results, &proc) : [ total_hits, results ]
22
+ end
23
+
24
+ def id_multi_search(query, models, options, &proc)
25
+ total_hits, results = @server.id_multi_search(model_class_name, query, models, options)
26
+ block_given? ? yield_results(total_hits, results, &proc) : [ total_hits, results ]
27
+ end
28
+
29
+ # add record to index
30
+ def add(record)
31
+ @server.add record.class.name, record.to_doc
32
+ end
33
+ alias << add
34
+
35
+ private
36
+
37
+ def yield_results(total_hits, results)
38
+ results.each do |result|
39
+ yield result[:model], result[:id], result[:score], result[:data]
40
+ end
41
+ total_hits
42
+ end
43
+
44
+ def model_class_name
45
+ @config[:class_name]
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,14 @@
1
+ module ActsAsFerret
2
+
3
+ class SharedIndex < LocalIndex
4
+
5
+ # build a ferret query matching only the record with the given id and class
6
+ def query_for_record(id, class_name)
7
+ returning bq = Ferret::Search::BooleanQuery.new do
8
+ bq.add_query(Ferret::Search::TermQuery.new(:id, id.to_s), :must)
9
+ bq.add_query(Ferret::Search::TermQuery.new(:class_name, class_name), :must)
10
+ end
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,90 @@
1
+ module ActsAsFerret
2
+
3
+ # class methods for classes using acts_as_ferret :single_index => true
4
+ module SharedIndexClassMethods
5
+
6
+ def find_id_by_contents(q, options = {}, &block)
7
+ # add class name scoping to query if necessary
8
+ unless options[:models] == :all # search needs to be restricted by one or more class names
9
+ options[:models] ||= []
10
+ # add this class to the list of given models
11
+ options[:models] << self unless options[:models].include?(self)
12
+ # keep original query
13
+ original_query = q
14
+
15
+ if original_query.is_a? String
16
+ model_query = options[:models].map(&:name).join '|'
17
+ q << %{ +class_name:"#{model_query}"}
18
+ else
19
+ q = Ferret::Search::BooleanQuery.new
20
+ q.add_query(original_query, :must)
21
+ model_query = Ferret::Search::BooleanQuery.new
22
+ options[:models].each do |model|
23
+ model_query.add_query(Ferret::Search::TermQuery.new(:class_name, model.name), :should)
24
+ end
25
+ q.add_query(model_query, :must)
26
+ end
27
+ end
28
+ options.delete :models
29
+
30
+ super(q, options, &block)
31
+ end
32
+
33
+ # Overrides the standard find_by_contents for searching a shared index.
34
+ #
35
+ # please note that records from different models will be fetched in
36
+ # separate sql calls, so any sql order_by clause given with
37
+ # find_options[:order] will be ignored.
38
+ def find_by_contents(q, options = {}, find_options = {})
39
+ if order = find_options.delete(:order)
40
+ logger.warn "using a shared index, so ignoring order_by clause #{order}"
41
+ end
42
+ total_hits, result = find_records_lazy_or_not q, options, find_options
43
+ # sort so results have the same order they had when originally retrieved
44
+ # from ferret
45
+ return SearchResults.new(result, total_hits)
46
+ end
47
+
48
+ protected
49
+
50
+ def ar_find_by_contents(q, options = {}, find_options = {})
51
+ total_hits, id_arrays = collect_results(q, options)
52
+ result = retrieve_records(id_arrays, find_options)
53
+ result.sort! { |a, b| id_arrays[a.class.name][a.id.to_s].first <=> id_arrays[b.class.name][b.id.to_s].first }
54
+ [ total_hits, result ]
55
+ end
56
+
57
+ def collect_results(q, options = {})
58
+ id_arrays = {}
59
+ # get object ids for index hits
60
+ rank = 0
61
+ total_hits = find_id_by_contents(q, options) do |model, id, score, data|
62
+ id_arrays[model] ||= {}
63
+ # store result rank and score
64
+ id_arrays[model][id] = [ rank += 1, score ]
65
+ end
66
+ [ total_hits, id_arrays ]
67
+ end
68
+
69
+
70
+ # determine all field names in the shared index
71
+ # TODO unused
72
+ # def single_index_field_names(models)
73
+ # @single_index_field_names ||= (
74
+ # searcher = Ferret::Search::Searcher.new(class_index_dir)
75
+ # if searcher.reader.respond_to?(:get_field_names)
76
+ # (searcher.reader.send(:get_field_names) - ['id', 'class_name']).to_a
77
+ # else
78
+ # puts <<-END
79
+ #unable to retrieve field names for class #{self.name}, please
80
+ #consider naming all indexed fields in your call to acts_as_ferret!
81
+ # END
82
+ # models.map { |m| m.content_columns.map { |col| col.name } }.flatten
83
+ # end
84
+ # )
85
+ #
86
+ # end
87
+
88
+ end
89
+ end
90
+
data/rakefile CHANGED
@@ -1,7 +1,8 @@
1
- # rakefile for acts_as_ferret.
1
+ # rakefile for acts_as_ferret.
2
2
  # use to create a gem or generate rdoc api documentation.
3
3
  #
4
- # heavily based on the one from the acts_as_searchable plugin.
4
+ # RELEASE creation:
5
+ # rake release REL=x.y.z
5
6
 
6
7
  require 'rake'
7
8
  require 'rake/rdoctask'
@@ -10,16 +11,21 @@ require 'rake/gempackagetask'
10
11
  require 'rake/testtask'
11
12
  require 'rake/contrib/rubyforgepublisher'
12
13
 
14
+ def announce(msg='')
15
+ STDERR.puts msg
16
+ end
17
+
18
+
13
19
  PKG_NAME = 'acts_as_ferret'
14
- PKG_VERSION = '0.3.1'
20
+ PKG_VERSION = ENV['REL']
15
21
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
16
- RUBY_FORGE_PROJECT = 'actsasferret'
17
- RUBY_FORGE_USER = 'jkraemer'
22
+ RUBYFORGE_PROJECT = 'actsasferret'
23
+ RUBYFORGE_USER = 'jkraemer'
18
24
 
19
25
  desc 'Default: run unit tests.'
20
26
  task :default => :test
21
27
 
22
- desc 'Test the acts_as_searchable plugin.'
28
+ desc 'Test the acts_as_ferret plugin.'
23
29
  Rake::TestTask.new(:test) do |t|
24
30
  t.libs << 'lib'
25
31
  t.pattern = 'test/**/*_test.rb'
@@ -37,155 +43,90 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
37
43
  rdoc.rdoc_files.include('lib/**/*.rb')
38
44
  end
39
45
 
40
- spec = Gem::Specification.new do |s|
41
- s.name = PKG_NAME
42
- s.version = PKG_VERSION
43
- s.platform = Gem::Platform::RUBY
44
- s.summary = "acts_as_ferret - Ferret based full text search for any ActiveRecord model"
45
- s.files = Dir.glob('**/*', File::FNM_DOTMATCH).reject do |f|
46
- [ /\.$/, /sqlite$/, /\.log$/, /^pkg/, /\.svn/,
47
- /\~$/, /\/\._/, /\/#/ ].any? {|regex| f =~ regex }
48
- end
49
- #s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG)
50
- # s.files.delete ...
51
- s.require_path = 'lib'
52
- s.autorequire = 'acts_as_ferret'
53
- s.has_rdoc = true
54
- # s.test_files = Dir['test/**/*_test.rb']
55
- s.author = "Jens Kraemer"
56
- s.email = "jk@jkraemer.net"
57
- s.homepage = "http://projects.jkraemer.net/acts_as_ferret"
58
- end
59
-
60
- Rake::GemPackageTask.new(spec) do |pkg|
61
- pkg.need_tar = true
62
- end
63
-
64
46
  desc "Publish the API documentation"
65
47
  task :pdoc => [:rdoc] do
66
- Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
48
+ Rake::RubyForgePublisher.new(RUBYFORGE_PROJECT, RUBYFORGE_USER).upload
67
49
  end
68
50
 
69
- desc 'Publish the gem and API docs'
70
- task :publish => [:pdoc, :rubyforge_upload]
71
-
72
- desc "Publish the release files to RubyForge."
73
- task :rubyforge_upload => :package do
74
- files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
75
-
76
- if RUBY_FORGE_PROJECT then
77
- require 'net/http'
78
- require 'open-uri'
79
-
80
- project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
81
- project_data = open(project_uri) { |data| data.read }
82
- group_id = project_data[/[?&]group_id=(\d+)/, 1]
83
- raise "Couldn't get group id" unless group_id
84
-
85
- # This echos password to shell which is a bit sucky
86
- if ENV["RUBY_FORGE_PASSWORD"]
87
- password = ENV["RUBY_FORGE_PASSWORD"]
88
- else
89
- print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
90
- password = STDIN.gets.chomp
51
+ if PKG_VERSION
52
+ spec = Gem::Specification.new do |s|
53
+ s.name = PKG_NAME
54
+ s.version = PKG_VERSION
55
+ s.platform = Gem::Platform::RUBY
56
+ s.summary = "acts_as_ferret - Ferret based full text search for any ActiveRecord model"
57
+ s.files = Dir.glob('**/*', File::FNM_DOTMATCH).reject do |f|
58
+ [ /\.$/, /sqlite$/, /\.log$/, /^pkg/, /\.svn/, /\.swp$/, /^html/, /\~$/, /\/\._/, /\/#/ ].any? {|regex| f =~ regex }
91
59
  end
60
+ #s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG)
61
+ # s.files.delete ...
62
+ s.require_path = 'lib'
63
+ s.autorequire = 'acts_as_ferret'
64
+ s.has_rdoc = true
65
+ # s.test_files = Dir['test/**/*_test.rb']
66
+ s.author = "Jens Kraemer"
67
+ s.email = "jk@jkraemer.net"
68
+ s.homepage = "http://projects.jkraemer.net/acts_as_ferret"
69
+ end
92
70
 
93
- login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
94
- data = [
95
- "login=Login",
96
- "form_loginname=#{RUBY_FORGE_USER}",
97
- "form_pw=#{password}"
98
- ].join("&")
99
-
100
- headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
101
-
102
- http.post("/account/login.php", data, headers)
103
- end
104
-
105
- cookie = login_response["set-cookie"]
106
- raise "Login failed" unless cookie
107
- headers = { "Cookie" => cookie }
108
-
109
- release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
110
- release_data = open(release_uri, headers) { |data| data.read }
111
- package_id = release_data[/[?&]package_id=(\d+)/, 1]
112
- raise "Couldn't get package id" unless package_id
113
-
114
- first_file = true
115
- release_id = ""
116
-
117
- files.each do |filename|
118
- basename = File.basename(filename)
119
- file_ext = File.extname(filename)
120
- file_data = File.open(filename, "rb") { |file| file.read }
121
-
122
- puts "Releasing #{basename}..."
123
-
124
- release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
125
- release_date = Time.now.strftime("%Y-%m-%d %H:%M")
126
- type_map = {
127
- ".zip" => "3000",
128
- ".tgz" => "3110",
129
- ".gz" => "3110",
130
- ".gem" => "1400"
131
- }; type_map.default = "9999"
132
- type = type_map[file_ext]
133
- boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
134
-
135
- query_hash = if first_file then
136
- {
137
- "group_id" => group_id,
138
- "package_id" => package_id,
139
- "release_name" => PKG_FILE_NAME,
140
- "release_date" => release_date,
141
- "type_id" => type,
142
- "processor_id" => "8000", # Any
143
- "release_notes" => "",
144
- "release_changes" => "",
145
- "preformatted" => "1",
146
- "submit" => "1"
147
- }
148
- else
149
- {
150
- "group_id" => group_id,
151
- "release_id" => release_id,
152
- "package_id" => package_id,
153
- "step2" => "1",
154
- "type_id" => type,
155
- "processor_id" => "8000", # Any
156
- "submit" => "Add This File"
157
- }
158
- end
159
-
160
- data = [
161
- "--" + boundary,
162
- "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
163
- "Content-Type: application/octet-stream",
164
- "Content-Transfer-Encoding: binary",
165
- "", file_data, "",
166
- query_hash.collect do |name, value|
167
- [ "--" + boundary,
168
- "Content-Disposition: form-data; name='#{name}'",
169
- "", value, "" ]
170
- end
171
- ].flatten.join("\x0D\x0A")
172
-
173
- release_headers = headers.merge(
174
- "Content-Type" => "multipart/form-data; boundary=#{boundary}"
175
- )
176
-
177
- target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
178
- http.post(target, data, release_headers)
179
- end
71
+ package_task = Rake::GemPackageTask.new(spec) do |pkg|
72
+ pkg.need_tar = true
73
+ end
180
74
 
181
- if first_file then
182
- release_id = release_response.body[/release_id=(\d+)/, 1]
183
- raise("Couldn't get release id") unless release_id
75
+ # Validate that everything is ready to go for a release.
76
+ task :prerelease do
77
+ announce
78
+ announce "**************************************************************"
79
+ announce "* Making RubyGem Release #{PKG_VERSION}"
80
+ announce "**************************************************************"
81
+ announce
82
+ # Are all source files checked in?
83
+ if ENV['RELTEST']
84
+ announce "Release Task Testing, skipping checked-in file test"
85
+ else
86
+ announce "Pulling in svn..."
87
+ `svk pull .`
88
+ announce "Checking for unchecked-in files..."
89
+ data = `svk st`
90
+ unless data =~ /^$/
91
+ fail "SVK status is not clean ... do you have unchecked-in files?"
184
92
  end
185
-
186
- first_file = false
93
+ announce "No outstanding checkins found ... OK"
94
+ announce "Pushing to svn..."
95
+ `svk push .`
187
96
  end
188
97
  end
98
+
99
+
100
+ desc "tag the new release"
101
+ task :tag => [ :prerelease ] do
102
+ reltag = "REL_#{PKG_VERSION.gsub(/\./, '_')}"
103
+ reltag << ENV['REUSE'].gsub(/\./, '_') if ENV['REUSE']
104
+ announce "Tagging with [#{PKG_VERSION}]"
105
+ if ENV['RELTEST']
106
+ announce "Release Task Testing, skipping tagging"
107
+ else
108
+ `svn copy -m 'tagging version #{PKG_VERSION}' svn://projects.jkraemer.net/acts_as_ferret/trunk/plugin svn://projects.jkraemer.net/acts_as_ferret/tags/#{PKG_VERSION}`
109
+ `svn del -m 'remove old stable' svn://projects.jkraemer.net/acts_as_ferret/tags/stable`
110
+ `svn copy -m 'tagging version #{PKG_VERSION} as stable' svn://projects.jkraemer.net/acts_as_ferret/tags/#{PKG_VERSION} svn://projects.jkraemer.net/acts_as_ferret/tags/stable`
111
+ end
112
+ end
113
+
114
+ # Upload release to rubyforge
115
+ desc "Upload release to rubyforge"
116
+ #task :prel => [ :tag, :prerelease, :package ] do
117
+ task :prel => [ :package ] do
118
+ #task :prel do
119
+ `rubyforge login`
120
+ release_command = "rubyforge add_release #{RUBYFORGE_PROJECT} #{PKG_NAME} '#{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.gem"
121
+ puts release_command
122
+ system(release_command)
123
+ `rubyforge config #{RUBYFORGE_PROJECT}`
124
+ release_command = "rubyforge add_file #{RUBYFORGE_PROJECT} #{PKG_NAME} '#{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.tgz"
125
+ puts release_command
126
+ system(release_command)
127
+ end
128
+
129
+ desc 'Publish the gem and API docs'
130
+ task :release => [:pdoc, :prel ]
131
+
189
132
  end
190
-
191
-