jm81-svn-fixture 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ .project
7
+ .loadpath
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jared Morgan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ svn-fixture
2
+ ===========
3
+
4
+ svn-fixture simplifies creating (or updating) a Subversion repository. It is
5
+ designed to be used in tests that require a Subversion repo, but can also be
6
+ used to initialize a repository according to some template. svn-fixture depends
7
+ on the Subversion Ruby bindings (see below for Installation help).
8
+
9
+ ##Usage
10
+
11
+ svn-fixture uses blocks to mimic the structure of the Repository, in the
12
+ hierarchy: Repository -> Revision -> Directory tree structure with
13
+ subdirectories and files. For example:
14
+
15
+ SvnFixture::repo('hello_world') do
16
+ revision(1, 'Create directories',
17
+ :author => 'jmorgan',
18
+ :date => Time.parse('2009-01-01 12:00:00Z')) do
19
+ dir 'app'
20
+ dir 'docs'
21
+ dir 'lib'
22
+ end
23
+
24
+ revision 2, 'Add a file' do
25
+ dir 'app' do
26
+ file 'hello.rb' do
27
+ prop 'is_ruby', 'Yes'
28
+ body 'puts "Hello World"'
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ SvnFixture::repo('hello_world').commit
35
+
36
+ See spec/svn-fixture/fixtures/hello_world.rb and
37
+ spec/svn-fixture/integration_spec.rb for a more complete example.
38
+
39
+ Each Repository is given a name ('hello_world' in the example above), so it can
40
+ be reopened multiple times. Repository#revision defines a new Revision. It
41
+ requires a name--but only for informational purposes--and a log message. A
42
+ Revision also accepts an options Hash including optional :author and :date
43
+ revision properties.
44
+
45
+ Within a Revision is a directory tree, specifying only **changes** in that
46
+ Revision. See Directory and File classes for details on available methods.
47
+
48
+ To actually (optionally) create the repository and make the changes and commits
49
+ specified in the Revision blocks, call Repository#commit. See Repository class
50
+ for finer tuned control over the create/checkout/commit process.
51
+
52
+ ##Installation
53
+
54
+ Install Subversion Swig bindings for Ruby. Some distros have a package for this.
55
+ In debian: sudo apt-get install libsvn-ruby . See
56
+ [https://bssvnbrowser.bountysource.com/docs/subversion_ruby_bindings](https://bssvnbrowser.bountysource.com/docs/subversion_ruby_bindings) or
57
+ [http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/INSTALL](http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/INSTALL)
58
+ for more information.
59
+
60
+ To install the gem:
61
+
62
+ gem sources -a http://gems.github.com
63
+ sudo gem install jm81-svn-fixture
64
+
65
+ To require:
66
+
67
+ gem 'jm81-svn-fixture'
68
+ require 'svn-fixture'
69
+
70
+ Note: This library could work using the svn command line client instead. I use
71
+ the bindings regularly, so using them makes sense for me. However, if you want
72
+ to be able to use svn-fixture without installing the bindings, please send an
73
+ email to jmorgan at morgancreative dot net, and I'll give it a shot.
74
+
75
+ ##Copyright
76
+
77
+ Copyright (c) 2009 Jared Morgan. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "svn-fixture"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = "jmorgan@morgancreative.net"
10
+ gem.homepage = "http://github.com/jm81/svn-fixture"
11
+ gem.authors = ["Jared Morgan"]
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'spec/rake/spectask'
20
+ Spec::Rake::SpecTask.new(:spec) do |spec|
21
+ spec.libs << 'lib' << 'spec'
22
+ spec.spec_files = FileList['spec/**/*_spec.rb']
23
+ end
24
+
25
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.pattern = 'spec/**/*_spec.rb'
28
+ spec.rcov = true
29
+ end
30
+
31
+
32
+ task :default => :spec
33
+
34
+ require 'rake/rdoctask'
35
+ Rake::RDocTask.new do |rdoc|
36
+ if File.exist?('VERSION.yml')
37
+ config = YAML.load(File.read('VERSION.yml'))
38
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
39
+ else
40
+ version = ""
41
+ end
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "svn-fixture #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,71 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require "date"
4
+ require "time"
5
+ # Subversion Ruby bindings must be installed (see README)
6
+ require "svn/core"
7
+ require "svn/fs"
8
+ require "svn/repos"
9
+
10
+ module SvnFixture
11
+ CONFIG_DEFAULTS = {
12
+ :base_path => File.join(Dir.tmpdir, 'svn-fixture')
13
+ }
14
+
15
+ class << self
16
+ # SvnFixture::config method returns Hash that can be edited.
17
+ # The only current option is
18
+ # +:base_path+ : The path at which repositories are created. It default
19
+ # to the OS tmp directory, plus "svn-fixture". For example,
20
+ # "/tmp/svn-fixture". The repo name is then appended in
21
+ # +SvnFixture::Repository+.
22
+ def config
23
+ @config ||= CONFIG_DEFAULTS.dup
24
+ end
25
+
26
+ # Return time string formatted as expected by ::Svn::Client::Context#propset
27
+ # (example 2009-06-28T12:00:00.000000Z). If +val+ does not respond to
28
+ # +strftime+, val will first be parsed via +Time.parse+.
29
+ def svn_time(val)
30
+ return nil if val.nil?
31
+ val = Time.parse(val) unless val.respond_to?(:strftime)
32
+ val.strftime("%Y-%m-%dT%H:%M:%S.000000Z")
33
+ end
34
+
35
+ # Return a Date or Time formatted as expected by
36
+ # ::Svn::Client::Context#propset (see +svn_time+); leave other values alone.
37
+ def svn_prop(val)
38
+ val.respond_to?(:strftime) ? svn_time(val) : val
39
+ end
40
+
41
+ # .repo is just a shortcut to +SvnFixture::Repository.get+
42
+ def repo(*args, &block)
43
+ SvnFixture::Repository.get(*args, &block)
44
+ end
45
+
46
+ # Setup and return a simple ::Svn::Client::Context. This is called by
47
+ # Repository#checkout, but can also be used in called Directory.new or
48
+ # File.new directly. See SvnFixture::File for examples.
49
+ def simple_context
50
+ ctx = ::Svn::Client::Context.new
51
+
52
+ # I don't understand the auth_baton and log_baton, so I set them here,
53
+ # then use revision properties.
54
+ ctx.add_username_prompt_provider(0) do |cred, realm, username, may_save|
55
+ cred.username = "ANON"
56
+ end
57
+ ctx.set_log_msg_func {|items| [true, ""]}
58
+ ctx
59
+ end
60
+ end
61
+ end
62
+
63
+ if defined?(Merb::Plugins)
64
+ # Make config accessible through Merb's Merb::Plugins.config hash
65
+ Merb::Plugins.config[:svn_fixture] = SvnFixture.config
66
+ end
67
+
68
+ # Require classes
69
+ %w{ repository revision directory file }.each do |file|
70
+ require File.dirname(__FILE__) + '/svn-fixture/' + file
71
+ end
@@ -0,0 +1,99 @@
1
+ module SvnFixture
2
+ # A Directory to be added to or edited within the Repository. Normally, this
3
+ # would br done through Directory#dir, in a block given to a Directory or
4
+ # Revision, for example:
5
+ #
6
+ # SvnFixture.repo('repo_name') do
7
+ # revision(1, 'msg') do
8
+ # dir('test-dir') do
9
+ # prop('name', 'value')
10
+ # file('file.txt')
11
+ # end
12
+ # end
13
+ # end
14
+ #
15
+ # In that case, Revision takes care of passing the +ctx+ argument.
16
+ #
17
+ # To call SvnFixture::Directory.new directly, you will need to set up a
18
+ # context (instance of Svn::Client::Context) and check out a working copy.
19
+ # +SvnFixture.simple_context+ is a quick method for settin up a Context.
20
+ #
21
+ # Assuming an existing checked out working copy:
22
+ #
23
+ # ctx = SvnFixture.simple_context
24
+ # d = SvnFixture::Directory.new(ctx, '/full/fs/path/to/dir')
25
+ # d.prop('propname', 'Value')
26
+ #
27
+ # Or, call #checkout on Context:
28
+ #
29
+ # ctx = SvnFixture.simple_context
30
+ # ctx.checkout('file:///repository/uri', '/fs/path/of/wc')
31
+ # d = SvnFixture::Directory.new(ctx, '/fs/path/of/wc/to/dir')
32
+ # d.prop('propname', 'Value')
33
+ class Directory
34
+
35
+ # +new+ is normally called through Directory#dir (a block to a Revision is
36
+ # applied to the root Directory).
37
+ #
38
+ # Arguments are:
39
+ # - +ctx+: An Svn::Client::Context, normally from Repository#ctx
40
+ # - +path+: The path (on the file system) of the Directory in the working
41
+ # copy.
42
+ def initialize(ctx, path)
43
+ @ctx = ctx
44
+ @path = path
45
+ @path += "/" unless path[-1] == 47
46
+ end
47
+
48
+ # Create or access a subdirectory. Takes the name of the subdirectory (not a
49
+ # full path) and an optional block with the subdirectory as self.
50
+ def dir(name, &block)
51
+ path = @path + name
52
+ unless ::File.directory?(path)
53
+ FileUtils.mkdir_p(path)
54
+ @ctx.add(path)
55
+ end
56
+ d = self.class.new(@ctx, path)
57
+ d.instance_eval(&block) if block_given?
58
+ d
59
+ end
60
+
61
+ # Create or access a subdirectory. Takes the name of the file (not a
62
+ # full path) and an optional block with the File as self.
63
+ def file(name, &block)
64
+ path = @path + name
65
+ unless ::File.file?(path)
66
+ FileUtils.touch(path)
67
+ @ctx.add(path)
68
+ end
69
+ f = File.new(@ctx, path)
70
+ f.instance_eval(&block) if block_given?
71
+ f
72
+ end
73
+
74
+ # Move a File or Directory. From should be an existing node. From and to can
75
+ # be any relative path below the directory.
76
+ def move(from, to)
77
+ @ctx.mv(@path + from, @path + to)
78
+ end
79
+
80
+ # Copy a File or Directory. From should be an existing node. From and to can
81
+ # be any relative path below the directory.
82
+ def copy(from, to)
83
+ @ctx.cp(@path + from, @path + to)
84
+ end
85
+
86
+ # Delete (and remove from Repository) a child node.
87
+ def delete(name)
88
+ @ctx.delete(@path + name)
89
+ end
90
+
91
+ # Set a property for the Directory
92
+ # (see http://svnbook.red-bean.com/en/1.1/ch07s02.html):
93
+ # - +name+: The property name (must be "human-readable text")
94
+ # - +value+: The value of the property.
95
+ def prop(name, value)
96
+ @ctx.propset(name, SvnFixture.svn_prop(value), @path[0..-2])
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,58 @@
1
+ module SvnFixture
2
+ # A File to be added to or edited within the Repository. Normally, this would
3
+ # done through Directory#file, in a block given to a Directory or
4
+ # Revision, for example:
5
+ #
6
+ # SvnFixture.repo('repo_name') do
7
+ # revision(1, 'msg') do
8
+ # file('file.txt') do
9
+ # prop('name', 'value')
10
+ # body('Some Text')
11
+ # end
12
+ # end
13
+ # end
14
+ #
15
+ # In that case, Revision takes care of passing the +ctx+ argument.
16
+ #
17
+ # To call SvnFixture::File.new directly, you will need to set up a context
18
+ # (instance of Svn::Client::Context) and check out a working copy.
19
+ # +SvnFixture.simple_context+ is a quick method for settin up a Context.
20
+ #
21
+ # Assuming an existing checked out working copy:
22
+ #
23
+ # ctx = SvnFixture.simple_context
24
+ # f = SvnFixture::File.new(ctx, '/full/fs/path/to/file.txt')
25
+ # f.prop('propname', 'Value')
26
+ #
27
+ # Or, call #checkout on Context:
28
+ #
29
+ # ctx = SvnFixture.simple_context
30
+ # ctx.checkout('file:///repository/uri', '/fs/path/of/wc')
31
+ # f = SvnFixture::File.new(ctx, '/full/fs/path/to/file.txt')
32
+ # f.prop('propname', 'Value')
33
+ class File
34
+
35
+ # +new+ is normally called through Directory#file (a block to a Revision is
36
+ # applied to the root Directory).
37
+ #
38
+ # Arguments are:
39
+ # - +ctx+: An Svn::Client::Context, normally from Repository#ctx
40
+ # - +path+: The path (on the file system) of the File in the working copy
41
+ def initialize(ctx, path)
42
+ @ctx, @path = ctx, path
43
+ end
44
+
45
+ # Set a property for the file
46
+ # (see http://svnbook.red-bean.com/en/1.1/ch07s02.html):
47
+ # - +name+: The property name (must be "human-readable text")
48
+ # - +value+: The value of the property.
49
+ def prop(name, value)
50
+ @ctx.propset(name, SvnFixture.svn_prop(value), @path)
51
+ end
52
+
53
+ # Set the content of a file
54
+ def body(val)
55
+ ::File.open(@path, 'w') { |f| f.write(val) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,168 @@
1
+ module SvnFixture
2
+ # Repository sets up the repository and is reponsible for checkouts and
3
+ # the actual commit(s). No actual work is done until +commit+ is called.
4
+ class Repository
5
+ attr_reader :repos, :ctx, :wc_path, :revisions
6
+
7
+ class << self
8
+ # Get an SvnFixture::Repository by name. If not found, it creates a new
9
+ # one. It accepts a block which is evaluated within the Repository
10
+ # instance. +get+ is useful for re-accessing a Repository after initially
11
+ # created. For example:
12
+ #
13
+ # SvnFixture::Repository.get('test') do
14
+ # revision(1, 'log msg') ...
15
+ # end
16
+ #
17
+ # SvnFixture::Repository.get('test') do
18
+ # revision(2, 'log msg') ...
19
+ # revision(3, 'log msg') ...
20
+ # end
21
+ #
22
+ # SvnFixture::Repository.get('test').commit
23
+ def get(name, repos_path = nil, wc_path = nil, &block)
24
+ if repositories[name]
25
+ repositories[name].instance_eval(&block) if block_given?
26
+ repositories[name]
27
+ else
28
+ Repository.new(name, repos_path, wc_path, &block)
29
+ end
30
+ end
31
+
32
+ # Hash of {name => Repository} of currently defined Repositories
33
+ def repositories
34
+ @repositories ||= {}
35
+ end
36
+
37
+ # Remove all Repositories from +.repositories+ and delete repos and
38
+ # working copy directories. Useful to call upon completion of tests.
39
+ def destroy_all
40
+ repositories.each {|name, repo| repo.destroy}
41
+ end
42
+ end
43
+
44
+ # Arguments (last two are optional)
45
+ # - +name+: The name of the repository, used by Repository.get and used in
46
+ # +repos_path+ and +wc_path+ if not given.
47
+ # - +repos_path+: The path where the repository is stored (defaults to
48
+ # "#{config[:base_path]}/repo_#{name}"
49
+ # - +wc_path+: The path where the working copy is checked out (defaults to
50
+ # "#{config[:base_path]}/wc_#{name}"
51
+ # Note: the paths should be normal file system paths, not file:/// paths.
52
+ #
53
+ # +new+ also accepts a block which is evaluated within the Repository
54
+ # instance:
55
+ #
56
+ # SvnFixture::Repository.new('name') do
57
+ # revision(1, 'log msg') ...
58
+ # end
59
+ #
60
+ # Otherwise, you could, for example:
61
+ #
62
+ # r = SvnFixture::Repository.new('name')
63
+ # r.revision(1, 'log msg') do
64
+ # ...
65
+ # end
66
+ # r.commit
67
+ def initialize(name, repos_path = nil, wc_path = nil, &block)
68
+ @name = name
69
+ if self.class.repositories[name]
70
+ raise RuntimeError, "A Repository with this name (#{@name}) already exists."
71
+ end
72
+
73
+ @repos_path = repos_path || ::File.join(SvnFixture::config[:base_path], "repo_#{name}")
74
+ @wc_path = wc_path || ::File.join(SvnFixture::config[:base_path], "wc_#{name}")
75
+ check_paths_available
76
+ @revisions = []
77
+ @dirs_created = [] # Keep track of any directories created for use by #destroy
78
+ self.class.repositories[name] = self
79
+ self.instance_eval(&block) if block_given?
80
+ end
81
+
82
+ # Add a Revision to this Repository. +name+ and +msg+ are required.
83
+ # - +name+: A name (or number of Revision). This is used in informational
84
+ # messages only.
85
+ # - +msg+: Log message for the revision.
86
+ # - +options+: :author and :date Revision properties.
87
+ # - Accepts a block that is processed by Revision#commit within a Directory
88
+ # instance (the root directory at this revision). See +Directory+ for
89
+ # more information.
90
+ def revision(name, msg, options = {}, &block)
91
+ r = Revision.new(name, msg, options, &block)
92
+ @revisions << r
93
+ r
94
+ end
95
+
96
+ # Create the Subversion repository. This is called by #checkout unless
97
+ # something already exists at @repos_path. It can also be called directly.
98
+ # This allows the flexibility of doing some work between creating the
99
+ # Repository and running checkout or commit (although I've yet to think of
100
+ # what that work would be), or creating the repository some other way.
101
+ def create
102
+ FileUtils.mkdir_p(@repos_path)
103
+ @dirs_created << @repos_path
104
+ ::Svn::Repos.create(@repos_path)
105
+ self
106
+ end
107
+
108
+ # Checkout a working copy, and setup context. This is call by #commit unless
109
+ # something already exists at @wc_path. It can also be called directly.
110
+ # This allows the flexibility of doing some work between checking out the
111
+ # Repository and commit, or checking out some other way. Also, calls #create
112
+ # if needed.
113
+ def checkout
114
+ create unless ::File.exist?(@repos_path)
115
+ @repos = ::Svn::Repos.open(@repos_path)
116
+ @repos_uri = "file://" + ::File.expand_path(@repos_path)
117
+ FileUtils.mkdir_p(@wc_path)
118
+ @dirs_created << @wc_path
119
+ @ctx = SvnFixture::simple_context
120
+ @ctx.checkout(@repos_uri, @wc_path)
121
+ self
122
+ end
123
+
124
+ # Commit actually commits the changes of the revisions. It optionally
125
+ # accepts Revisions or Revision names. If none are given, it commits all
126
+ # revisions. If any of the arguments are Revisions (not revision names),
127
+ # they do not need to be explicitly part of this Repository (that is, they
128
+ # do not need to have been created through self#revision)
129
+ #
130
+ # repos.commit # Commits all Revisions added through self#revision
131
+ # repos.commit(1,2,4) # Commits Revisions named 1, 2, and 4, added through self#revision
132
+ # repos.commit(rev1, rev3) # Assuming rev1 and rev3 are instances of
133
+ # # SvnFixture::Revision, commits them
134
+ # # whether or not they were added through self#revision
135
+ #
136
+ # A Revision can be added to the revisions Array directly:
137
+ #
138
+ # repos.revisions << Revision.new(1, 'msg')
139
+ def commit(*to_commit)
140
+ checkout unless ::File.exist?(@wc_path)
141
+ to_commit = @revisions if to_commit.empty?
142
+ to_commit = [to_commit] if (!to_commit.respond_to?(:each) || to_commit.kind_of?(String))
143
+
144
+ to_commit.each do | rev |
145
+ rev = @revisions.find{ |r| r.name == rev } unless rev.kind_of?(Revision)
146
+ rev.commit(self)
147
+ end
148
+ end
149
+
150
+ # Remove Repository from +.repositories+ and delete repos and working copy
151
+ # directories.
152
+ def destroy
153
+ @dirs_created.each { |d| FileUtils.rm_rf(d) }
154
+ self.class.repositories.delete(@name)
155
+ end
156
+
157
+ private
158
+
159
+ # Check if either @repos_path or @wc_path exist. Called by #initialize.
160
+ def check_paths_available
161
+ if ::File.exist?(@repos_path)
162
+ raise RuntimeError, "repos_path already exists (#{@repos_path})"
163
+ elsif ::File.exist?(@wc_path)
164
+ raise RuntimeError, "wc_path already exists (#{@wc_path})"
165
+ end
166
+ end
167
+ end
168
+ end