svn 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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