public
Description: Subtlety: SVN => RSS, hAtom => Atom
Homepage: http://subtlety.errtheblog.com/
Clone URL: git://github.com/defunkt/subtlety.git
Search Repo:
moving to sinatra
defunkt (author)
Mon Jan 21 18:07:26 -0800 2008
commit  45ad27c7898df71f1e94f8331a2cdca23f5a5493
tree    b77d8af2588f9ff3f13941962c476e323491ec1b
parent  5f476de066a5398cc0d49006a5a45198cfd1df18
...
1
2
 
 
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
18
19
20
21
 
22
23
24
25
26
27
28
29
30
31
 
 
 
 
 
32
33
34
35
 
 
 
 
 
 
 
 
 
 
 
36
37
38
39
40
41
42
43
44
45
46
 
 
47
48
49
50
51
52
53
54
 
 
55
56
57
58
59
60
61
62
 
 
 
63
 
 
 
 
64
 
65
66
67
68
69
70
71
72
73
74
75
 
 
76
77
78
79
80
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
83
84
85
86
 
 
87
88
89
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
92
93
94
 
 
95
96
97
98
 
 
 
 
 
 
 
 
 
99
100
101
 
 
 
 
 
102
103
 
 
 
104
105
106
107
108
109
110
111
112
 
 
 
113
114
115
116
117
118
119
120
 
 
 
121
122
 
 
 
 
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
 
 
 
 
144
145
146
147
148
 
 
 
 
 
149
150
151
152
153
154
155
156
157
158
159
160
161
162
 
 
 
 
163
 
164
165
166
167
168
169
170
171
172
173
174
 
 
 
 
 
175
176
177
178
179
180
181
182
 
 
 
 
 
 
 
 
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
211
212
213
214
215
216
217
218
219
 
 
 
 
 
 
 
 
 
 
 
220
221
222
223
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
...
 
 
1
2
3
4
 
 
 
 
 
 
 
 
 
 
 
 
 
5
6
7
8
9
10
11
 
 
 
 
 
 
 
 
 
12
13
14
15
16
17
 
 
 
18
19
20
21
22
23
24
25
26
27
28
29
30
 
 
 
 
 
 
 
 
 
31
32
33
 
34
 
 
 
 
 
35
36
37
38
 
 
 
 
 
 
39
40
41
42
43
44
45
46
47
48
49
 
 
 
 
 
 
 
 
 
 
50
51
52
 
 
 
 
 
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
 
 
 
 
72
73
74
 
 
 
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
 
 
 
99
100
101
 
 
 
102
103
104
105
106
107
108
109
110
111
 
 
112
113
114
115
116
117
 
118
119
120
121
 
 
 
 
 
 
 
 
122
123
124
125
126
127
 
 
 
 
 
128
129
130
131
 
132
133
134
135
136
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
139
140
141
142
143
 
 
 
144
145
146
147
148
149
 
 
 
 
 
 
 
 
 
 
 
 
 
150
151
152
153
154
155
156
 
 
 
 
 
 
 
 
 
 
157
158
159
160
161
162
163
 
 
 
 
 
 
164
165
166
167
168
169
170
171
172
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
 
 
 
 
 
 
 
 
201
202
203
204
205
206
207
208
209
210
211
212
213
214
 
 
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
0
@@ -1,225 +1,242 @@
0
-# < subtlety : a remote subversion excursion'
0
-# > copyrite 2007 chris wanstrath
0
+# < subtlety 2 : a remote subversion (and hAtom) excursion'
0
+# > copyrite 2008 chris wanstrath
0
 # < chris[at]ozmm[dot]org
0
 # > MIT License
0
-%w(rubygems erb camping camping/db open3).each { |f| require f }
0
-
0
-$debug = $0 =~ /camping/
0
-
0
-# for defunkt. campistrano.
0
-# ruby subtle.rb --update yourname@yourserver.com
0
-if at = ARGV.index('--update')
0
- ssh = 'ssh ' << (ARGV[at+1] || 'chris@errtheblog.com')
0
- puts `#{ssh} 'cd sites/subtlety; svn up; rm feeds/*'`
0
- exec "#{ssh} 'sudo /etc/init.d/rv restart'"
0
-end
0
-
0
-Camping.goes :Subtle
0
+%w( rubygems erb timeout sinatra sequel open3 ).each { |f| require f }
0
 
