svn 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/svn.rb CHANGED
@@ -16,4 +16,12 @@ module Svn
16
16
  autoload :Root, 'svn/roots'
17
17
  autoload :Revision, 'svn/revisions'
18
18
  autoload :Diff, 'svn/diffs'
19
+
20
+ def self.create( path )
21
+ Repo.create( path )
22
+ end
23
+
24
+ def self.open( path )
25
+ Repo.open( path )
26
+ end
19
27
  end
@@ -7,10 +7,51 @@ module Svn
7
7
 
8
8
  class << self
9
9
 
10
+ ERROR_CLASSES = {}
11
+
12
+ # used to turn generic messages for unknown errors into class names
13
+ # e.g., "Repository creation failed" => 'RepositoryCreationFailedError'
14
+ def class_name_for( message )
15
+ message.split(/\s+/).each(&:capitalize!).join + 'Error'
16
+ end
17
+
18
+ def specific_error_class( c_error )
19
+ # if an error class is already set, return it. otherwise, create a new
20
+ # one from the error's generic message
21
+ (
22
+ get( c_error.code ) ||
23
+ add( c_error.code, class_name_for( c_error.generic_message ) )
24
+ )
25
+ end
26
+
27
+ def get( code )
28
+ ERROR_CLASSES[code]
29
+ end
30
+
31
+ def add( code, class_or_name )
32
+ klass = nil # keep in scope
33
+
34
+ if class_or_name.is_a? Class
35
+ klass = class_or_name
36
+ else
37
+ name = class_or_name
38
+ begin
39
+ # fetch an existing error class and save it for the error code
40
+ klass = Svn.const_get( name )
41
+ rescue NameError => err
42
+ # create the error class and return it
43
+ $stderr.puts "Creating #{name} for #{code}" if $debug_svn_errors
44
+ klass = Svn.const_set( name, Class.new( Svn::Error ) )
45
+ end
46
+ end
47
+
48
+ ERROR_CLASSES[code] = klass
49
+ end
50
+
10
51
  # checks error and raises an exception for error if necessary
11
- def check_and_raise( err )
12
- return if err.null?
13
- raise Error.new( err )
52
+ def check_and_raise( err_ptr )
53
+ return if err_ptr.null?
54
+ raise specific_error_class( err_ptr ).new( err_ptr.message )
14
55
  end
15
56
 
16
57
  # returns a proc that calls check_and_raise
@@ -24,7 +65,7 @@ module Svn
24
65
 
25
66
  def initialize( message_or_c_error )
26
67
  if message_or_c_error.is_a? CError
27
- super( message_or_c_error.best_message )
68
+ super( message_or_c_error.cause_message )
28
69
  @c_error = message_or_c_error
29
70
  else
30
71
  super( message_or_c_error )
@@ -33,9 +74,16 @@ module Svn
33
74
 
34
75
  end
35
76
 
77
+ # create sensible names for known error classes
78
+ Error.add( 2, :PathNotFoundError )
79
+ Error.add( 20, :NotADirectoryError )
80
+ Error.add( 160006, :InvalidRevisionError )
81
+ Error.add( 165002, ArgumentError )
82
+ Error.add( 200011, :DirectoryNotEmptyError )
83
+
36
84
  class CError < FFI::ManagedStruct
