svn 0.1.0

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