0
 # hax.
0
 class String; def compact; gsub(/(\s{2,})/, ' ').gsub("\n", '') end end
0
 module Kernel; def `(string) Open3.popen3(*string.split(' '))[1].read end end
0
+class Integer; def minutes; self * 60 end; def ago; Time.now - self end end
0
 
0
-module Subtle::Models
0
- class GetReadyToParty < V 1.0
0
- def self.up
0
- create_table :subtle_repositories do |t|
0
- t.column :id, :integer
0
- t.column :url, :string
0
- t.column :created_at, :datetime
0
- end
0
- end
0
+def timeout(&block)
0
+ Timeout.timeout(5, &block)
0
+rescue Timeout::Error
0
+ nil
0
+end
0
 
0
- def self.down
0
- drop_table :repositories
0
- end
0
+##
0
+# DB stuff
0
+DB = Sequel('sqlite:/db/subtle.db')
0
+
0
+class Item < Sequel::Model
0
+ set_schema do
0
+ primary_key :id
0
+ string :url
0
+ boolean :atom, :default => false
0
+ timestamp :created_at
0
+ index [ :url, :atom ]
0
   end
0
 
0
- class Repository < Base
0
- def validate
0
- errors.add("wrong format") unless url =~ /^(svn|http):\/\/(\w|\.|\/|-)+$/
0
- errors.add("invalid repos") unless `/usr/bin/env svn info #{url}` =~ /Path:/
0
- end
0
-
0
- def key
0
- id.to_s(16)
0
- end
0
+ def key
0
+ pk.to_s(16)
0
   end
0
-end
0
 
0
-module Subtle::Controllers
0
- class Index < R '/'
0
- def get
0
- render :index
0
- end
0
+ def full_url
0
+ "http://subtlety.errtheblog.com/O_o/#{key}.xml"
0
   end
0
 
0
- class Save < R '/s'
0
- def post
0
- url = input.feed.gsub(/\/$/,'')
0
- @repository = Repository.find_or_create_by_url(url)
0
- @rss = R(Feed, @repository.key) if @repository.errors.blank?
0
- render :save
0
+ def self.find_or_create_by_url(url, atom = false)
0
+ if model = self[:url => url, :atom => atom]
0
+ return model
0
     end
0
+
0
+ return unless url =~ /^(svn|http):\/\/(\w|\.|\/|-)+$/ && timeout { `/usr/bin/env svn info #{url}` =~ /Path:/ }
0
+
0
+ create(:url => url, :created_at => Time.now, :atom => atom)
0
   end
0
+end
0
 
0
- class Feed < R '/O_o/(\w+).xml'
0
- def get(key)
0
- @headers['Content-Type'] = 'application/xml'
0
- if File.exists?(@file = "feeds/#{key.gsub(/\W/,'')}.xml") && File.mtime(@file) > 15.minutes.ago
0
- sendfile(@file)
0
- else
0
- @repository = Repository.find_by_id(key.to_i(16))
0
- render_feed
0
- end
0
- end
0
+Item.set_dataset DB[:items]
0
+Item.create_table unless Item.table_exists?
0
 
0
- def render_feed
0
- tmp_file = "/tmp/tmp-#{@repository.key}.xml"
0
- dir = File.expand_path(File.dirname(__FILE__))
0
- erb_file = "#{dir}/svnlog.erb"
0
- xslt_file = "#{dir}/svnlog-#{@repository.key}.xslt"
0
+##
0
+# actions / views
0
+get '/' do
0
+ index = %Q[
0
+ <p class="first">
0
+ Welcome. Here's what we do: we take a remote, public subversion repository (http:// or svn://) and give you an rss feed of
0
+ the changes. That's it. Have an <strong>svn:external</strong> or <strong><a href="http://piston.rubyforge.org/">pistonized</a></strong>
0
+ repository in your app you need to monitor? Look no further: just plug in the repository's location and start reveling in the
0
+ sweet, sweet changesets, rss-style.
0
+ </p>
0
+ <p>
0
+ The inaugural blog entry <a href="http://errtheblog.com/post/701">is here</a>.
0
+ </p>
0
+ <p>
0
+ One thing: please add the repository's root path. So, svn://errtheblog.com/svn/mofo, <strong>not</strong>
0
+ svn://errtheblog.com/svn/mofo/trunk. Thanks, and enjoy.
0
+ </p>
0
+ ] << Helpers.form
0
 
