svn_wc 0.0.1

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_wc.rb ADDED
@@ -0,0 +1,840 @@
1
+ #--
2
+ # Copyright (c) 2009 David Wright
3
+ #
4
+ # You are free to modify and use this file under the terms of the GNU LGPL.
5
+ # You should have received a copy of the LGPL along with this file.
6
+ #
7
+ # Alternatively, you can find the latest version of the LGPL here:
8
+ #
9
+ # http://www.gnu.org/licenses/lgpl.txt
10
+ #
11
+ # This library is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # Lesser General Public License for more details.
15
+ #++
16
+
17
+ require 'yaml'
18
+ require 'pathname'
19
+ require 'find'
20
+ require 'svn/core'
21
+ require 'svn/client'
22
+ require 'svn/wc'
23
+ require 'svn/repos'
24
+ require 'svn/info'
25
+ require 'svn/error'
26
+
27
+ # = SvnWc::RepoAccess
28
+
29
+ # This module is designed to operate on a working copy (on the local filesystem)
30
+ # of a remote Subversion repository.
31
+ #
32
+ # It aims to provide (simple) client CLI type behavior, it does not do any
33
+ # sort of repository administration type operations, just working directory repository management.
34
+ #
35
+ # == Current supported operations:
36
+ # * open
37
+ # * checkout/co
38
+ # * list/ls
39
+ # * update/up
40
+ # * commit/ci
41
+ # * status/stat
42
+ # * diff
43
+ # * info
44
+ # * add
45
+ # * revert
46
+ # * delete
47
+ # * svn+ssh is our primary connection use case, however can connect to, and operate on a (local) file:/// URI as well
48
+ #
49
+ # Is built on top of the SVN (SWIG) (Subversion) Ruby Bindings and requires that they be installed.
50
+ #
51
+ # == Examples
52
+ #
53
+ # require 'svn_wc'
54
+ #
55
+ # yconf = Hash.new
56
+ # yconf['svn_user'] = 'test_user'
57
+ # yconf['svn_pass'] = 'test_pass'
58
+ # yconf['svn_repo_master'] = 'svn+ssh://www.example.com/svn_repository'
59
+ # yconf['svn_repo_working_copy'] = '/opt/svn_repo'
60
+ #
61
+ # svn = SvnWc::RepoAccess.new(YAML::dump(yconf), do_checkout=true, force=true)
62
+ # # or, can pass path to conf file
63
+ # #svn = SvnWc::RepoAccess.new(File.join(path_to_conf,'conf.yml'), do_checkout=true, force=true)
64
+ #
65
+ # info = svn.info
66
+ # puts info[:root_url] # 'svn+ssh://www.example.com/svn_repository'
67
+ #
68
+ # file = Tempfile.new('tmp', svn.svn_repo_working_copy).path
69
+ # begin
70
+ # svn.info(file)
71
+ # rescue SvnWc::RepoAccessError => e
72
+ # puts e.message.match(/is not under version control/)
73
+ # end
74
+ #
75
+ # svn.add file
76
+ # puts svn.commit file # returns the revision number of the commit
77
+ # puts svn.status file # ' ' empty string, file is current
78
+ #
79
+ # File.open(file, 'a') {|f| f.write('adding this to file.')}
80
+ # puts svn.status(file)[0][:status] # 'M' (modified)
81
+ # puts svn.info(file)[:rev] # current revision of file
82
+ #
83
+ # puts svn.diff(file) # =~ 'adding this to file.'
84
+ #
85
+ # svn.revert file # discard working copy changes, get current repo version
86
+ # svn.commit file # -1 i.e commit failed, file is current
87
+ #
88
+ # svn.delete file
89
+ # svn.commit file # must commit our delete
90
+ # puts "#{file} deleted' unless File.exists? file
91
+ #
92
+ # (In general also works with an Array of files)
93
+ # See test/* for more examples.
94
+ #
95
+ # See the README.rdoc for more
96
+ #
97
+ # Category:: Version Control System/SVN/Subversion Ruby Lib
98
+ # Package:: SvnWc::RepoAccess
99
+ # Author:: David V. Wright <david_v_wright@yahoo.com>
100
+ # License:: LGPL License
101
+ #
102
+ #
103
+ #--
104
+ # TODO make sure args are what is expected for all methods
105
+ # TODO props
106
+ # look into:
107
+ # #wc_status = infos.assoc(@wc_path).last
108
+ # #assert(wc_status.text_normal?)
109
+ # #assert(wc_status.entry.dir?)
110
+ # #assert(wc_status.entry.normal?)
111
+ # #ctx.prop_set(Svn::Core::PROP_IGNORE, file2, dir_path)
112
+ #++
113
+
114
+ module SvnWc
115
+
116
+ # (general) exception class raised on all errors
117
+ class RepoAccessError < RuntimeError ; end
118
+
119
+ class RepoAccess
120
+
121
+ VERSION = '0.0.1'
122
+
123
+ DEFAULT_CONF_FILE = File.join(File.dirname(File.dirname(\
124
+ File.expand_path(__FILE__))), 'svn_wc_conf.yaml')
125
+
126
+ # initialization
127
+ # three optional parameters
128
+ # 1. Path to yaml conf file (default used, if none specified)
129
+ # 2. Do a checkout from remote svn repo (usually, necessary with first
130
+ # time set up only)
131
+ # 3. Force. Overwrite anything that may be preventing a checkout
132
+
133
+ def initialize(conf=nil, checkout=false, force=false)
134
+ set_conf(conf)
135
+ do_checkout(force) if checkout == true
136
+
137
+ # instance var of out open repo session
138
+ @ctx = svn_session
139
+ end
140
+
141
+ #--
142
+ # TODO revist these
143
+ #++
144
+ attr_accessor :svn_user, :svn_pass, :svn_repo_master, \
145
+ :svn_repo_working_copy, :cur_file
146
+ attr_reader :ctx, :repos
147
+
148
+ def do_checkout(force=false)
149
+ ## do checkout if not exists at specified local path
150
+ if File.directory? @svn_repo_working_copy and not force
151
+ raise RepoAccessError, 'target local directory ' << \
152
+ "[#{@svn_repo_working_copy}] exists, please remove" << \
153
+ 'or specify another directory'
154
+ end
155
+ checkout
156
+ end
157
+
158
+ def set_conf(conf)
159
+ begin
160
+ conf = load_conf(conf)
161
+ @svn_user = conf['svn_user']
162
+ @svn_pass = conf['svn_pass']
163
+ @svn_repo_master = conf['svn_repo_master']
164
+ @svn_repo_working_copy = conf['svn_repo_working_copy']
165
+ @config_path = conf['svn_repo_config_path']
166
+ Svn::Core::Config.ensure(@config_path)
167
+ rescue Exception => e
168
+ raise RepoAccessError, 'errors loading conf file'
169
+ end
170
+ end
171
+
172
+ # checkout
173
+ #
174
+ # create a local working copy of a remote svn repo (creates dir if not
175
+ # exist)
176
+ # raises RepoAccessError if something goes wrong
177
+ #
178
+
179
+ def checkout
180
+ if not File.directory? @svn_repo_working_copy
181
+ begin
182
+ FileUtils.mkdir_p @svn_repo_working_copy
183
+ rescue Errno::EACCES => e
184
+ raise RepoAccessError, e.message
185
+ end
186
+ end
187
+
188
+ begin
189
+ svn_session() { |ctx|
190
+ ctx.checkout(@svn_repo_master, @svn_repo_working_copy)
191
+ }
192
+ rescue Svn::Error::RaLocalReposOpenFailed,
193
+ Svn::Error::FsAlreadyExists,
194
+ Exception => e
195
+ raise RepoAccessError, e.message
196
+ end
197
+ end
198
+ alias_method :co, :checkout
199
+
200
+ #
201
+ # load conf file (yaml)
202
+ #
203
+ # takes a path to a yaml config file, loads values. uses default if
204
+ # nothing passed
205
+ # raises RepoAccessError if something goes wrong
206
+ #
207
+ # private
208
+ #
209
+
210
+ def load_conf(cnf) # :nodoc:
211
+
212
+ if cnf.nil? or cnf.empty?
213
+ cnf = IO.read(DEFAULT_CONF_FILE)
214
+ elsif cnf and cnf.class == String and File.exists? cnf
215
+ cnf = IO.read(cnf)
216
+ end
217
+
218
+ begin
219
+ YAML::load(cnf)
220
+ rescue Exception => e
221
+ raise RepoAccessError, e.message
222
+ end
223
+ end
224
+ private :load_conf
225
+
226
+ #
227
+ # add entities to the repo
228
+ #
229
+ # pass a single entry or list of file(s) with fully qualified path,
230
+ # which must exist,
231
+ #
232
+ # raises RepoAccessError if something goes wrong
233
+ #
234
+
235
+ def add(files=[])
236
+
237
+ # TODO make sure args are what is expected for all methods
238
+ raise ArgumentError, 'files is empty' unless files
239
+
240
+ svn_session() do |svn|
241
+ begin
242
+ files.each { |ef|
243
+ svn.add(ef, true)
244
+ }
245
+ rescue Svn::Error::ENTRY_EXISTS,
246
+ Svn::Error::AuthnNoProvider,
247
+ Svn::Error::WcNotDirectory,
248
+ Svn::Error::SvnError => e
249
+ raise RepoAccessError, "Add Failed: #{e.message}"
250
+ end
251
+ end
252
+ end
253
+
254
+ #
255
+ # delete entities from the repository
256
+ #
257
+ # pass single entity or list of files with fully qualified path,
258
+ # which must exist,
259
+ #
260
+ # raises RepoAccessError if something goes wrong
261
+ #
262
+
263
+ def delete(files=[], recurs=nil)
264
+ svn_session() do |svn|
265
+ begin
266
+ svn.delete(files)
267
+ rescue Svn::Error::AuthnNoProvider,
268
+ Svn::Error::WcNotDirectory,
269
+ Svn::Error::ClientModified,
270
+ Svn::Error::SvnError => e
271
+ raise RepoAccessError, "Delete Failed: #{e.message}"
272
+ end
273
+ end
274
+ end
275
+ alias_method :rm, :delete
276
+
277
+
278
+ #
279
+ # commit entities to the repository
280
+ #
281
+ # params single or list of files (full relative path (to repo root) needed)
282
+ #
283
+ # optional message
284
+ #
285
+ # raises RepoAccessError if something goes wrong
286
+ # returns the revision of the commmit
287
+ #
288
+
289
+ def commit(files=[], msg='')
290
+ if files and files.empty? or files.nil? then files = self.svn_repo_working_copy end
291
+
292
+ rev = ''
293
+ svn_session(msg) do |svn|
294
+ begin
295
+ rev = svn.commit(files).revision
296
+ rescue Svn::Error::WcNotDirectory,
297
+ Svn::Error::AuthnNoProvider,
298
+ Svn::Error::IllegalTarget,
299
+ Svn::Error::EntryNotFound => e
300
+ raise RepoAccessError, "Commit Failed: #{e.message}"
301
+ end
302
+ end
303
+ rev
304
+ end
305
+ alias_method :ci, :commit
306
+
307
+ #
308
+ # update local working copy with most recent (remote) repo version
309
+ # (does not resolve conflict - or alert or anything at the moment)
310
+ #
311
+ # if nothing passed, does repo root
312
+ #
313
+ # params optional:
314
+ # single or list of files (full relative path (to repo root) needed)
315
+ #
316
+ # raises RepoAccessError if something goes wrong
317
+ #
318
+ # alias up
319
+ #--
320
+ # XXX refactor this (too long)
321
+ #++
322
+ def update(paths=[])
323
+
324
+ if paths.empty? then paths = self.svn_repo_working_copy end
325
+ #XXX update is a bummer, just returns the rev num, not affected files
326
+ #(svn command line up, also returns altered/new files - mimic that)
327
+ # hence our inplace hack
328
+ #
329
+ # unfortunetly, we cant use 'Repos', only works on local filesystem repo
330
+ # (NOT remote)
331
+ #p Svn::Repos.open(@svn_repo_master) # Svn::Repos.open('/tmp/svnrepo')
332
+
333
+ pre_up_entries = Array.new
334
+ modified_entries = Array.new
335
+ list_entries.each { |ent|
336
+ ##puts "#{ent[:status]} | #{ent[:repo_rev]} | #{ent[:entry_name]}"
337
+ pre_up_entries.push ent[:entry_name]
338
+ ## how does it handle deletes?
339
+ #if info()[:rev] != ent[:repo_rev]
340
+ # puts "changed file: #{File.join(paths, ent[:entry_name])} | #{ent[:status]} "
341
+ #end
342
+ if ent[:status] == 'M'
343
+ modified_entries.push "#{ent[:status]}\t#{ent[:entry_name]}"
344
+ end
345
+ }
346
+
347
+ rev = String.new
348
+ svn_session() do |svn|
349
+ begin
350
+ #p svn.status paths
351
+ rev = svn.update(paths, nil, 'infinity')
352
+ rescue Svn::Error::WcNotDirectory,
353
+ Svn::Error::AuthnNoProvider, #Svn::Error::FS_NO_SUCH_REVISION,
354
+ Svn::Error::EntryNotFound => e
355
+ raise RepoAccessError, "Update Failed: #{e.message}"
356
+ end
357
+ end
358
+
359
+ post_up_entries = Array.new
360
+ list_entries.each { |ent| post_up_entries.push ent[:entry_name] }
361
+
362
+ added = post_up_entries - pre_up_entries
363
+ removed = pre_up_entries - post_up_entries
364
+
365
+ if added.length > 0 ;
366
+ added.each {|e| modified_entries.push "A\t#{e}" }
367
+ end
368
+
369
+ if removed.length > 0
370
+ removed.each {|e| modified_entries.push "D\t#{e}" }
371
+ end
372
+
373
+ return rev, modified_entries
374
+
375
+ end
376
+ alias_method :up, :update
377
+
378
+ #
379
+ # get status on dir/file path.
380
+ #
381
+ # if nothing passed, does repo root
382
+ #
383
+ #--
384
+ # TODO/XXX add optional param to return results as a data structure
385
+ # (current behavior)
386
+ # or as a puts 'M' File (like the CLI version, have the latter as the
387
+ # default, this avoids the awkward s.status(file)[0][:status] notation
388
+ # one could just say: s.status file and get the list displayed on stdout
389
+ #++
390
+ def status(path='')
391
+
392
+ raise ArgumentError, 'path not a String' if ! (path or path.class == String)
393
+
394
+ if path and path.empty? then path = self.svn_repo_working_copy end
395
+
396
+ status_info = Hash.new
397
+
398
+ if File.file?(path)
399
+ # is single file path
400
+ file = path
401
+ status_info = do_status(File.dirname(path), file)
402
+ elsif File.directory?(path)
403
+ status_info = do_status(path)
404
+ else
405
+ raise RepoAccessError, "Arg is not a file or directory"
406
+ end
407
+ status_info
408
+
409
+ end
410
+ alias_method :stat, :status
411
+
412
+
413
+ #
414
+ # get status of all entries at (passed) dir level in repo
415
+ # use repo root if not specified
416
+ #
417
+ # private does the real work for 'status'
418
+ #
419
+ # @params [String] optional params, defaults to repo root
420
+ # if file passed, get specifics on file, else get
421
+ # into on all in dir path passed
422
+ # @returns [Hash] path/status of entries at dir level passed
423
+ #
424
+
425
+ def do_status(dir=self.svn_repo_working_copy, file=nil) # :nodoc:
426
+
427
+ wc_path = Svn::Core.path_canonicalize dir if File.directory? dir
428
+
429
+ wc_path = Svn::Core.path_canonicalize file \
430
+ if (!file.nil? && File.file?(file))
431
+
432
+ infos = Array.new
433
+ svn_session() do |svn|
434
+ begin
435
+ # from client.rb
436
+ rev = svn.status(wc_path, rev=nil, depth_or_recurse='infinity',
437
+ get_all=true, update=true, no_ignore=false,
438
+ changelists_name=nil #, &status_func
439
+ ) do |path, status|
440
+ infos << [path, status]
441
+ end
442
+ rescue Svn::Error::WcNotDirectory,
443
+ RuntimeError => svn_err
444
+ raise RepoAccessError, "status check Failed: #{svn_err}"
445
+ end
446
+ end
447
+
448
+ files = Array.new
449
+ infos.each {|r|
450
+ #p r.inspect
451
+ # file is not modified, we don't want to see it (this is 'status')
452
+ next if ' ' == status_codes(r[1].text_status)
453
+ f_rec = Hash.new
454
+ f_rec[:path] = r[0]
455
+ f_rec[:status] = status_codes(r[1].text_status)
456
+ files.push f_rec
457
+ }
458
+
459
+ files
460
+
461
+ end
462
+ private :do_status
463
+
464
+ #
465
+ # list (ls)
466
+ #
467
+ # list all entries at (passed) dir level in repo
468
+ # use repo root if not specified
469
+ #
470
+ # no repo/file info is returned, just a list of files, with abs_path
471
+ #
472
+ # optional
473
+ #
474
+ # @params [String] working copy directory, defaults to repo root
475
+ # if dir passed, get list for dir, else
476
+ # repo_root
477
+ #
478
+ # @params [String] revision, defaults to 'head' (others untested)
479
+ #
480
+ # @params [String] verbose, not currently enabled
481
+ #
482
+ # @params [String] depth of list, default, 'infinity', (whole repo)
483
+ # (read the Doxygen docs for possible values - sorry)
484
+ #
485
+ # @returns [Array] list of entries at dir level passed
486
+ #
487
+
488
+ def list(wc_path=self.svn_repo_working_copy, rev='head',
489
+ verbose=nil, depth='infinity')
490
+ paths = []
491
+ svn_session() do |svn|
492
+
493
+ begin
494
+ svn.list(wc_path, rev, verbose, depth) do |path, dirent, lock, abs_path|
495
+ #paths.push(path.empty? ? abs_path : File.join(abs_path, path))
496
+ f_rec = Hash.new
497
+ f_rec[:entry] = (path.empty? ? abs_path : File.join(abs_path, path))
498
+ f_rec[:last_changed_rev] = dirent.created_rev
499
+ paths.push f_rec
500
+ end
501
+ rescue Svn::Error::WcNotDirectory,
502
+ Svn::Error::AuthnNoProvider,
503
+ Svn::Error::FS_NO_SUCH_REVISION,
504
+ Svn::Error::EntryNotFound => e
505
+ raise RepoAccessError, "List Failed: #{e.message}"
506
+ end
507
+ end
508
+
509
+ paths
510
+
511
+ end
512
+ alias_method :ls, :list
513
+
514
+ #--
515
+ # TODO what is this? look into, revisit
516
+ #entr = svn.ls(paths,'HEAD')
517
+ #entr.each {|ent|
518
+ # ent.each {|k,dir_e|
519
+ # next unless dir_e.class == Svn::Ext::Core::Svn_dirent_t
520
+ # puts "#{dir_e.kind} | #{dir_e.created_rev} | #{dir_e.time2} | #{dir_e.last_author} "
521
+ # #puts dir_e.public_methods
522
+ # #puts "#{k} -> #{v.kind} : #{v.created_rev}"
523
+ # }
524
+ #}
525
+ #++
526
+
527
+ # Get list of all entries at (passed) dir level in repo
528
+ # use repo root if nothing passed
529
+ #
530
+ # params [String, String, String] optional params, defaults to repo root
531
+ # if file passed, get specifics on file, else get
532
+ # into on all in dir path passed
533
+ # 3rd arg is verbose flag, if set to true, lot's
534
+ # more info is returned about the object
535
+ # returns [Array] list of entries in svn repository
536
+ #
537
+
538
+ def list_entries(dir=self.svn_repo_working_copy, file=nil, verbose=false)
539
+ @entry_list = []
540
+ show = true
541
+ Svn::Wc::AdmAccess.open(nil, dir, false, 5) do |adm|
542
+ if file.nil?
543
+ #also see walk_entries (in svn bindings) has callback
544
+ adm.read_entries.keys.sort.each { |ef|
545
+ next unless ef.length >= 1 # why this check and not file.exists?
546
+ f_path = File.join(dir, ef)
547
+ if File.file? f_path
548
+ _collect_get_entry_info(f_path, adm, show, verbose)
549
+ elsif File.directory? f_path
550
+ _walk_entries(f_path, adm, show, verbose)
551
+ end
552
+ }
553
+ else
554
+ _collect_get_entry_info(file, adm, show, verbose)
555
+ end
556
+ end
557
+ @entry_list
558
+ end
559
+
560
+ #
561
+ # private
562
+ #
563
+ # given a dir, iterate each entry, getting detailed file entry info
564
+ #
565
+
566
+ def _walk_entries(f_path, adm, show, verbose)#:nodoc:
567
+ Dir.entries(f_path).each do |de|
568
+ next if de == '..' or de == '.' or de == '.svn'
569
+ fp_path = File.join(f_path, de)
570
+ _collect_get_entry_info(fp_path, adm, show, verbose)
571
+ end
572
+ end
573
+ private :_walk_entries
574
+
575
+
576
+ #
577
+ # private
578
+ #
579
+ # _collect_get_entry_info - initialize empty class varialbe
580
+ # @status_info to keep track of entries, push that onto
581
+ # class variable @entry_list a hash of very useful svn info of each entry
582
+ # requested
583
+ #
584
+
585
+ def _collect_get_entry_info(abs_path_file, adm, show, verbose=false)#:nodoc:
586
+ @status_info = {}
587
+ _get_entry_info(abs_path_file, adm, show, verbose)
588
+ @entry_list.push @status_info
589
+ end
590
+ private :_collect_get_entry_info
591
+
592
+ #
593
+ # private
594
+ #
595
+ # _get_entry_info - set's class varialbe @status_info (hash)
596
+ # with very useful svn info of each entry requested
597
+ # needs an Svn::Wc::AdmAccess token to obtain detailed repo info
598
+ #
599
+ # NOTE: just does one entry at a time, set's a hash of that one
600
+ # entries svn info
601
+ #--
602
+ # TODO - document all the params available from this command
603
+ #++
604
+
605
+ def _get_entry_info(abs_path_file, adm, show, verbose=false) # :nodoc:
606
+ wc = self.svn_repo_working_copy
607
+ entry_repo_location = abs_path_file[(wc.length+1)..-1]
608
+
609
+ entry = Svn::Wc::Entry.new(abs_path_file, adm, show)
610
+ @status_info[:entry_name] = entry_repo_location
611
+
612
+ status = adm.status(abs_path_file)
613
+ return if status.entry.nil?
614
+
615
+ @status_info[:status] = status_codes(status.text_status)
616
+ @status_info[:repo_rev] = status.entry.revision
617
+ @status_info[:kind] = status.entry.kind
618
+
619
+ if @status_info[:kind] == 2
620
+ # remove the repo root abs path, give dirs relative to repo root
621
+ @status_info[:dir_name] = entry_repo_location
622
+ # XXX hmmm, this is a little like a goto, revisit this
623
+ _walk_entries(abs_path_file, adm, show, verbose)
624
+ end
625
+ return if verbose == false
626
+ # only on demand ; i.e. verbose = true
627
+ @status_info[:lock_creation_date] = status.entry.lock_creation_date
628
+ @status_info[:entry_conflict] = entry.conflicted?(abs_path_file)
629
+ @status_info[:present_props] = status.entry.present_props
630
+ @status_info[:has_prop_mods] = status.entry.has_prop_mods
631
+ @status_info[:copyfrom_url] = status.entry.copyfrom_url
632
+ @status_info[:conflict_old] = status.entry.conflict_old
633
+ @status_info[:conflict_new] = status.entry.conflict_new
634
+ @status_info[:lock_comment] = status.entry.lock_comment
635
+ @status_info[:copyfrom_rev] = status.entry.copyfrom_rev
636
+ @status_info[:working_size] = status.entry.working_size
637
+ @status_info[:conflict_wrk] = status.entry.conflict_wrk
638
+ @status_info[:cmt_author] = status.entry.cmt_author
639
+ @status_info[:changelist] = status.entry.changelist
640
+ @status_info[:lock_token] = status.entry.lock_token
641
+ @status_info[:keep_local] = status.entry.keep_local
642
+ @status_info[:lock_owner] = status.entry.lock_owner
643
+ @status_info[:prop_time] = status.entry.prop_time
644
+ @status_info[:has_props] = status.entry.has_props
645
+ @status_info[:schedule] = status.entry.schedule
646
+ @status_info[:text_time] = status.entry.text_time
647
+ @status_info[:revision] = status.entry.revision
648
+ @status_info[:checksum] = status.entry.checksum
649
+ @status_info[:cmt_date] = status.entry.cmt_date
650
+ @status_info[:prejfile] = status.entry.prejfile
651
+ @status_info[:is_file] = status.entry.file?
652
+ @status_info[:normal?] = status.entry.normal?
653
+ @status_info[:cmt_rev] = status.entry.cmt_rev
654
+ @status_info[:deleted] = status.entry.deleted
655
+ @status_info[:absent] = status.entry.absent
656
+ @status_info[:is_add] = status.entry.add?
657
+ @status_info[:is_dir] = status.entry.dir?
658
+ @status_info[:repos] = status.entry.repos
659
+ @status_info[:depth] = status.entry.depth
660
+ @status_info[:uuid] = status.entry.uuid
661
+ @status_info[:url] = status.entry.url
662
+ end
663
+ private :_get_entry_info
664
+
665
+ # get detailed repository info about a specific file or (by default)
666
+ # the entire repository
667
+ #--
668
+ # TODO - document all the params available from this command
669
+ #++
670
+ #
671
+ def info(file='')
672
+ if file and not (file.empty? or file.nil? or file.class != String)
673
+ wc_path = file
674
+ else
675
+ wc_path = self.svn_repo_working_copy
676
+ end
677
+
678
+ r_info = {}
679
+ begin
680
+ @ctx.info(wc_path) do |path, type|
681
+ r_info[:last_changed_author] = type.last_changed_author
682
+ r_info[:last_changed_rev] = type.last_changed_rev
683
+ r_info[:last_changed_date] = type.last_changed_date
684
+ r_info[:conflict_old] = type.conflict_old
685
+ r_info[:tree_conflict] = type.tree_conflict
686
+ r_info[:repos_root_url] = type.repos_root_url
687
+ r_info[:repos_root_URL] = type.repos_root_URL
688
+ r_info[:copyfrom_rev] = type.copyfrom_rev
689
+ r_info[:copyfrom_url] = type.copyfrom_url
690
+ r_info[:working_size] = type.working_size
691
+ r_info[:conflict_wrk] = type.conflict_wrk
692
+ r_info[:conflict_new] = type.conflict_new
693
+ r_info[:has_wc_info] = type.has_wc_info
694
+ r_info[:repos_UUID] = type.repos_UUID
695
+ r_info[:changelist] = type.changelist
696
+ r_info[:prop_time] = type.prop_time
697
+ r_info[:text_time] = type.text_time
698
+ r_info[:checksum] = type.checksum
699
+ r_info[:prejfile] = type.prejfile
700
+ r_info[:schedule] = type.schedule
701
+ r_info[:taguri] = type.taguri
702
+ r_info[:depth] = type.depth
703
+ r_info[:lock] = type.lock
704
+ r_info[:size] = type.size
705
+ r_info[:url] = type.url
706
+ r_info[:dup] = type.dup
707
+ r_info[:URL] = type.URL
708
+ r_info[:rev] = type.rev
709
+ end
710
+ rescue Svn::Error::EntryNotFound,
711
+ Svn::Error::RaIllegalUrl,
712
+ Svn::Error::WcNotDirectory => e
713
+ raise RepoAccessError, "cant get info: #{e.message}"
714
+ end
715
+ r_info
716
+
717
+ end
718
+
719
+
720
+ #--
721
+ # this is a good idea but the mapping implementation is crappy,
722
+ # the svn SWIG bindings *could* (although, unlikly?) change,
723
+ # in which case this mapping would be wrong
724
+ #
725
+ # TODO get the real status message, (i.e. 'none', modified, etc)
726
+ # and map that to the convenience single character i.e. A, M, ?
727
+ #--
728
+ def status_codes(status) # :nodoc:
729
+ if status == 0 ; raise RepoAccessError, 'Zero Status Unknown' ; end
730
+ status -= 1
731
+ # See this
732
+ #http://svn.collab.net/svn-doxygen/svn__wc_8h-source.html#l03422
733
+ #enum svn_wc_status_kind
734
+ #++
735
+ status_map = [
736
+ ' ', #"svn_wc_status_none" => 1,
737
+ '?', #"svn_wc_status_unversioned" => 2,
738
+ ' ', #"svn_wc_status_normal" => 3,
739
+ 'A', #"svn_wc_status_added" => 4,
740
+ '!', #"svn_wc_status_missing" => 5,
741
+ 'D', #"svn_wc_status_deleted" => 6,
742
+ 'R', #"svn_wc_status_replaced" => 7,
743
+ 'M', #"svn_wc_status_modified" => 8,
744
+ 'G', #"svn_wc_status_merged" => 9,
745
+ 'C', #"svn_wc_status_conflicted" => 10,
746
+ 'I', #"svn_wc_status_ignored" => 11,
747
+ '~', #"svn_wc_status_obstructed" => 12,
748
+ 'X', #"svn_wc_status_external" => 13,
749
+ '!', #"svn_wc_status_incomplete" => 14
750
+ ]
751
+ status_map[status]
752
+ end
753
+ private :status_codes
754
+
755
+ # discard working copy changes, get current repository entry
756
+ def revert(file_path='')
757
+ if file_path.empty? then file_path = self.svn_repo_working_copy end
758
+ svn_session() { |svn| svn.revert(file_path) }
759
+ end
760
+
761
+ # By Default compares current working directory file with 'HEAD' in
762
+ # repository (NOTE: does not yet support diff to previous revisions)
763
+ #--
764
+ # TODO support diffing previous revisions
765
+ #++
766
+ def diff(file='', rev1='', rev2='')
767
+ raise ArgumentError, 'file list empty or nil' unless file and file.size
768
+
769
+ raise RepoAccessError, "Diff requires an absolute path to a file" \
770
+ unless File.exists? file
771
+
772
+ # can also use new (updated) svn.status(f)[0][:repo_rev]
773
+ rev = info(file)[:rev]
774
+ out_file = Tempfile.new("svn_diff")
775
+ err_file = Tempfile.new("svn_diff")
776
+ svn_session() do |svn|
777
+ begin
778
+ svn.diff([], file, rev, file, "WORKING", out_file.path, err_file.path)
779
+ rescue Svn::Error::EntryNotFound => e
780
+ raise RepoAccessError, "Diff Failed: #{e.message}"
781
+ end
782
+ end
783
+ out_file.readlines
784
+ end
785
+
786
+ # svn session set up
787
+ #--
788
+ # from
789
+ # http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/ruby/test/util.rb
790
+ #++
791
+ def svn_session(commit_msg = String.new) # :nodoc:
792
+ ctx = Svn::Client::Context.new
793
+
794
+ # Function for commit messages
795
+ ctx.set_log_msg_func do |items|
796
+ [true, commit_msg]
797
+ end
798
+
799
+ # don't fail on non CA signed ssl server
800
+ ctx.add_ssl_server_trust_file_provider
801
+
802
+ setup_auth_baton(ctx.auth_baton)
803
+ ctx.add_username_provider
804
+
805
+ # username and password
806
+ ctx.add_simple_prompt_provider(0) do |cred, realm, username, may_save|
807
+ cred.username = @svn_user
808
+ cred.password = @svn_pass
809
+ cred.may_save = false
810
+ end
811
+
812
+ return ctx unless block_given?
813
+
814
+ begin
815
+ yield ctx
816
+ #ensure
817
+ # warning!?
818
+ # ctx.destroy
819
+ end
820
+ end
821
+
822
+ def setup_auth_baton(auth_baton) # :nodoc:
823
+ auth_baton[Svn::Core::AUTH_PARAM_CONFIG_DIR] = @config_path
824
+ auth_baton[Svn::Core::AUTH_PARAM_DEFAULT_USERNAME] = @svn_user
825
+ end
826
+
827
+ end
828
+
829
+ end
830
+
831
+ if __FILE__ == $0
832
+
833
+ svn = SvnWc::RepoAccess.new
834
+ p svn
835
+ #n = '/tmp/NEW'
836
+ #svn.add n
837
+ #svn.commit n
838
+
839
+ end
840
+