whistle 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/History.txt +3 -0
  2. data/README.txt +38 -0
  3. data/bin/whistle +90 -0
  4. data/lib/config.rb +19 -0
  5. data/lib/phash.rb +16 -0
  6. data/lib/relay.rb +24 -0
  7. data/lib/resource.rb +113 -0
  8. data/lib/ssl_patch.rb +15 -0
  9. data/lib/switchbox.rb +54 -0
  10. data/lib/time_ext.rb +30 -0
  11. data/lib/version.rb +3 -0
  12. data/sample/config.yml +12 -0
  13. data/vendor/rscm-0.5.1-patched-stripped/README +218 -0
  14. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm.rb +14 -0
  15. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/abstract_log_parser.rb +35 -0
  16. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/base.rb +289 -0
  17. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/command_line.rb +146 -0
  18. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/difftool.rb +44 -0
  19. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/line_editor.rb +46 -0
  20. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/mockit.rb +157 -0
  21. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/parser.rb +39 -0
  22. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/path_converter.rb +60 -0
  23. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/platform.rb +26 -0
  24. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision.rb +103 -0
  25. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_file.rb +85 -0
  26. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_poller.rb +93 -0
  27. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revisions.rb +79 -0
  28. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/clearcase.rb +182 -0
  29. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs.rb +374 -0
  30. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs_log_parser.rb +154 -0
  31. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs.rb +120 -0
  32. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs_log_parser.rb +65 -0
  33. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone.rb +338 -0
  34. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone_log_parser.rb +109 -0
  35. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/mooky.rb +6 -0
  36. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/perforce.rb +216 -0
  37. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/star_team.rb +104 -0
  38. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion.rb +397 -0
  39. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion_log_parser.rb +165 -0
  40. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/tempdir.rb +17 -0
  41. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/time_ext.rb +11 -0
  42. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/version.rb +13 -0
  43. data/vendor/ruby-feedparser-0.5-stripped/README +14 -0
  44. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser.rb +28 -0
  45. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/feedparser.rb +300 -0
  46. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/filesizes.rb +12 -0
  47. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html-output.rb +126 -0
  48. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html2text-parser.rb +409 -0
  49. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/rexml_patch.rb +28 -0
  50. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/sgml-parser.rb +332 -0
  51. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/text-output.rb +83 -0
  52. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/textconverters.rb +120 -0
  53. metadata +132 -0
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ === 0.1 / 2008-03-05
2
+
3
+ * Initial release!
data/README.txt ADDED
@@ -0,0 +1,38 @@
1
+ = whistle
2
+
3
+ * http://whistle.rubyforge.org/
4
+
5
+ == DESCRIPTION:
6
+
7
+ Whistle is a jabber bot that will follow SVN repositories and RSS/Atom feeds and notify you about the new changes
8
+
9
+ == INSTALL:
10
+
11
+ sudo gem install whistle
12
+
13
+ Follow directions from http://whistle.rubyforge.org/
14
+
15
+ == LICENSE:
16
+
17
+ (The MIT License)
18
+
19
+ Copyright (c) 2008 Esad Hajdarevic <esad@esse.at>
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining
22
+ a copy of this software and associated documentation files (the
23
+ "Software"), to deal in the Software without restriction, including
24
+ without limitation the rights to use, copy, modify, merge, publish,
25
+ distribute, sublicense, and/or sell copies of the Software, and to
26
+ permit persons to whom the Software is furnished to do so, subject to
27
+ the following conditions:
28
+
29
+ The above copyright notice and this permission notice shall be
30
+ included in all copies or substantial portions of the Software.
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
33
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
34
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
35
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
36
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
37
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
38
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/whistle ADDED
@@ -0,0 +1,90 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__),'..','lib')
2
+ Dir[File.join(File.dirname(__FILE__),'..','vendor','**','lib')].each { |p| $LOAD_PATH << p }
3
+
4
+ begin
5
+ if ARGV.empty?
6
+ puts "Hey there! You'll need to specify at least a config file"
7
+ exit
8
+ end
9
+ require 'config'
10
+ path = ARGV.shift
11
+ path += '.yml' unless path[/\.yml$/]
12
+ $config = Whistle::Config.new(path)
13
+ rescue Errno::ENOENT
14
+ puts "Config file #{path} not found, push enter to create sample or CTRL-C"
15
+ gets
16
+ require 'fileutils'
17
+ FileUtils.copy(Whistle::Config::SAMPLE,path)
18
+ puts "Ok, edit the #{path} and try again."
19
+ exit
20
+ end
21
+
22
+ puts "Starting..."
23
+
24
+ # Ugly global logger to STDOUT only ;-)
25
+ def log(msg)
26
+ puts msg
27
+ STDOUT.flush
28
+ end
29
+
30
+ # Initialize some utilities and stuff we need
31
+ require 'rubygems'
32
+ gem 'activesupport'
33
+ require 'activesupport'
34
+
35
+ # Other extensions and patches
36
+ require 'time_ext'
37
+ require 'ssl_patch'
38
+
39
+ # Initialize store
40
+ require 'phash'
41
+ $store = PHash.new($config.name + '.data') # Global store
42
+
43
+ # Initialize switchbox
44
+ require 'switchbox'
45
+ $switchbox = Whistle::Switchbox.new($config.jabber.user,$config.jabber.password,$config.jabber.subscribers)
46
+ puts "Logged into jabber as #{$config.jabber.user}, notifying:"
47
+ $config.jabber.subscribers.each {|s| puts "- #{s}" }
48
+ #at_exit { $switchbox.deliver("Bye!") }
49
+
50
+ # Initialize relay
51
+ if $config.relay
52
+ require 'relay'
53
+ Whistle::Relay.activate
54
+ end
55
+
56
+ require 'resource'
57
+
58
+ $switchbox.inform("Hello! I just started.")
59
+
60
+ begin
61
+ while true do
62
+ log "Check started"
63
+ start = Time.now
64
+ $config.check.map {|url| Whistle::Resource.new(url) }.each do |r|
65
+ log " Checking #{r.url}"
66
+ begin
67
+ message = r.tail
68
+ if message.blank?
69
+ log " - nothing new"
70
+ else
71
+ log " - new stuff, sending notification"
72
+ $switchbox.deliver(message)
73
+ end
74
+ rescue => e
75
+ log " - error (#{e.message})"
76
+ $switchbox.inform("Error while processing #{r.url}: #{e.message}")
77
+ end
78
+ $store.flush
79
+ end
80
+ left = (5.minutes - (Time.now - start)).to_i
81
+ if left > 0
82
+ log "Finished! Now sleeping a bit."
83
+ sleep left
84
+ end
85
+ end
86
+ rescue => e
87
+ ensure
88
+ $switchbox.inform("Hey! Something funny (#{(e && e.message)}) happened. I have to shutdown now.")
89
+ raise e
90
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'yaml'
2
+ require 'ostruct'
3
+
4
+ module Whistle
5
+ class Config
6
+ SAMPLE = File.join(File.dirname(__FILE__),'..','sample','config.yml')
7
+ attr_reader :check, :jabber, :name, :relay
8
+ def initialize(path)
9
+ @name = File.basename(path,'.yml')
10
+ @config = YAML.load_file(path)
11
+ @check = @config['check']
12
+ @jabber = OpenStruct.new(@config['jabber'])
13
+ if @config['relay']
14
+ host, port = @config['relay'].split(/\:/)
15
+ @relay = OpenStruct.new :host => host, :port => port
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/phash.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'activesupport' #to_yaml
3
+
4
+ class PHash < Hash
5
+ def initialize(path)
6
+ @path = path
7
+ if File.exist?(@path)
8
+ data = YAML.load(File.read(@path)) rescue nil
9
+ self.replace(data) if data
10
+ end
11
+ end
12
+
13
+ def flush
14
+ File.open(@path,'w') { |f| f.write(self.to_yaml)}
15
+ end
16
+ end
data/lib/relay.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'socket'
2
+
3
+ module Whistle
4
+ class Relay
5
+ def self.activate
6
+ Thread.new do
7
+ server = TCPServer.new($config.relay.host, $config.relay.port)
8
+ puts "Started relay on port #{$config.relay.port}"
9
+ while s = server.accept
10
+ begin
11
+ msg = s.gets
12
+ puts "Received #{msg}, relaying"
13
+ msg.chomp!.strip! unless msg.blank?
14
+ $switchbox.deliver(msg) unless msg.blank?
15
+ rescue => e
16
+ puts "ERROR: #{e.message}"
17
+ ensure
18
+ s.close
19
+ end
20
+ end
21
+ end #.abort_on_exception = true
22
+ end
23
+ end
24
+ end
data/lib/resource.rb ADDED
@@ -0,0 +1,113 @@
1
+ require 'uri'
2
+
3
+ # Patch it so that we can remove userinfo
4
+ module URI
5
+ class Generic
6
+ def remove_userinfo!
7
+ @user, @password = nil, nil
8
+ end
9
+ end
10
+ end
11
+
12
+ require 'cgi'
13
+
14
+ require 'open-uri'
15
+ require 'feedparser'
16
+ require 'activesupport'
17
+
18
+ require 'rscm'
19
+
20
+ require 'htmlentities/string'
21
+
22
+ module Whistle
23
+ class Resource
24
+ attr_reader :url
25
+
26
+ def initialize(url)
27
+ @uri = returning URI.parse(url) do |u|
28
+ @username, @password = u.user, u.password
29
+ @username = CGI.unescape(@username) unless @username.blank? # Convert %40 in username to @ (gmail)
30
+ u.remove_userinfo!
31
+ end
32
+ @url = @uri.to_s
33
+ @store = ($store[@url] ||= {})
34
+ @type = (@store[:type] ||= guess_type)
35
+ end
36
+
37
+ def tail
38
+ case @type
39
+ when :svn
40
+ revisions = []
41
+ silence_stream(STDOUT) do # RSCM outputs everything to stdout?
42
+ scm = returning RSCM::Subversion.new(@url) do |svn|
43
+ svn.username, svn.password = @username, @password
44
+ end
45
+ since = @store[:last] || begin
46
+ rs = scm.revisions(7.days.ago)
47
+ diff_from = rs[rs.length-2]
48
+ return if diff_from.nil? # There's no revision in last days to show updates since
49
+ diff_from.identifier # Grab max last two
50
+ end
51
+ revisions = scm.revisions(since)
52
+ end
53
+ last_revision = revisions[revisions.length-1]
54
+ @store[:last] = last_revision.identifier.to_i if last_revision
55
+ revisions.map do |r|
56
+ message = "Revision #{r.identifier} commited by #{r.developer} #{Time.now.distance_in_words_since(r.time)} ago: "
57
+ message << "\n"
58
+ message << r.message.chomp.strip
59
+ message << "\n"
60
+ message << r.map {|f| " #{f.status[0,1]} #{f.path}" }.join("\n")
61
+ message << "\n"
62
+ message
63
+ end
64
+ when :feed
65
+ f = FeedParser::Feed.new(open(@url,:http_basic_authentication => [@username,@password]).read)
66
+
67
+ @store[:seen] ||= {}
68
+ @store[:seen].delete_if {|link,date| date && (date < 1.month.ago) } # Purge seen hash remove items older than one month
69
+
70
+ old, new = f.items.partition {|i| @store[:seen].has_key?(i.link) }
71
+ new = new.first(5) if @store[:seen].empty? # Limit first tail to 5 entries
72
+
73
+ # Mark all items as seen
74
+ f.items.each { |i| @store[:seen][i.link] = i.date }
75
+
76
+ return new.reverse.map do |i|
77
+ safe_title = i.title.encode_entities(:basic)
78
+ title = ''
79
+ if @uri.host == 'mail.google.com'
80
+ #title = "<span style=\"background:#ccccff; color: #000000;\">#{@username}</span>"
81
+ title << "#{@username}: #{safe_title}"
82
+ else
83
+ title = safe_title
84
+ title << " (#{i.creator})" if i.creator
85
+ end
86
+ link = i.link.encode_entities(:basic) # xmpp4r had problems with unencoded &
87
+ pre,post = title.split(/\:/,2)
88
+ if post.nil?
89
+ body = "#{title} <a href=\"#{link}\">#</a>"
90
+ else
91
+ body = "#{pre}: <a href=\"#{link}\">#{post.strip}</a>"
92
+ end
93
+ "<html><body>#{body}</body></html>"
94
+ end
95
+ end
96
+ end
97
+ private
98
+ def guess_type
99
+ log "Guessing type for #{@url}"
100
+ type = case @uri.scheme
101
+ when 'svn'
102
+ return :svn
103
+ when 'http'
104
+ is_svn = RSCM::Subversion.new(@url).central_exists? rescue false
105
+ return is_svn ? :svn : :feed
106
+ else
107
+ return :feed
108
+ end
109
+ log "Guessed #{type}"
110
+ return type
111
+ end
112
+ end
113
+ end
data/lib/ssl_patch.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'net/https'
2
+
3
+ module Net
4
+ class HTTP
5
+ alias :orig_use_ssl= :use_ssl=
6
+ def use_ssl=(flag)
7
+ self.orig_use_ssl=(flag)
8
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
9
+ end
10
+
11
+ def verify_mode=(m)
12
+ # Ignore, don't let open-uri reconfigure verify mode
13
+ end
14
+ end
15
+ end
data/lib/switchbox.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'xmpp4r-simple'
2
+ require 'htmlentities/string'
3
+
4
+ module Whistle
5
+ class Switchbox
6
+ attr_reader :user
7
+
8
+ def initialize(user,password,subscribers)
9
+ @user, @password, @subscribers = user, password, subscribers
10
+ @jabber = Jabber::Simple.new(@user, @password)
11
+ end
12
+
13
+ def deliver(messages)
14
+ @subscribers.each do |to|
15
+ Array(messages).each do |body|
16
+ format_and_send :to => to, :body => body
17
+ end
18
+ end
19
+ end
20
+
21
+ # Sends message to primary (first) subscriber only
22
+ def inform(message)
23
+ if (primary = @subscribers.first)
24
+ format_and_send :to => @subscribers.first, :body => message
25
+ end
26
+ end
27
+ private
28
+ # Processes html in the messages and sends it to a recipient
29
+ def format_and_send(options)
30
+ m = Jabber::Message::new(options[:to],options[:body]).set_type(:normal)
31
+ r = REXML::Document.new(options[:body]) rescue nil
32
+ if r && r.root && r.root.name == 'html'
33
+ # We have a html message here, prepare for sending via jabber
34
+ m.body = strip_tags(options[:body]) # Make a plaintext version
35
+ r.root.add_namespace('http://jabber.org/protocol/xhtml-im')
36
+ r.root.elements['body'].add_namespace('http://www.w3.org/1999/xhtml')
37
+ m.add_element(r)
38
+ end
39
+ # Use deliver from xmpp4r-simple so it will try adding contact first and queue the delivery
40
+ @jabber.deliver(options[:to],m)
41
+ end
42
+
43
+ def strip_tags(html)
44
+ html.gsub(/<.+?>/,'').
45
+ gsub(/&amp;/,'&').
46
+ gsub(/&quot;/,'"').
47
+ gsub(/&lt;/,'<').
48
+ gsub(/&gt;/,'>').
49
+ gsub(/&ellip;/,'...').
50
+ gsub(/&apos;/, "'").
51
+ gsub("\n",'')
52
+ end
53
+ end
54
+ end
data/lib/time_ext.rb ADDED
@@ -0,0 +1,30 @@
1
+ class Time
2
+ # Taken from ruby on rails
3
+ def distance_in_words_since(from_time, include_seconds = true)
4
+ distance_in_minutes = (((self - from_time).abs)/60).round
5
+ distance_in_seconds = ((self - from_time).abs).round
6
+
7
+ case distance_in_minutes
8
+ when 0..1
9
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
10
+ case distance_in_seconds
11
+ when 0..4 then 'less than 5 seconds'
12
+ when 5..9 then 'less than 10 seconds'
13
+ when 10..19 then 'less than 20 seconds'
14
+ when 20..39 then 'half a minute'
15
+ when 40..59 then 'less than a minute'
16
+ else '1 minute'
17
+ end
18
+
19
+ when 2..44 then "#{distance_in_minutes} minutes"
20
+ when 45..89 then 'about 1 hour'
21
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
22
+ when 1440..2879 then '1 day'
23
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
24
+ when 43200..86399 then 'about 1 month'
25
+ when 86400..525959 then "#{(distance_in_minutes / 43200).round} months"
26
+ when 525960..1051919 then 'about 1 year'
27
+ else "over #{(distance_in_minutes / 525960).round} years"
28
+ end
29
+ end
30
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Whistle
2
+ VERSION = '0.1'
3
+ end
data/sample/config.yml ADDED
@@ -0,0 +1,12 @@
1
+ check:
2
+ - svn://user:password@svn.example.org/repos/trunk
3
+ - http://user:password@example.org/rss
4
+ jabber:
5
+ user: user@jabber.example.com
6
+ password: mypassword
7
+ subscribers:
8
+ - django@example.org
9
+ - tina@somewhere.org
10
+ #- disabled@user.org
11
+ # Uncomment this to enable relaying - everything sent to this port will get relayed over jabber
12
+ #relay: localhost:2000
@@ -0,0 +1,218 @@
1
+ = RSCM - Ruby Source Control Management (0.5.1)
2
+
3
+ RSCM is to SCM what DBI/JDBC/ODBC are to databases - an SCM-independent API for accessing different SCMs. The high level features are roughly:
4
+
5
+ * Check out a working copy (with possibility to specify branch/date/label)
6
+ * Get revisions (changesets) (Emulated for non-transactional SCMs like CVS, ClearCase and StarTeam)
7
+ * Get diffs
8
+ * Add and commit files
9
+ * Manipluate triggers
10
+
11
+ Although RSCM's main focus is operations on a working copy of an SCM repository,
12
+ the API also allows some level of interaction with the SCM repository itself,
13
+ like creating new repositories.
14
+
15
+ == Download
16
+
17
+ RSCM is available as a RubyGem, and can be installed like this:
18
+
19
+ gem install rscm
20
+
21
+ (You may need administrator access to do this on a POSIX system). You can also download prebuilt gems from
22
+ http://rubyforge.org/frs/?group_id=490
23
+
24
+ If you want the latest and greatest, you can get the sources from subversion:
25
+
26
+ svn co svn://buildpatterns.com/svn/repos/rscm/trunk
27
+
28
+ == Contributors
29
+
30
+ * Aslak Hellesoy - All
31
+ * Steven Baker - Monotone
32
+ * Jon Tirsen - CVS, Subversion
33
+ * Yogi Kulkarni - Perforce
34
+
35
+ == Supported SCMs
36
+
37
+ * CVS - http://www.nongnu.org/cvs (stable)
38
+ * Subversion - http://subversion.tigris.org (stable)
39
+
40
+ In progress:
41
+
42
+ * ClearCase - http://www-306.ibm.com/software/awdtools/clearcase (not thoroughly tested)
43
+ * Darcs - http://www.abridgegame.org/darcs (very incomplete)
44
+ * Monotone - http://www.venge.net/monotone (half complete)
45
+ * Perforce - http://www.perforce.com (nearly complete - a little out of date with recent API)
46
+ * StarTeam - http://www.borland.com/starteam (nearly complete - a little out of date with recent API)
47
+
48
+ Planned:
49
+
50
+ Loads! All of them! How to add support for a new one is described further down in this file.
51
+
52
+ == Related projects
53
+
54
+ * DamageControl - http://dev.buildpatterns.com/trac/wiki/DamageControl (Continuous Integration system
55
+ built on top of RSCM and Ruby on Rails).
56
+
57
+ == Sample usage
58
+
59
+ Here is an example of how to use RSCM to get a list of revisions (aka changesets) from a subversion repository:
60
+
61
+ require 'rscm'
62
+
63
+ scm = RSCM::Subversion.new("svn://some.server/some/path/trunk")
64
+ # What follows would look the same for any supported SCM
65
+ revisions = scm.revisions(Time.utc(2004, 11, 10, 12, 34, 22)) # For Subversion, you can also pass a revision number (int)
66
+ revisions.each do |revision|
67
+ puts revision # or do something more funky with it
68
+ end
69
+
70
+ == Future plans
71
+
72
+ === Cross-SCM synchronisation
73
+ RSCM could be used as a tool to migrate files from one SCM to another (of a different type)
74
+ while keeping the history. -Similar to cvs2svn or http://nautilus.homeip.net/~lele/projects/tailor/
75
+
76
+ RSCM could also be used as a continuous synchronisation service between SCMs. This can be very useful
77
+ when you have to work with an SCM and you'd rather use a different one. RSCM could synchronise between
78
+ the central SCM and one that you set up on your local machine.
79
+
80
+ === SCM browser
81
+ A rails webapp that allows browsing of a repository, using RSCM to access it. -Perhaps even with a simple
82
+ editor allowing people to modify files and commit them via the browser.
83
+
84
+ = Implementing a new RSCM adapter
85
+
86
+ If you want RSCM to support a new SCM, you must implement a subclass of RSCM::Base.
87
+ You should focus on implementing only the features that you need. For example, if
88
+ you plan to use your new RSCM adapter with DamageControl, you only need to implement
89
+ the parts of the API that are used by DamageControl.
90
+
91
+ We'll see what steps are needed to make an adapter that passes the DamageControl compatibility suite
92
+ (which is part of RSCM). Let's imagine we want DamageControl to be able to work with the imaginary SCM
93
+ called Mooky. The rest of this section explains how to get started. You're going to need a preexisting
94
+ repository with some existing contents, basic knowledge of the SCM's command line tools and some Ruby
95
+ programming skills.
96
+
97
+ == Create the test class and the implementation class
98
+
99
+ Start by writing a test that includes the compatibility test suite you're interested in:
100
+
101
+ test/rscm/scm/mooky_test.rb
102
+
103
+ With the following content:
104
+
105
+ require 'rscm/test_helper'
106
+
107
+ module RSCM
108
+ class MookyTest < Test::Unit::TestCase
109
+ include Compatibility::DamageControlMinimal
110
+ end
111
+ end
112
+
113
+ Now create the implementation class:
114
+
115
+ lib/rscm/scm/mooky.rb
116
+
117
+ With the following content:
118
+
119
+ require 'rscm/base'
120
+
121
+ module RSCM
122
+ class Cvs < Base
123
+ end
124
+ end
125
+
126
+ Now that we have set up the basics, we can run the tests:
127
+
128
+ rake test TEST=test/rscm/scm/mooky_test.rb
129
+
130
+ It will fail - there is still some setup to do:
131
+
132
+ == Create testdata directory
133
+ You must create a new directory under test/rscm/compatibility to contain testdata. Give it a name representative
134
+ of the scm type and the contents of the scm. For example, if the existing mooky repository we're going to test
135
+ against contains source code for a chess engine, we could call the directory test/rscm/compatibility/mooky_chess.
136
+
137
+ Also add an entry in test/rscm/compatibility/config.yml mapping your test class to the testdata directory.
138
+
139
+ The DamageControl compatibility suite expects to find three YAML files in this directory, scm.yml, revisions.yml
140
+ and files_0.yml.
141
+
142
+ === Create scm.yml
143
+ This file should contain a YAML representation of the SCM instance used for testing. The test suite will load it to create an instance of your class. You're free to use whatever properties you want in your SCM implementation, and the YAML file should
144
+ contain the necessary values to connect to the preexisting repository.
145
+
146
+ === Create revisions.yml
147
+ This file should contain a RSCM::Revisions object with two RSCM::Revision objects. You can start off by making a copy of one of the
148
+ existing revisions.yml files, but you should hand-edit this file to represent two revisions in the existing repository.
149
+
150
+ You cannot choose any revision though. There are some constraints that need to be followed:
151
+ * There must be exactly two RSCM::Revision objects
152
+ * The two RSCM::Revision objects must represent to adjacent revisions from the repository
153
+ * Each of the RSCM::Revision objects must contain at least two RSCM::RevisionFile objects
154
+ * The second RSCM::Revision object must contain at least one RSCM::RevisionFile with "ADDED" state
155
+
156
+ Given these constraints you should spend some time locating two revisions that follow these constraints.
157
+ This would be a good time to familiarize yourself with the SCM's command line tool (or whatever kind of tool
158
+ the SCM provides to access it).
159
+
160
+ ==== Special note for non-transactional SCMs
161
+
162
+ Non-transactional SCMs usually use dates (and not revision identifiers) to report changes. Most SCMs report changes to files
163
+ (which will become RSCM::RevisionFile instances) in some sort of log. These changes will typically not be logically
164
+ grouped. RSCM::Revisions.add will group revision files that have:
165
+ * similar modification time (max 1 minute apart)
166
+ * the same commit message
167
+ * the same developer
168
+
169
+ So when mining for revisions that follow the constraints for revisions.yml, you should also be looking for groupings
170
+ in modification time, commit message and developer.
171
+
172
+ === Create files_0.yml
173
+ This file should contain the files that will be in the working copy after a checkout of the 1st revision in revisions.yml,
174
+ sorted by their path.
175
+
176
+ === Create old.yml
177
+ This file should contain a start time and all the revision identifiers before that time.
178
+ The start time should be a carefully selected timestamp close to the start of the scm.
179
+ "identifiers" should be a list of all identifiers from the beginning of time up until
180
+ the start identifier.
181
+
182
+ === Create diff.txt
183
+ This file should contain a diff. It should be the diff of the first revision file in revisions.yml -
184
+ between its native_revision_identifier and the previous_revision_identifier.
185
+
186
+ === Create file.txt
187
+ This file should contain the contents of the first revision file in revisions.yml (at the revision
188
+ specified by native_revision_identifier).
189
+
190
+ == Implement the methods
191
+ Now that we have set up everything needed for the tests, we can run the tests again:
192
+
193
+ rake test TEST=test/rscm/scm/mooky_test.rb
194
+
195
+ Now you should get errors about methods not being implemented. At this point you should start implementing the methods.
196
+
197
+ == Implementation tips
198
+ * Run the tests often. Let the error messages guide you in what you do next.
199
+ * Use the execute method to invoke command line tools.
200
+ * Split the implementation into two classes. One class for parsing logs and one for translating API calls to command line executions. This will allow you to test the log parsing against hard coded logs in your tests.
201
+
202
+ = Building RSCM
203
+ This section is for developers who are new to ruby development and do not already know how to build and install Ruby gems.
204
+
205
+ You need to install rubygems from http://rubyforge.org/projects/rubygems
206
+ Afterwards you need to install rake and rails
207
+
208
+ gem install rake
209
+
210
+ Now change to the RSCM root directory and type
211
+
212
+ rake gem
213
+
214
+ This will create a gem for RSCM. To install this gem, you have to change to the pkg directory and type
215
+
216
+ sudo gem install pkg/rscm-0.4.X.gem
217
+
218
+ Now you can use RSCM in other Ruby apps with a simple require 'rscm'.