0
-
0
- File.open(xslt_file, 'w') do |file|
0
- file.puts ERB.new(File.read(erb_file)).result(binding)
0
- end
0
+ erb index
0
+end
0
 
0
- File.open(tmp_file, 'w') do |f|
0
- f.puts `/usr/bin/env svn log #{@repository.url.gsub(/ |\\|;/,'')} --limit 15 -v --xml`
0
- end
0
+post '/s' do
0
+ url = params[:feed].chomp('/')
0
+ @item = Item.find_or_create_by_url(url)
0
+ @rss = "/O_o/#{@item.key}" if @item
0
+
0
+ if @rss
0
+ text = <<-end_html
0
+ <p class="first">
0
+ Here it is, your very own RSS feed of the changes committed to <strong>#{@item.url}</strong>:
0
+ </p>
0
+ <h3>
0
+ <a href="#{@item.full_url}">#{@item.full_url}</a>
0
+ </h3>
0
+ &laquo; <a href="/">back home.</a>
0
+ end_html
0
+ else
0
+ text = <<-end_html
0
+ <p class="highlight">
0
+ Sorry, there was some kind of error. Are you sure your repository url's valid? Does it start with svn:// or http://?
0
+ </p>
0
+ #{Helpers.form}
0
+ end_html
0
+ end
0
 
0
- File.open(@file, 'w') do |f|
0
- f.puts `/usr/bin/env xsltproc #{xslt_file} #{tmp_file}`
0
- end
0
+ erb text
0
+end
0
 
0
- `rm #{tmp_file} #{xslt_file}`
0
- File.read(@file)
0
- end
0
+get '/O_o/:key.xml' do
0
+ key = params[:key]
0
+ @headers['Content-Type'] = 'application/xml'
0
+ if File.exists?(@file = "feeds/#{key.gsub(/\W/,'')}.xml") && File.mtime(@file) > 15.minutes.ago
0
+ sendfile @file
0
+ else
0
+ render_feed Item[:id => key.to_i(16)]
0
+ end
0
+end
0
 
0
- def sendfile(file)
0
- return if file.include? '..'
0
+def render_feed(item)
0
+ tmp_file = "/tmp/tmp-#{item.key}.xml"
0
+ dir = File.expand_path(File.dirname(__FILE__))
0
+ erb_file = "#{dir}/templates/svnlog.erb"
0
+ xslt_file = "#{dir}/tmp/svnlog-#{item.key}.xslt"
0
 
0
- return File.read(file)
0
+ File.open(xslt_file, 'w') do |file|
0
+ file.puts ERB.new(File.read(erb_file)).result(binding)
0
+ end
0
 
0
- # TODO: cant get this working
0
- if $sendfile
0
- # not implemented or tested, rly
0
- @headers['X-Sendfile'] = Pathname.new(__FILE__).dirname.realpath.to_s + file
0
- else
0
- # default to nginx
0
- @headers['X-Accel-Redirect'] = "/static/#{file}"
0
- end
0
+ File.open(tmp_file, 'w') do |f|
0
+ timeout do
0
+ f.puts `/usr/bin/env svn log #{item.url.gsub(/ |\\|;/,'')} --limit 15 -v --xml`
0
     end
0
   end
0
 
0
- # dev mode only
0
- class Image < R '/images/diag.gif'
0
- def get
0
- @headers['Content-Type'] = 'image/gif'
0
- File.read('images/diag.gif')
0
+ File.open(@file, 'w') do |f|
0
+ timeout do
0
+ f.puts `/usr/bin/env xsltproc #{xslt_file} #{tmp_file}`
0
     end
0
- end
0
+ end
0
+
0
+ `rm #{tmp_file} #{xslt_file}`
0
+ sendfile @file
0
 end
0
 
0
-module Subtle::Views
0
- def layout
0
- html {
0
- head {
0
- _style
0
- if @rss
0
- link :href => @rss, :rel => "alternate", :type => "application/rss+xml"
0
- end
0
- title 'subtlety : a remote subversion excursion'
0
- }
0
- body {
0
- div.wrap {
0
- div.header { h1 'subtlety.' }
0
- div.main { self << yield.compact }
0
- div.footer { _footer }
0
- }
0
- _clicky
0
- }
0
- }
0
+config_for :development do
0
+ def sendfile(file)
0
+ return if file.include? '..'
0
+ File.read(file)
0
   end
