svn 0.1.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.
@@ -0,0 +1,83 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Svn
5
+
6
+ class Error < RuntimeError
7
+
8
+ class << self
9
+
10
+ # 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 )
14
+ end
15
+
16
+ # returns a proc that calls check_and_raise
17
+ def return_check
18
+ @@return_check ||= Proc.new { |ptr| Error.check_and_raise( ptr ) }
19
+ end
20
+
21
+ end
22
+
23
+ attr_reader :c_error
24
+
25
+ def initialize( message_or_c_error )
26
+ if message_or_c_error.is_a? CError
27
+ super( message_or_c_error.best_message )
28
+ @c_error = message_or_c_error
29
+ else
30
+ super( message_or_c_error )
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ class CError < FFI::ManagedStruct
37
+ layout(
38
+ :apr_error, :int,
39
+ :message, :string,
40
+ :child, :pointer,
41
+ :pool, :pointer,
42
+ :filename, :string,
43
+ :line, :long
44
+ )
45
+
46
+ class << self
47
+ def release( ptr )
48
+ C.clear( ptr )
49
+ end
50
+ end
51
+
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
+ module C
64
+ extend FFI::Library
65
+ ffi_lib 'libsvn_subr-1.so.1'
66
+
67
+ typedef CError.by_ref, :error
68
+ typedef :int, :size
69
+
70
+ attach_function :best_message,
71
+ :svn_err_best_message,
72
+ [ :error, :buffer_inout, :size ],
73
+ :string
74
+
75
+ attach_function :clear,
76
+ :svn_error_clear,
77
+ [ :error ],
78
+ :void
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,120 @@
1
+ require 'time'
2
+ require 'rubygems'
3
+ require 'ffi'
4
+
5
+ module Svn #:nodoc:
6
+
7
+ module Log
8
+
9
+ # description of a changed path
10
+ class ChangedPathStruct < FFI::Struct
11
+ layout(
12
+ :action, :char, # 'A'dd, 'D'elete, 'R'eplace, 'M'odify
13
+ :copyfrom_path, :string,
14
+ :copyfrom_rev, :long,
15
+ :node_kind, NodeKind
16
+ )
17
+
18
+ # returns a character that represents the type of the change: :added,
19
+ # :deleted, :replaced, :modified
20
+ def action
21
+ Actions[ self[:action] ]
22
+ end
23
+
24
+ # returns the path's node type (:none, :file, :dir, :unknown)
25
+ def kind
26
+ self[:node_kind]
27
+ end
28
+
29
+ # returns whether the "copied from" values are known
30
+ def copyfrom_known?
31
+ ( self[:copyfrom_rev] >= 0 )
32
+ end
33
+
34
+ # if the node was copied from another path/rev, returns the [path, rev]
35
+ # pair or nil otherwise
36
+ def copied_from
37
+ [ self[:copyfrom_path], self[:copyfrom_rev] ] if copyfrom_known?
38
+ end
39
+
40
+ def to_h
41
+ h = { :action => action, :kind => kind }
42
+ h[:copied_from] = copied_from if copyfrom_known?
43
+ h
44
+ end
45
+ end
46
+
47
+ # create a mapped type for use elsewhere
48
+ ChangedPath = ChangedPathStruct.by_ref
49
+
50
+ # A subversion log entry
51
+ class EntryStruct < FFI::Struct
52
+ layout(
53
+ :old_changed_paths, AprHash.factory( :string, :pointer ),
54
+ :rev, :long,
55
+ :rev_props, AprHash.factory( :string, [:pointer, :string] ),
56
+ :has_children, :int,
57
+ :changed_paths, AprHash.factory( :string, ChangedPath )
58
+ )
59
+
60
+ # returns the revision number
61
+ def rev
62
+ self[:rev]
63
+ end
64
+ alias_method :num, :rev
65
+
66
+ # returns whether this revision has children
67
+ def has_children?
68
+ ( self[:has_children] == 1 )
69
+ end
70
+
71
+ # returns a Hash of the revision's changed paths
72
+ def changed_paths
73
+ @changed_paths ||= ( self[:changed_paths].null? ? nil :
74
+ self[:changed_paths].to_h.tap { |by_path|
75
+ by_path.each_key { |k| by_path[k] = by_path[k].to_h }
76
+ }
77
+ )
78
+ end
79
+
80
+ # returns a Hash of the revision's properties
81
+ def props
82
+ @props ||= ( self[:rev_props].null? ? nil : self[:rev_props].to_h )
83
+ end
84
+
85
+ # return the revision's log message
86
+ def message
87
+ props[ LOG_PROP_NAME ] if props
88
+ end
89
+ alias_method :log, :message
90
+
91
+ # return the revision's author
92
+ def author
93
+ props[ AUTHOR_PROP_NAME ] if props
94
+ end
95
+
96
+ # return the Time that this revision was committed
97
+ def timestamp
98
+ Time.parse( props[ TIMESTAMP_PROP_NAME ] ) if props
99
+ end
100
+
101
+ # get the contents of this log entry as a multi-level hash
102
+ def to_h
103
+ h = {
104
+ :rev => rev,
105
+ :log => message,
106
+ :author => author,
107
+ :timestamp => timestamp,
108
+ :has_children? => has_children?,
109
+ }
110
+ h[:changed_paths] = changed_paths if changed_paths
111
+ h
112
+ end
113
+ end
114
+
115
+ # create a mapped type for use elsewhere
116
+ Entry = EntryStruct.by_ref
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,68 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Svn #:nodoc:
5
+
6
+ extend FFI::Library
7
+
8
+ NodeKind = enum( :none, :file, :dir, :unknown )
9
+
10
+ Actions = enum(
11
+ :added, 65,
12
+ :deleted, 68,
13
+ :replaced, 82,
14
+ :modified, 77
15
+ )
16
+
17
+ PathChangeKind = enum( :modified, :added, :deleted, :replaced, :reset )
18
+
19
+ class ChangedPathStruct < FFI::Struct
20
+ layout(
21
+ :id, :pointer,
22
+ :change_kind, PathChangeKind,
23
+ :text_mods, :int,
24
+ :prop_mods, :int,
25
+ :node_kind, NodeKind,
26
+ :copyfrom_known, :int,
27
+ :copyfrom_rev, :long,
28
+ :copyfrom_path, :string
29
+ )
30
+
31
+ def change_kind
32
+ self[ :change_kind ]
33
+ end
34
+
35
+ def node_kind
36
+ self[ :node_kind ]
37
+ end
38
+
39
+ def text_mods?
40
+ ( self[ :text_mods ] == 1 )
41
+ end
42
+
43
+ def prop_mods?
44
+ ( self[ :prop_mods ] == 1 )
45
+ end
46
+
47
+ def copyfrom_known?
48
+ ( self[ :copyfrom_known ] == 1 )
49
+ end
50
+
51
+ def copied_from
52
+ [ self[:copyfrom_path], self[:copyfrom_rev] ] unless copyfrom_known?
53
+ end
54
+
55
+ def to_h
56
+ h = {
57
+ :change_kind => change_kind,
58
+ :node_kind => node_kind,
59
+ :text_mods? => text_mods?,
60
+ :prop_mods? => prop_mods?,
61
+ }
62
+ h[:copied_from] = copied_from if copyfrom_known?
63
+ h
64
+ end
65
+ end
66
+ ChangedPath = ChangedPathStruct.by_ref
67
+
68
+ end
@@ -0,0 +1,67 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Svn #:nodoc:
5
+
6
+ # APR memory pool
7
+ class Pool < FFI::AutoPointer
8
+
9
+ class << self
10
+ # create a new pool in +parent+. if +parent+ is nil, the allocate a root pool.
11
+ def create( parent=nil )
12
+ out = FFI::MemoryPointer.new( :pointer )
13
+ C.create( out, parent, C::RaiseOutOfMemory, nil )
14
+ return new( out.read_pointer )
15
+ end
16
+
17
+ # free and release a pool
18
+ def release( ptr )
19
+ C.destroy( ptr )
20
+ end
21
+ end
22
+
23
+ module C
24
+ extend FFI::Library
25
+ ffi_lib 'libapr-1.so.0'
26
+
27
+ typedef :int, :apr_status
28
+ typedef Pool, :pool
29
+
30
+ callback :abort_function, [:apr_status], :int
31
+
32
+ # life-cycle methods
33
+ attach_function :initialize, :apr_initialize, [], :apr_status
34
+
35
+ attach_function :create,
36
+ :apr_pool_create_ex,
37
+ [:pointer, :pool, :abort_function, :pointer],
38
+ :apr_status
39
+
40
+ attach_function :clear, :apr_pool_clear, [:pool], :void
41
+
42
+ attach_function :destroy, :apr_pool_destroy, [:pool], :void
43
+
44
+ # instance methods
45
+ # apr_pool_cleanup_register
46
+ # apr_pool_userdata_set
47
+ # apr_pool_userdata_get
48
+
49
+ RaiseOutOfMemory = FFI::Function.new( :int, [:int] ) do |status|
50
+ raise NoMemoryError.new('Could not allocate new pool')
51
+ end
52
+
53
+ initialize
54
+ end
55
+
56
+ # use the C module for all bound methods
57
+ bind_to C
58
+
59
+ # clear all allocated memory in the pool so it can be reused
60
+ bind :clear
61
+
62
+ end
63
+
64
+ # create the root pool that will be the default pool
65
+ RootPool = Pool.create
66
+
67
+ end
@@ -0,0 +1,333 @@
1
+ require 'rubygems'
2
+ require 'ffi'
3
+
4
+ module Svn #:nodoc:
5
+
6
+ INVALID_REVNUM = -1
7
+
8
+ # A subverion repository object
9
+ #
10
+ # This represents both the repository and the filesystem
11
+ class Repo < FFI::AutoPointer
12
+
13
+ attr_reader :pool
14
+
15
+ def initialize( ptr, pool )
16
+ super( ptr )
17
+ @pool = pool
18
+ end
19
+
20
+ class << self
21
+ #--
22
+ # several methods remove trailing separators; this is to avoid triggering
23
+ # assertions in SVN libs
24
+ #++
25
+ def open( path, parent=RootPool )
26
+ # get a new pool for all interactions with this repository
27
+ pool = Pool.create( parent )
28
+
29
+ # TODO: we may need to call find_root_path for this, if C.open expects
30
+ # an exact repository root path
31
+ out = FFI::MemoryPointer.new( :pointer )
32
+
33
+ Error.check_and_raise(
34
+ C.open( out, path.chomp(File::SEPARATOR), pool )
35
+ )
36
+
37
+ new( out.read_pointer, pool )
38
+ end
39
+
40
+ def create( path, parent=RootPool )
41
+ # get a new pool for all interactions with this repository
42
+ pool = Pool.create( parent )
43
+
44
+ out = FFI::MemoryPointer.new( :pointer )
45
+
46
+ Error.check_and_raise(
47
+ C.create(
48
+ out, # an out pointer to the newly created repository
49
+ path.chomp(File::SEPARATOR), # path on disk
50
+ nil, nil, # unused
51
+ nil, nil, # configs are not implemeted
52
+ pool # the pool to use for any allocations
53
+ )
54
+ )
55
+
56
+ new( out.read_pointer, pool )
57
+ end
58
+
59
+ def delete( path, pool=RootPool )
60
+ Error.check_and_raise(
61
+ C.delete( path.chomp(File::SEPARATOR), pool )
62
+ )
63
+ end
64
+
65
+ # this release method does nothing because the repo will be released when
66
+ # its pool is destroyed
67
+ def release( ptr )
68
+ end
69
+ end
70
+
71
+ # A filesystem object. Repositories completely encapsulate the filesystem,
72
+ # so it is unnecessary to use it directly.
73
+ class FileSystem < FFI::AutoPointer
74
+ class << self
75
+ # this release method does nothing because the filesystem will be
76
+ # released with its pool, which is associated with the repo that owns
77
+ # the filesystem
78
+ def release( ptr )
79
+ end
80
+ end
81
+ end
82
+
83
+ # returns the repository's filesystem
84
+ #
85
+ # this is for internal use because Repo objects should handle all fs
86
+ # functionality
87
+ def filesystem
88
+ @fs ||= C.filesystem( self )
89
+ end
90
+ alias_method :fs, :filesystem
91
+
92
+ module C
93
+ extend FFI::Library
94
+ ffi_lib 'libsvn_repos-1.so.1'
95
+
96
+ # convenience pointers
97
+ typedef :pointer, :out_pointer
98
+ typedef :string, :path
99
+ typedef :long, :revnum
100
+ typedef :int, :bool
101
+
102
+ # data types
103
+ typedef Pool, :pool
104
+ typedef AprHash, :hash
105
+ typedef AprArray, :array
106
+ typedef CError.by_ref, :error
107
+ typedef FileSystem, :filesystem
108
+ typedef Log::Entry, :log_entry
109
+ typedef Root, :root
110
+ typedef Revision, :revision
111
+ #typedef Transaction, :transaction
112
+ typedef Repo, :repo
113
+
114
+ callback :history_function,
115
+ [ :pointer, :path, :revnum, :pool ],
116
+ :error
117
+
118
+ CollectChanges = FFI::Function.new(
119
+ :pointer, [ :pointer, :string, :long, :pointer ]
120
+ ) do |data_ptr, path, rev, pool|
121
+ arr = Utils.unwrap( data_ptr )
122
+ arr << [ path, rev ]
123
+ nil # no error
124
+ end
125
+
126
+ callback :authz_function,
127
+ [ :out_pointer, :root, :path, :pointer, :pool ],
128
+ :error
129
+
130
+ callback :log_entry_function,
131
+ [ :pointer, :log_entry, :pool ],
132
+ :error
133
+
134
+ CollectHistory = FFI::Function.new(
135
+ :pointer, [ :pointer, :pointer, :pointer ]
136
+ ) do |data_ptr, log_entry_ptr, pool|
137
+ arr = Utils.unwrap( data_ptr )
138
+ # the struct passed here is shared for all calls and the data is freed
139
+ # and overwritten, so the data from each Log::Entry struct needs to be
140
+ # copied out using :to_h
141
+ arr << Utils.content_for( log_entry_ptr, Log::Entry ).to_h
142
+ nil # no error
143
+ end
144
+
145
+ # misc functions
146
+ attach_function :find,
147
+ :svn_repos_find_root_path,
148
+ [ :path, :pool ],
149
+ :string
150
+
151
+ # repository functions
152
+ attach_function :open,
153
+ :svn_repos_open,
154
+ [ :out_pointer, :path, :pool ],
155
+ :error
156
+ attach_function :create,
157
+ :svn_repos_create,
158
+ [ :out_pointer, :path,
159
+ :pointer, :pointer, # both unused
160
+ :hash, :hash, # config, fs-config
161
+ :pool ],
162
+ :error
163
+ attach_function :delete,
164
+ :svn_repos_delete,
165
+ [ :path, :pool ],
166
+ :error
167
+
168
+ # filesystem accessor
169
+ attach_function :filesystem,
170
+ :svn_repos_fs,
171
+ [ :repo ],
172
+ :filesystem
173
+
174
+ # repository-level inspection
175
+ attach_function :history,
176
+ :svn_repos_history2,
177
+ [ :filesystem, :path,
178
+ :history_function, :pointer, # history callback and data
179
+ :authz_function, :pointer, # authz callback and data
180
+ :revnum, :revnum, # start rev and end rev
181
+ :bool, :pool ], # cross copies?
182
+ :error
183
+
184
+ attach_function :logs,
185
+ :svn_repos_get_logs4,
186
+ [ :repo,
187
+ :array, # file paths
188
+ :revnum, :revnum, :int, # start rev, end rev, and limit
189
+ :bool, # discover changed paths?
190
+ :bool, # strict history? strict = no cross copies
191
+ :bool, # include merged revisions?
192
+ :array, # rev-prop names to get; NULL=all, []=none
193
+ :authz_function, :pointer, # authz callback and data
194
+ :log_entry_function, :pointer, # receiver callback and data
195
+ :pool
196
+ ],
197
+ :error
198
+
199
+ # transaction (root) accessor, creation, and manipulation
200
+ #attach_function :create_transaction,
201
+ # :svn_repos_fs_begin_txn_for_commit2,
202
+ # [ :out_pointer, :repo, :revnum, :hash, :pool ],
203
+ # :error
204
+ #attach_function :commit_transaction,
205
+ # :svn_repos_fs_commit_txn,
206
+ # [ :out_pointer, :repo, :out_pointer, :transaction ],
207
+ # :error
208
+
209
+ ffi_lib 'libsvn_fs-1.so.1'
210
+
211
+ # youngest revision number accessor
212
+ attach_function :youngest,
213
+ :svn_fs_youngest_rev,
214
+ [ :out_pointer, :filesystem, :pool ],
215
+ :error
216
+
217
+ # revision (root) accessor
218
+ attach_function :revision,
219
+ :svn_fs_revision_root,
220
+ [ :out_pointer, :filesystem, :revnum, :pool ],
221
+ :error
222
+
223
+ #attach_function :open_transaction,
224
+ # :svn_fs_begin_txn2,
225
+ # [ :out_pointer, :filesystem, :string, :pool ],
226
+ # :error
227
+ end
228
+
229
+ use_fs_and_add_pool = Proc.new { |out, this, *args|
230
+ [ out, fs, *args, pool ]
231
+ }
232
+
233
+ # use the above C module for the source of bound functions
234
+ bind_to C
235
+
236
+ # returns the number of the youngest revision in the repository
237
+ bind :youngest,
238
+ :returning => :long,
239
+ :before_return => Proc.new { |rev| revision(rev) },
240
+ :validate => Error.return_check,
241
+ &use_fs_and_add_pool
242
+
243
+ alias_method :latest, :youngest
244
+
245
+ # returns a Revision for rev +num+ or raises Svn::Error if the revision
246
+ # does not exist
247
+ bind :revision,
248
+ :returning => :pointer,
249
+ :before_return => Proc.new { |ptr|
250
+ Revision.new( ptr, self, pool ) unless ptr.null?
251
+ },
252
+ :validate => Error.return_check,
253
+ &use_fs_and_add_pool
254
+
255
+ # returns an array of "interesting" [path, rev] pairs for path
256
+ #
257
+ # for more detailed information, use +history+
258
+ #
259
+ # if a block is given, each pair will be yielded
260
+ #
261
+ # options:
262
+ # +start_rev+ :: restricts revisions to newer than :start_rev
263
+ # +end_rev+ :: restricts revisions to older than :end_rev
264
+ # +cross_copies+ :: continue history at filesystem copies?
265
+ def changes( path, options={}, &block )
266
+ # ensure the options can be passed to C successfully
267
+ start_rev = (options[:start_rev] || 0).to_i
268
+ end_rev = (options[:end_rev] || youngest).to_i
269
+ cross_copies = ( options[:cross_copies] ? 1 : 0 )
270
+
271
+ # collect the change [path, rev] pairs
272
+ changes = []
273
+ Error.check_and_raise( C.history(
274
+ fs, path,
275
+ C::CollectChanges, Utils.wrap( changes ),
276
+ nil, nil, start_rev, end_rev, cross_copies, pool
277
+ ) )
278
+
279
+ # if the caller supplied a block, use it
280
+ changes.each( &block ) if block_given?
281
+
282
+ changes
283
+ end
284
+
285
+ # returns an array of log entry hashes for relevant revisions, containing:
286
+ # * revision number (+rev+), +log+, +author+, and +timestamp+
287
+ #
288
+ # if a block is given, each log entry hash will be yielded
289
+ #
290
+ # options:
291
+ # +start_rev+ :: restricts revisions to newer than :start_rev
292
+ # +end_rev+ :: restricts revisions to older than :end_rev
293
+ # +cross_copies+ :: continue history at filesystem copies?
294
+ # +include_merged+ :: include merged history?
295
+ # +include_changes+ :: include change info in +:changed_paths+?
296
+ #
297
+ # starting and ending revisions can be switched to reverse the final order
298
+ def history( paths=nil, options={}, &block )
299
+ # if no paths were passed, but options were, then paths will be a hash
300
+ if paths.is_a? Hash
301
+ options = paths
302
+ paths = nil
303
+ end
304
+
305
+ # ensure the options can be passed to C successfully
306
+ paths_c_arr = AprArray.create_from( Array( paths ), :string )
307
+ start_rev = (options[:start_rev] || 0).to_i
308
+ end_rev = (options[:end_rev] || youngest).to_i
309
+ limit = (options[:limit] || 0).to_i # 0 => all entries
310
+ discover_changed_paths = ( options[:include_changes] ? 1 : 0 )
311
+ strict_history = ( options[:cross_copies] ? 0 : 1 )
312
+ include_merged = ( options[:include_merged] ? 1 : 0 )
313
+
314
+ # collect the history entries
315
+ history = []
316
+ Error.check_and_raise( C.logs(
317
+ self, paths_c_arr,
318
+ start_rev, end_rev, limit,
319
+ discover_changed_paths, strict_history, include_merged,
320
+ nil, nil, nil,
321
+ C::CollectHistory, Utils.wrap( history ),
322
+ pool
323
+ ) )
324
+
325
+ # if the caller supplied a block, use it
326
+ history.each( &block ) if block_given?
327
+
328
+ history
329
+ end
330
+
331
+ end
332
+
333
+ end