svn 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,39 @@
1
+ == About ==
2
+
3
+ This is a library for interacting with Subverison repositories. It is a
4
+ new set of ruby bindings that are simple to use and compatible with RVM
5
+ because they are dynamically linked to libsvn and do not require any native
6
+ extensions.
7
+
8
+ In addition, these new bindings expose a ruby-style API and try to hide the
9
+ underlying SVN details---for example, SVN separates repositories from their
10
+ inner filesystems, but this library hides that distinction.
11
+
12
+ == Getting Started ==
13
+
14
+ Example code:
15
+
16
+ require 'svn'
17
+
18
+ repo = Svn::Repo.open('/path/to/repo')
19
+ r4 = repo.revision(4)
20
+ puts "#{r}, by #{r.author} (on #{r.timestamp.strftime('%Y-%m-%d %H:%M')})\nLog: #{r.log}"
21
+
22
+ r4.changes.each_pair { |path, changes| ... }
23
+ puts r4.props.inspect
24
+ # => {"svn:log"=>"...", "svn:author"=>"blue", "svn:date"=>"2011-10-14T23:51:58.705026Z"}
25
+ puts r4.diff('/some/repo/file') # by default, against the previous revision
26
+
27
+ == Current Status ==
28
+
29
+ Version 0.1.0 supports read-only operations to inspect a subversion repository,
30
+ its files, and file properties. Transactions are planned and will be included
31
+ in a future version.
32
+
33
+ The current release, 0.1.0 is an experimental release and the API included is
34
+ subject to change. Rubygems version number conventions will be respected, so it
35
+ is recommended that callers specify the version of this library using the "~>"
36
+ operator:
37
+ spec.add_dependency 'ffi', '~> 0.1'
38
+
39
+ == Contributing ==
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ require 'svn/utils'
5
+ require 'svn/misc'
6
+ require 'svn/errors'
7
+ require 'svn/pools'
8
+ require 'svn/apr_utils'
9
+ require 'svn/counted_strings'
10
+
11
+ # General Svn docs here!
12
+ module Svn
13
+ autoload :Stream, 'svn/streams'
14
+ autoload :Log, 'svn/logs'
15
+ autoload :Repo, 'svn/repos'
16
+ autoload :Root, 'svn/roots'
17
+ autoload :Revision, 'svn/revisions'
18
+ autoload :Diff, 'svn/diffs'
19
+ end
@@ -0,0 +1,313 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Svn #:nodoc:
5
+
6
+ class AprHash < FFI::AutoPointer
7
+
8
+ # when used as key length, indicates that string length should be used
9
+ HASH_KEY_STRING = -1
10
+
11
+ include Enumerable
12
+
13
+ attr_accessor :keys_null_terminated
14
+ alias_method :keys_null_terminated?, :keys_null_terminated
15
+
16
+ def initialize( ptr, key_type, val_type, keys_null_terminated=true)
17
+ super( ptr )
18
+ @key_type = key_type
19
+ @val_type = val_type
20
+ @pointers = []
21
+ @keys_null_terminated = keys_null_terminated
22
+ end
23
+
24
+ class << self
25
+ # creates a new apr_hash_t that contains +contents+, if given
26
+ def create( key_type, val_type, pool=RootPool )
27
+ ptr = C.create( pool )
28
+ new( ptr, key_type, val_type )
29
+ end
30
+
31
+ def create_from( rb_hash, key_type, val_type, pool=RootPool )
32
+ create( key_type, val_type, pool ).copy_from( rb_hash )
33
+ end
34
+
35
+ def release( ptr )
36
+ # memory will be released with the allocation pool
37
+ end
38
+ end
39
+
40
+ module C
41
+ extend FFI::Library
42
+ ffi_lib 'libapr-1.so.0'
43
+
44
+ typedef :pointer, :index
45
+ typedef :pointer, :out_pointer
46
+ typedef :long, :apr_ssize
47
+ typedef Pool, :pool
48
+ typedef AprHash, :hash
49
+
50
+ # lifecycle functions
51
+ # returns a :pointer instead of a :hash because AprHash#create needs to
52
+ # add extra args to the ruby instantiation
53
+ attach_function :create,
54
+ :apr_hash_make,
55
+ [ :pool ],
56
+ :pointer
57
+
58
+ # pool accessor -- returns a Pointer instead of a Pool because Pool
59
+ # objects will destroy the pool when they are garbage collected. If the
60
+ # pool was created in SVN, we should never destroy it and if the pool was
61
+ # created elsewhere, then we should let the original instance destroy it.
62
+ # This means that the function here should not actually return a Pool.
63
+ attach_function :pool,
64
+ :apr_hash_pool_get,
65
+ [ :hash ],
66
+ :pointer
67
+
68
+ # size
69
+ attach_function :count,
70
+ :apr_hash_count,
71
+ [ :hash ],
72
+ :uint
73
+
74
+ # getter/setter methods
75
+ attach_function :get,
76
+ :apr_hash_get,
77
+ # hash key klen
78
+ [ :hash, :pointer, :apr_ssize ],
79
+ :pointer
80
+ attach_function :set,
81
+ :apr_hash_set,
82
+ # hash key klen val (NULL = delete entry)
83
+ [ :hash, :pointer, :apr_ssize, :pointer ],
84
+ :void
85
+
86
+ # iteration functions
87
+ attach_function :first,
88
+ :apr_hash_first,
89
+ [ :pool, :hash ],
90
+ :index
91
+ attach_function :next,
92
+ :apr_hash_next,
93
+ [ :index ],
94
+ :index
95
+ attach_function :this,
96
+ :apr_hash_this,
97
+ [ :index, :out_pointer, :out_pointer, :out_pointer ],
98
+ :void
99
+ end
100
+
101
+ # use the above C module for the source of bound functions
102
+ bind_to C
103
+
104
+ # bound method definitions
105
+ bind :size, :to => :count
106
+
107
+ # because pool returns a Pointer and not a Pool as is expected, make it
108
+ # private so others can't use it.
109
+ bind :pool
110
+ private :pool
111
+
112
+ def each_pair
113
+ # outgoing pointers for keys and values
114
+ key_ptr = FFI::MemoryPointer.new( :pointer )
115
+ # apr_ssize_t => ssize_t => platform-specific long
116
+ len_ptr = FFI::MemoryPointer.new( :long )
117
+ val_ptr = FFI::MemoryPointer.new( :pointer )
118
+
119
+ # initialize a hash index to the first entry
120
+ iter = C.first( pool, self )
121
+
122
+ while !iter.null?
123
+ # get the key, key length, and val
124
+ C.this( iter, key_ptr, len_ptr, val_ptr )
125
+
126
+ # read the key
127
+ key_len = len_ptr.read_long
128
+ key = Utils.content_for( key_ptr.read_pointer, @key_type, key_len )
129
+
130
+ # yield the key and value
131
+ yield key, Utils.content_for( val_ptr.read_pointer, @val_type ) if block_given?
132
+
133
+ # advance to the next iteration
134
+ iter = C.next( iter )
135
+ end
136
+ end
137
+
138
+ def []( key )
139
+ val = nil # keep val in scope
140
+
141
+ if key.is_a?( String ) && keys_null_terminated?
142
+ val = C.get( self, key, HASH_KEY_STRING )
143
+ elsif key.respond_to? :size
144
+ val = C.get( self, key, key.size )
145
+ elsif key.respond_to? :length
146
+ val = C.get( self, key, key.length )
147
+ else
148
+ raise ArgumentError, "Invalid key #{key}: cannot determine length"
149
+ end
150
+
151
+ Utils.content_for( val, @val_type )
152
+ end
153
+
154
+ def []=( key, val )
155
+ val_ptr = Utils.pointer_for( val, @val_type )
156
+
157
+ # because the pointers passed in are referenced in native code, keep
158
+ # track of the pointers so they aren't garbage collected until this hash
159
+ # is destroyed
160
+ @pointers << val_ptr
161
+
162
+ if key.is_a?( String ) && keys_null_terminated?
163
+ C.set( self, key, HASH_KEY_STRING, val_ptr )
164
+ elsif key.respond_to? :size
165
+ C.set( self, key, key.size, val_ptr )
166
+ elsif key.respond_to? :length
167
+ C.set( self, key, key.length, val_ptr )
168
+ else
169
+ raise ArgumentError, "Invalid key #{key}: cannot determine length"
170
+ end
171
+
172
+ val
173
+ end
174
+
175
+ def copy_from( rb_hash )
176
+ rb_hash.each_pair do |key, val|
177
+ self[ key ] = val
178
+ end
179
+
180
+ self
181
+ end
182
+
183
+ def to_h
184
+ rb_hash = {}
185
+ each_pair do |key, val|
186
+ rb_hash[ key ] = val
187
+ end
188
+ rb_hash
189
+ end
190
+
191
+ end
192
+
193
+ class AprArray < FFI::AutoPointer
194
+
195
+ include Enumerable
196
+
197
+ def initialize( ptr, type )
198
+ super( ptr )
199
+ @type = type
200
+ @pointers = []
201
+ end
202
+
203
+ class << self
204
+ # creates a new AprArray of +nelts+ elements of +type+; allocation is done
205
+ # in +pool+, which defaults to Svn::RootPool
206
+ def create( type, nelts, pool=RootPool )
207
+ ptr = C.create( pool, nelts, FFI::Pointer.size )
208
+ new( ptr, type )
209
+ end
210
+
211
+ def create_from( rb_arr, type, pool=RootPool )
212
+ create( type, rb_arr.size ).copy_from( rb_arr )
213
+ end
214
+
215
+ def release
216
+ # memory will be released with the allocation pool
217
+ end
218
+ end
219
+
220
+ module C
221
+ extend FFI::Library
222
+ ffi_lib 'libapr-1.so.0'
223
+
224
+ typedef :pointer, :index
225
+ typedef :pointer, :out_pointer
226
+ typedef :long, :apr_ssize
227
+ typedef Pool, :pool
228
+ typedef AprArray, :array
229
+
230
+ # allocation
231
+ # returns a :pointer instead of a :array because AprArray#create needs to
232
+ # add extra args to the ruby instantiation
233
+ attach_function :create,
234
+ :apr_array_make,
235
+ [ :pool, :int, :int ],
236
+ :pointer
237
+
238
+ # empty?
239
+ attach_function :is_empty,
240
+ :apr_is_empty_array,
241
+ [ :array ],
242
+ :int
243
+
244
+ # modifier methods
245
+ attach_function :push,
246
+ :apr_array_push,
247
+ [ :array ],
248
+ :pointer
249
+
250
+ attach_function :pop,
251
+ :apr_array_pop,
252
+ [ :array ],
253
+ :pointer
254
+ end
255
+
256
+ # helper procs for method binding
257
+ test_c_true = Proc.new { |i| i == 1 }
258
+
259
+ # use the above C module for the source of bound functions
260
+ bind_to C
261
+
262
+ # bound method definitions
263
+ bind :empty?, :to => :is_empty,
264
+ :before_return => test_c_true
265
+
266
+ def push( el )
267
+ # turn element into a pointer
268
+ ptr = Utils.pointer_for( el, @type )
269
+ # get the array element location and write ptr there
270
+ location = C.push( self )
271
+ location.write_pointer( ptr )
272
+
273
+ # because the pointers passed in are referenced in native code, keep
274
+ # track of the pointers so they aren't garbage collected until this hash
275
+ # is destroyed
276
+ @pointers << ptr
277
+
278
+ el
279
+ end
280
+ alias_method :add, :push
281
+ alias_method :<<, :push
282
+
283
+ def copy_from( arr )
284
+ arr.each do |el|
285
+ self << el
286
+ end
287
+ self
288
+ end
289
+
290
+ def pop
291
+ location = C.pop( self )
292
+ Utils.content_for( location.read_pointer, @type )
293
+ end
294
+
295
+ # these are commented out because they don't work. it doesn't look like APR
296
+ # array accesses happen this way.
297
+ # def []( num )
298
+ # num = num.to_i
299
+ # Utils.content_for( get_pointer( num * FFI::Pointer.size ), @type )
300
+ # end
301
+ #
302
+ # def []=( num, val )
303
+ # num = num.to_i
304
+ #
305
+ # ptr = Utils.pointer_for( val, @type )
306
+ # put_pointer( num * FFI::Pointer.size, ptr )
307
+ #
308
+ # val
309
+ # end
310
+
311
+ end
312
+
313
+ end
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Svn #:nodoc:
5
+
6
+ # a struct for interacting with svn_string_t values
7
+ class CountedStringStruct < FFI::Struct
8
+
9
+ layout(
10
+ # because the data may not be NULL-terminated, treat it as a pointer
11
+ # and always read the string contents with FFI::Pointer#read_string
12
+ :data, :pointer,
13
+ :length, :size_t
14
+ )
15
+
16
+ # returns a new ruby String with the CountedString's contents.
17
+ def to_s
18
+ self[:data].read_string( self[:length] )
19
+ end
20
+
21
+ def inspect
22
+ to_s.inspect
23
+ end
24
+
25
+ end
26
+
27
+ # the svn_string_t pointer type, which is the one used externally
28
+ CountedString = CountedStringStruct.by_ref
29
+
30
+ def CountedString.from_string( content )
31
+ return content if content.is_a? CountedStringStruct
32
+ cstr = CountedStringStruct.new
33
+ cstr[:data] = FFI::MemoryPointer.from_string( content )
34
+ cstr[:length] = content.size
35
+ cstr
36
+ end
37
+
38
+ end
@@ -0,0 +1,209 @@
1
+ require 'rubygems'
2
+ require 'stringio'
3
+ require 'ffi'
4
+
5
+ module Svn #:nodoc:
6
+
7
+ class Diff < FFI::AutoPointer
8
+
9
+ class << self
10
+ def string_diff( original, modified, options={}, pool=RootPool )
11
+ options = FileOptions.from_hash( options ) if options.is_a? Hash
12
+ original = CountedString.from_string( original )
13
+ modified = CountedString.from_string( modified )
14
+
15
+ out = FFI::MemoryPointer.new( Diff )
16
+
17
+ Error.check_and_raise(
18
+ C.string_diff( out, original, modified, options, pool )
19
+ )
20
+
21
+ d = new( out.read_pointer )
22
+
23
+ return nil if d.null?
24
+
25
+ d.type = :string
26
+ d.original = original
27
+ d.modified = modified
28
+ d.options = options
29
+ d.pool = pool
30
+
31
+ return d
32
+ end
33
+
34
+ def file_diff( original_path, modified_path, options={}, pool=RootPool )
35
+ options = FileOptions.from_hash( options ) if options.is_a? Hash
36
+
37
+ out = FFI::MemoryPointer.new( Diff )
38
+
39
+ Error.check_and_raise(
40
+ C.file_diff( out, original_path, modified_path, options, pool )
41
+ )
42
+
43
+ d = new( out.read_pointer )
44
+
45
+ return nil if d.null?
46
+
47
+ d.type = :file
48
+ d.original_path = original_path
49
+ d.modified_path = modified_path
50
+ d.options = options
51
+ d.pool = pool
52
+
53
+ return d
54
+ end
55
+
56
+ def release( ptr )
57
+ # diff objects will probably need to keep track of the pool in which
58
+ # they are allocated so they can be freed in that pool
59
+ end
60
+ end
61
+
62
+ attr_accessor :type
63
+ attr_accessor :original
64
+ attr_accessor :original_path
65
+ attr_accessor :modified
66
+ attr_accessor :modified_path
67
+ attr_accessor :options
68
+ attr_accessor :pool
69
+
70
+ def changed?
71
+ ( C.is_changed( self ) == 1 )
72
+ end
73
+
74
+ def conflicts?
75
+ ( C.has_conflicts( self ) == 1 )
76
+ end
77
+
78
+ def unified( *args )
79
+ # keep these in scope
80
+ out_stream = nil
81
+ options = nil
82
+ pool = nil
83
+
84
+ case args.size
85
+ when 0
86
+ # use all defaults
87
+ when 1
88
+ if args.first.is_a? Hash
89
+ options = args.first
90
+ elsif args.first.is_a? IO or args.first.is_a? StringIO
91
+ out_stream = args.first
92
+ elsif args.first.is_a? Pool
93
+ pool = args.first
94
+ end
95
+ when 2, 3
96
+ out_stream, options, pool = args
97
+ else
98
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 3)"
99
+ end
100
+
101
+ # defaults
102
+ out_stream ||= StringIO.new
103
+ options ||= {}
104
+ pool ||= RootPool
105
+
106
+ # get common options
107
+ encoding = options[:encoding] || 'utf-8'
108
+ original_header = options[:original_header]
109
+ modified_header = options[:modified_header]
110
+
111
+ case type
112
+ when :string
113
+ with_diff_header = ( options[:with_diff_header] ? 1 : 0 )
114
+
115
+ Error.check_and_raise( C.string_output_unified(
116
+ Svn::Stream.wrap_io( out_stream ), self,
117
+ original_header, modified_header, encoding,
118
+ original, modified, pool
119
+ ) )
120
+
121
+ when :file
122
+ path_strip = options[:path_strip]
123
+ show_c_function = ( options[:show_c_function] ? 1 : 0 )
124
+
125
+ Error.check_and_raise( C.file_output_unified(
126
+ Svn::Stream.wrap_io( out_stream ), self,
127
+ original_path, modified_path,
128
+ original_header, modified_header, encoding, path_strip,
129
+ show_c_function, pool
130
+ ) )
131
+ end
132
+
133
+ out_stream.rewind if out_stream.is_a? StringIO
134
+
135
+ out_stream
136
+ end
137
+
138
+ class FileOptionsStruct < FFI::Struct
139
+ layout(
140
+ :ignore_whitespace, :int, # :whitespace,
141
+ :ignore_eol_style, :int,
142
+ :show_c_function, :int
143
+ )
144
+ end
145
+
146
+ # create a mapped type for use elsewhere
147
+ FileOptions = FileOptionsStruct.by_ref
148
+
149
+ def FileOptions.from_hash( hash )
150
+ options = FileOptionsStruct.new
151
+ hash.each_pair do |key, val|
152
+ # add a check if key is in members?
153
+ options[key] = val
154
+ end
155
+ options
156
+ end
157
+
158
+ module C
159
+
160
+ extend FFI::Library
161
+ ffi_lib 'libsvn_diff-1.so.1'
162
+
163
+ typedef :pointer, :out_pointer
164
+ typedef Pool, :pool
165
+ typedef CError.by_ref, :error
166
+ typedef Stream, :stream
167
+ typedef FileOptions, :file_options
168
+ typedef CountedString, :counted_string
169
+ typedef Diff, :diff
170
+
171
+ enum :whitespace, [
172
+ :none, # do not ignore whitespace
173
+ :change, # treat as a single char
174
+ :all # ignore all whitespace chars
175
+ ]
176
+
177
+ # diff functions
178
+ attach_function :file_diff,
179
+ :svn_diff_file_diff_2,
180
+ [ :out_pointer, :string, :string, :file_options, :pool ],
181
+ :error
182
+
183
+ attach_function :string_diff,
184
+ :svn_diff_mem_string_diff,
185
+ [ :out_pointer, :counted_string, :counted_string, :file_options,
186
+ :pool ],
187
+ :error
188
+
189
+ # diff inspection
190
+ attach_function :is_changed, :svn_diff_contains_diffs, [:diff], :int
191
+ attach_function :has_conflicts, :svn_diff_contains_conflicts, [:diff], :int
192
+
193
+ # output functions
194
+ attach_function :file_output_unified,
195
+ :svn_diff_file_output_unified3,
196
+ [ :stream, :diff, :string, :string, :string, :string, :string,
197
+ :string, :int, :pool ],
198
+ :error
199
+
200
+ attach_function :string_output_unified,
201
+ :svn_diff_mem_string_output_unified,
202
+ [ :stream, :diff, :string, :string, :string,
203
+ :counted_string, :counted_string, :pool ],
204
+ :error
205
+ end
206
+
207
+ end
208
+
209
+ end