0
 
0
- def _clicky
0
- text '<script src="http://getclicky.com/1149.js"> </script> <noscript><img height=0 width=0 src="http://getclicky.com/1149ns.gif"></noscript>'
0
- end
0
+ get '/images/diag.gif' do
0
+ @headers['Content-Type'] = 'image/gif'
0
+ File.read('images/diag.gif')
0
+ end
0
+end
0
 
0
- def index
0
- p.first { %Q[
0
- Welcome. Here's what we do: we take a remote, public subversion repository (http:// or svn://) and give you an rss feed of
0
- the changes. That's it. Have an <strong>svn:external</strong> or <strong><a href="http://piston.rubyforge.org/">pistonized</a></strong>
0
- repository in your app you need to monitor? Look no further: just plug in the repository's location and start reveling in the
0
- sweet, sweet changesets, rss-style.
0
- ] }
0
- p { %[The inaugural blog entry <a href="http://errtheblog.com/post/701">is here</a>.] }
0
- p { %Q[
0
- One thing: please add the repository's root path. So, svn://errtheblog.com/svn/mofo, <strong>not</strong>
0
- svn://errtheblog.com/svn/mofo/trunk. Thanks, and enjoy.
0
- ] }
0
- _form
0
+config_for :production do
0
+ def sendfile(file)
0
+ return if file.include? '..'
0
+ @headers['X-Accel-Redirect'] = "/static/#{file}"
0
   end
0
+end
0
 
0
- def save
0
- if @rss
0
- p.first text("Here it is, your very own RSS feed of the changes committed to <strong>#{@repository.url}</strong>:")
0
- url = R(Feed, @repository.key)
0
- h3 { a("http://subtlety.errtheblog.com#{url}", :href => url) }
0
- p text("&laquo; " + a("back home.", :href => '/'))
0
- else
0
- p.highlight "Sorry, there was some kind of error. Are you sure your repository url's valid? Does it start with svn:// or http://?"
0
- _form
0
- end
0
+module Helpers
0
+ extend self
0
+
0
+ def clicky
0
+ '<script src="http://getclicky.com/1149.js"> </script> <noscript><img height=0 width=0 src="http://getclicky.com/1149ns.gif"></noscript>'
0
   end
0
 
0
- def _form
0
- h3 "create an rss feed from a public subversion repository:"
0
- form :id => 'feed-me', :method => 'post', :action => R(Save) do
0
- p { input.normal :name => 'feed', :size => 61, :type => 'text', :value => @repository ? @repository.url : '' }
0
- p { input.submit :type => 'submit', :value => "feed me." }
0
- end
0
+ def form
0
+ <<-end_form
0
+ <h3>create an rss feed from a public subversion repository:</h3>
0
+ <form id="feed-me" method="post" action="/s">
0
+ <p><input class="normal" type="text" name="feed" size="61" value="<%= @repository ? @repository.url : '' %>" /></p>
0
+ <p><input type="submit" value="feed me."/></p>
0
+ </form>
0
+ end_form
0
   end
0
 