37
85
  layout(
38
- :apr_error, :int,
86
+ :error_code, :int,
39
87
  :message, :string,
40
88
  :child, :pointer,
41
89
  :pool, :pointer,
@@ -49,35 +97,74 @@ module Svn
49
97
  end
50
98
  end
51
99
 
52
- # returns the most accurate message for an error
53
- def best_message
54
- # create a buffer, which may be used to hold the best message
55
- buf = FFI::MemoryPointer.new( 1024 )
56
- msg = C.best_message( self, buf, 1024 )
57
-
58
- # return a duplicate of msg, since it may be stored in the buffer
59
- # allocated above
60
- msg.dup
61
- end
62
-
63
100
  module C
64
101
  extend FFI::Library
65
102
  ffi_lib 'libsvn_subr-1.so.1'
66
103
 
67
104
  typedef CError.by_ref, :error
68
105
  typedef :int, :size
106
+ typedef :int, :error_code
69
107
 
70
108
  attach_function :best_message,
71
109
  :svn_err_best_message,
72
110
  [ :error, :buffer_inout, :size ],
73
111
  :string
74
112
 
113
+ attach_function :root_cause,
114
+ :svn_error_root_cause,
115
+ [ :error ],
116
+ :error
117
+
118
+ attach_function :generic_message,
119
+ :svn_strerror,
120
+ [ :error_code, :buffer_inout, :size ],
121
+ :string
122
+
75
123
  attach_function :clear,
76
124
  :svn_error_clear,
77
125
  [ :error ],
78
126
  :void
79
127
  end
80
128
 
129
+ copy_msg = Proc.new { |msg| msg.dup if msg }
130
+
131
+ bind_to C
132
+
133
+ MSG_BUFFER = FFI::MemoryPointer.new(1024)
134
+ # returns the error's specific message, if present, and the code's generic
135
+ # message otherwise
136
+ bind(
137
+ :best_message,
138
+ :before_return => copy_msg
139
+ ) { |this| [this, MSG_BUFFER, 1024] }
140
+
141
+ # returns the most specific error struct from this error chain
142
+ bind :root_cause
143
+
144
+ # returns the "generic" message for the error code
145
+ bind(
146
+ :generic_message,
147
+ :before_return => copy_msg
148
+ ) { |this| [ this[:error_code], MSG_BUFFER, 1024 ] }
149
+
150
+ # returns the most specific error message from this error chain
151
+ def cause_message
152
+ root_cause.best_message
153
+ end
154
+
155
+ # returns a combined message if there are children, or the best otherwise
156
+ def message
157
+ if self[:child].null?
158
+ best_message
159
+ else
160
+ "#{best_message}: #{cause_message}"
161
+ end
162
+ end
163
+
164
+ def code
165
+ self[:error_code]
166
+ end
167
+
81
168
  end
82
169
 
83
170
  end
@@ -23,26 +23,34 @@ module Svn #:nodoc:
23
23
  # assertions in SVN libs
24
24
  #++
25
25
  def open( path, parent=RootPool )
26
+ raise ArgumentError, 'Path cannot be nil' if path.nil?
27
+
26
28
  # get a new pool for all interactions with this repository
27
29
  pool = Pool.create( parent )
28
30
 
29
- # TODO: we may need to call find_root_path for this, if C.open expects
30
- # an exact repository root path
31
31
  out = FFI::MemoryPointer.new( :pointer )
32
32
 
33
+ # make sure the path is canonical: full path from / and no trailing /
34
+ final_path = File.expand_path( path.chomp(File::SEPARATOR) )
35
+
33
36
  Error.check_and_raise(
34
- C.open( out, path.chomp(File::SEPARATOR), pool )
37
+ C.open( out, final_path, pool )
35
38
  )
36
39
 
37
40
  new( out.read_pointer, pool )
38
41
  end
39
42
 
40
43
  def create( path, parent=RootPool )
44
+ raise ArgumentError, 'Path cannot be nil' if path.nil?
45
+
41
46
  # get a new pool for all interactions with this repository
42
47
  pool = Pool.create( parent )
43
48
 
44
49
  out = FFI::MemoryPointer.new( :pointer )
45
50
 
51
+ # make sure the path is canonical: full path from / and no trailing /
52
+ final_path = File.expand_path( path.chomp(File::SEPARATOR) )
53
+
46
54
  Error.check_and_raise(
47
55
  C.create(
48
56
  out, # an out pointer to the newly created repository
@@ -227,7 +235,7 @@ module Svn #:nodoc:
227
235
  end
228
236
 
229
237
  use_fs_and_add_pool = Proc.new { |out, this, *args|
230
- [ out, fs, *args, pool ]
238
+ ([ out, fs ] + args) << pool
231
239
  }
232
240
 
233
241
  # use the above C module for the source of bound functions
@@ -85,7 +85,7 @@ module Svn #:nodoc:
85
85
 
86
86
  # helper procs for method binding
87
87
  test_c_true = Proc.new { |i| i == 1 }
88
- add_pool = Proc.new { |out, this, *args| [ out, this, *args, pool ] }
88
+ add_pool = Proc.new { |out, this, *args| ([ out, this ] + args) << pool }
89
89
 
90
90
  # use the above C module for the source of bound functions
91
91
  bind_to C
@@ -46,6 +46,12 @@ module Svn #:nodoc:
46
46
  :error
47
47
  end
48
48
 
49
+ # use the C module for all bound methods
50
+ bind_to C
51
+
52
+ bind :cancel!, :to => :cancel_transaction,
53
+ &add_pool
54
+
49
55
  end
50
56
 
51
57
  end
@@ -36,7 +36,7 @@ module Svn #:nodoc:
36
36
  end
37
37
 
38
38
  def new( *args )
39
- @klass.new( *args, *@added_args )
39
+ @klass.new( *(args + @added_args) )
40
40
  end
41
41
 
42
42
  def from_native( ptr, ctx )
@@ -206,17 +206,22 @@ module Svn #:nodoc:
206
206
  # create a new method; blocks are used to re-arrange arguments
207
207
  define_method( name ) do |*args|
208
208
  # create new pointers for the specified out arguments
209
- return_ptrs = return_types.map { |type| FFI::MemoryPointer.new( type ) }
209
+ return_ptrs = return_types.map { |type|
210
+ FFI::MemoryPointer.new( type )
211
+ }
212
+
213
+ # create the argument list for the function
214
+ call_args = return_ptrs.dup
215
+ call_args << self
216
+ call_args += args
210
217
 
211
218
  return_val = nil # keep it in scope
212
219
  if block
213
220
  # call the method with the arguments after re-arranging via block
214
- return_val = meth.call( *instance_exec(
215
- *return_ptrs, self, *args, &block
216
- ) )
221
+ return_val = meth.call( *instance_exec( *call_args, &block ) )
217
222
  else
218
223
  # call the method with the standard argument order
219
- return_val = meth.call( *return_ptrs, self, *args )
224
+ return_val = meth.call( *call_args )
220
225
  end
221
226
 
222
227
  # call the return check, if present
@@ -241,7 +246,7 @@ module Svn #:nodoc:
241
246
  end
242
247
 
243
248
  # extend FFI objects with the new helper methods
244
- #FFI::Struct.extend Extensions
249
+ FFI::Struct.extend Extensions
245
250
  FFI::AutoPointer.extend Extensions
246
251
 
247
252
  end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Svn::Repo do
4
+
5
+ context ".create" do
6
+
7
+ it "complains about nil paths" do
8
+ expect { Svn::Repo.create(nil) }.to raise_error(
9
+ ArgumentError, /cannot be nil/
10
+ )
11
+ end
12
+
13
+ it "will complain about already existing repositories" do
14
+ expect { Svn::Repo.create(test_repo_path) }.to raise_error(
15
+ ArgumentError, /existing repository/
16
+ )
17
+ end
18
+
19
+ it "will not overwrite an existing path" do
20
+ path = temp_path('existing')
21
+ FileUtils.mkdir_p(temp_path('existing', 'content'))
22
+ begin
23
+ expect { Svn::Repo.create(path) }.to raise_error(
24
+ Svn::DirectoryNotEmptyError, /exists/
25
+ )
26
+ ensure
27
+ FileUtils.rm_rf(path)
28
+ end
29
+ end
30
+
31
+ it "complains about invalid paths" do
32
+ expect {
33
+ invalid_path = temp_path( 'blah', 'blah', 'blah' )
34
+ Svn::Repo.create( invalid_path )
35
+ }.to( raise_error( Svn::PathNotFoundError) )
36
+ end
37
+
38
+ it "can create a new repository" do
39
+ path = temp_path('new_repo')
40
+ repo = Svn::Repo.create( path )
41
+ begin
42
+ repo.should be_a(Svn::Repo)
43
+ repo.null?.should be_false
44
+ ensure
45
+ FileUtils.rm_rf(path) if File.exists? path
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ context ".open" do
52
+
53
+ it "complains about nil paths" do
54
+ expect {
55
+ Svn::Repo.open(nil)
56
+ }.to raise_error( ArgumentError, /cannot be nil/ )
57
+ end
58
+
59
+ it "complains about invalid paths" do
60
+ expect {
61
+ Svn::Repo.open( temp_path( 'blah', 'blah' ) )
62
+ }.to( raise_error( Svn::PathNotFoundError ) )
63
+ end
64
+
65
+ it "complains about paths inside the repository" do
66
+ path = link_test_repo
67
+ begin
68
+ expect {
69
+ Svn::Repo.open( File.join( test_repo_path, 'trunk', 'blah' ) )
70
+ }.to( raise_error( Svn::PathNotFoundError ) )
71
+ ensure
72
+ unlink_test_repo(path)
73
+ end
74
+ end
75
+
76
+ it "can open an existing repository" do
77
+ path = link_test_repo
78
+ begin
79
+ repo = Svn::Repo.open(path)
80
+ repo.should be_a(Svn::Repo)
81
+ repo.null?.should be_false
82
+ ensure
83
+ unlink_test_repo( path )
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ context "#revision" do
90
+
91
+ before do
92
+ @path = link_test_repo
93
+ @repo = open_test( @path )
94
+ end
95
+
96
+ after do
97
+ unlink_test_repo( @path )
98
+ end
99
+
100
+ it "complains about invalid revision numbers" do
101
+ expect {
102
+ @repo.revision(10_000_000)
103
+ }.to raise_error( Svn::InvalidRevisionError )
104
+ end
105
+
106
+ it "opens valid revisions" do
107
+ rev = @repo.revision(0)
108
+ rev.should be_a( Svn::Revision )
109
+ rev.null?.should be_false
110
+ rev = @repo.revision(1)
111
+ rev.should be_a( Svn::Revision )
112
+ rev.null?.should be_false
113
+ end
114
+
115
+ end
116
+
117
+ context "#youngest" do
118
+
119
+ before do
120
+ @path = link_test_repo
121
+ @repo = open_test( @path )
122
+ end
123
+
124
+ after do
125
+ unlink_test_repo( @path )
126
+ end
127
+
128
+ it "returns a revision" do
129
+ rev = @repo.youngest
130
+ rev.should be_a( Svn::Revision )
131
+ rev.null?.should be_false
132
+ end
133
+
134
+ it "is aliased as latest" do
135
+ rev = @repo.latest
136
+ rev.should be_a( Svn::Revision )
137
+ rev.null?.should be_false
138
+ end
139
+
140
+ end
141
+
142
+ end
@@ -0,0 +1,81 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper.rb"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+ end
12
+
13
+ # add the library path
14
+ $LOAD_PATH.unshift File.join(__FILE__, '..', 'lib')
15
+ require 'svn'
16
+
17
+ # print error codes/classes that are dynamically generated to stderr.
18
+ $debug_svn_errors = true
19
+
20
+ # manage the test repo
21
+ require 'tempfile'
22
+ require 'fileutils'
23
+ require 'zlib'
24
+ require 'archive/tar/minitar'
25
+
26
+ class Counter
27
+ def initialize
28
+ @num = 0
29
+ end
30
+
31
+ def next
32
+ @num += 1
33
+ @num
34
+ end
35
+ end
36
+
37
+ TEMP_PATH_BASE = File.join( Dir.tmpdir, "svn-#{Process.pid}" )
38
+ TEST_REPO_TARBALL = File.join( File.dirname(__FILE__), '..', 'test', 'test_repo.tar.gz' )
39
+ TEST_COUNTER = Counter.new
40
+
41
+ ObjectSpace.define_finalizer(TEMP_PATH_BASE) do
42
+ FileUtils.rm_rf( File.join( Dir.tmpdir, "svn-#{Process.pid}" ) )
43
+ end
44
+
45
+ def temp_path( *parts )
46
+ File.join( TEMP_PATH_BASE, *parts )
47
+ end
48
+
49
+ def test_repo_path
50
+ temp_path( 'test_repo' )
51
+ end
52
+
53
+ # unpacks a fresh copy of the test repo tarball
54
+ def unpack_test_repo
55
+ FileUtils.mkdir_p(TEMP_PATH_BASE)
56
+ gzip_stream = Zlib::GzipReader.new(File.open(TEST_REPO_TARBALL))
57
+ Archive::Tar::Minitar.unpack(gzip_stream, temp_path)
58
+ end
59
+
60
+ def link_test_repo
61
+ name = "test-#{TEST_COUNTER.next}"
62
+ link_path = temp_path(name)
63
+ FileUtils.ln_s(test_repo_path, link_path)
64
+ link_path
65
+ end
66
+
67
+ def unlink_test_repo( path )
68
+ FileUtils.rm( path )
69
+ end
70
+
71
+ def open_test( name )
72
+ Svn::Repo.open( test_repo_path )
73
+ end
74
+
75
+ def remove_test_repo
76
+ # clean up the temporary repository, if it is present
77
+ FileUtils.rm_rf test_repo_path if File.exists? test_repo_path
78
+ end
79
+
80
+ # make sure the test repo exists for all specs
81
+ unpack_test_repo
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: svn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-11 00:00:00.000000000Z
12
+ date: 2012-04-30 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ffi
16
- requirement: &15276700 !ruby/object:Gem::Requirement
16
+ requirement: &20726960 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,7 +21,29 @@ dependencies:
21
21
  version: '1.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *15276700
24
+ version_requirements: *20726960
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &20726480 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '2.8'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *20726480
36
+ - !ruby/object:Gem::Dependency
37
+ name: archive-tar-minitar
38
+ requirement: &20726020 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '0.5'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *20726020
25
47
  description:
26
48
  email: rdblue@gmail.com
27
49
  executables: []
@@ -29,21 +51,23 @@ extensions: []
29
51
  extra_rdoc_files:
30
52
  - README
31
53
  files:
32
- - lib/svn/misc.rb
33
- - lib/svn/pools.rb
34
- - lib/svn/apr_utils.rb
35
- - lib/svn/diffs.rb
54
+ - lib/svn.rb
55
+ - lib/svn/roots.rb
56
+ - lib/svn/repos.rb
36
57
  - lib/svn/utils.rb
58
+ - lib/svn/revisions.rb
59
+ - lib/svn/diffs.rb
37
60
  - lib/svn/transactions.rb
38
61
  - lib/svn/logs.rb
39
- - lib/svn/roots.rb
40
- - lib/svn/repos.rb
41
62
  - lib/svn/streams.rb
42
- - lib/svn/errors.rb
43
- - lib/svn/revisions.rb
44
63
  - lib/svn/counted_strings.rb
45
- - lib/svn.rb
64
+ - lib/svn/misc.rb
65
+ - lib/svn/errors.rb
66
+ - lib/svn/apr_utils.rb
67
+ - lib/svn/pools.rb
46
68
  - README
69
+ - spec/repo_spec.rb
70
+ - spec/spec_helper.rb
47
71
  homepage: http://github.com/codefoundry/svn
48
72
  licenses: []
49
73
  post_install_message:
@@ -64,8 +88,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
88
  version: '0'
65
89
  requirements: []
66
90
  rubyforge_project:
67
- rubygems_version: 1.8.10
91
+ rubygems_version: 1.8.15
68
92
  signing_key:
69
93
  specification_version: 3
70
- summary: Ruby bindings for SVN based on FFI
71
- test_files: []
94
+ summary: Ruby bindings for subversion (SVN) based on FFI
95
+ test_files:
96
+ - spec/repo_spec.rb
97
+ - spec/spec_helper.rb