0
- def _style
0
- style :type => "text/css" do
0
- %Q[
0
- body { background-color: #333; margin: 10px; font-family: arial, sans-serif; font-size: 13px; color: #333;
0
- background-image: url(/images/diag.gif); background-position: 760px; text-align: center; }
0
- div.wrap { width: 760px; text-align: left; margin: 0 auto; }
0
- a { text-decoration: none; color: #333; border-bottom: 1px solid #333; font-weight: bold; }
0
- a:hover { border-bottom: none; }
0
- div.header { background-color: #a7bc66; padding: 10px 10px 6px 10px; margin-bottom: 10px; text-align:right; }
0
- div.main { line-height: 150%; background-color: white; padding: 10px; margin-bottom: 10px; }
0
- div.footer { text-align: center; line-height: 150%; background-color: #999; padding: 10px;
0
- margin-bottom: 10px; }
0
- div.main p { margin: 10px 0 0 0; }
0
- div.main p.first { margin-top: 0; }
0
- div.main p.title { font-size: 16px; font-weight: bold; margin-top: 0; }
0
- p.highlight { font-size: 2em; background: #999; padding: 10px; line-height: 120%; }
0
- .caps { font-size: 93%; text-transform: uppercase; }
0
- .desc { margin-left: 10px; padding: 10px; background-color: #ddd; line-height: 150%; }
0
- .desc .title { font-weight: bold; }
0
- .bar { margin-top: 10px; border-top: 1px solid #ccc; padding-top: 10px; }
0
- table { margin-bottom: 10px; }
0
- input.normal { font-size: 18px; font-weight: bold; padding: 5px; color: #a7bc66; border-color: #999999;
0
- border-width: 1px; border-style: solid; }
0
- ].compact
0
- end
0
+ def style
0
+ style = %Q[
0
+ body { background-color: #333; margin: 10px; font-family: arial, sans-serif; font-size: 13px; color: #333;
0
+ background-image: url(/images/diag.gif); background-position: 760px; text-align: center; }
0
+ div.wrap { width: 760px; text-align: left; margin: 0 auto; }
0
+ a { text-decoration: none; color: #333; border-bottom: 1px solid #333; font-weight: bold; }
0
+ a:hover { border-bottom: none; }
0
+ div.header { background-color: #a7bc66; padding: 10px 10px 6px 10px; margin-bottom: 10px; text-align:right; }
0
+ div.main { line-height: 150%; background-color: white; padding: 10px; margin-bottom: 10px; }
0
+ div.footer { text-align: center; line-height: 150%; background-color: #999; padding: 10px;
0
+ margin-bottom: 10px; }
0
+ div.main p { margin: 10px 0 0 0; }
0
+ div.main p.first { margin-top: 0; }
0
+ div.main p.title { font-size: 16px; font-weight: bold; margin-top: 0; }
0
+ p.highlight { font-size: 2em; background: #999; padding: 10px; line-height: 120%; }
0
+ .caps { font-size: 93%; text-transform: uppercase; }
0
+ .desc { margin-left: 10px; padding: 10px; background-color: #ddd; line-height: 150%; }
0
+ .desc .title { font-weight: bold; }
0
+ .bar { margin-top: 10px; border-top: 1px solid #ccc; padding-top: 10px; }
0
+ table { margin-bottom: 10px; }
0
+ input.normal { font-size: 18px; font-weight: bold; padding: 5px; color: #a7bc66; border-color: #999999;
0
+ border-width: 1px; border-style: solid; }
0
+ ].compact
0
+
0
+ "<style type='text/css'>#{style}</style>"
0
   end
0
 
0
- def _footer
0
- text "Powered by "
0
- a 'Camping', :href => "http://code.whytheluckystiff.net/camping/"
0
- text ", "
0
- a 'Mongrel', :href => "http://mongrel.rubyforge.org/"
0
- text " and, to a lesser extent, "
0
- a 'Err the Blog', :href => "http://errtheblog.com/"
0
- text "."
0
+ def footer
0
+ links = [
0
+ [ 'Sinatra', "http://sinatra.rubyforge.org/" ],
0
+ [ 'Sequel', "http://sequel.rubyforge.org/" ],
0
+ " and, to a lesser extent",
0
+ [ 'Err the Blog', "http://errtheblog.com/" ]
0
+ ]
0
+
0
+ "Powered by " +
0
+ links.map { |link| link.is_a?(Array) ? "<a href='#{link.last}'>#{link.first}</a>" : link }.join(", ") +
0
+ "."
0
   end
0
 end
0
 
0
-def Subtle.create
0
- Subtle::Models.create_schema
0
+layout do
0
+ <<-end_html
0
+ <html>
0
+ <head>
0
+ #{Helpers.style}
0
+ <% if @rss %>
0
+ <link href="<%= @rss %>" rel="alternate" type="application/rss+xml"/>
0
+ <% end %>
0
+ <title>subtlety : a remote subversion and hAtom excursion</title>
0
+ </head>
0
+ <body>
0
+ <div class="wrap">
0
+ <div class="header">
0
+ <h1>subtlety 2.</h1>
0
+ </div>
0
+ <div class="main">
0
+ <%= yield.compact %>
0
+ </div>
0
+ <div class="footer">
0
+ #{Helpers.footer}
0
+ </div>
0
+ </div>
0
+ #{Helpers.clicky}
0
+ </body>
0
+ </html>
0
+ end_html
0
 end
0
+

Comments

    No one has commented yet.