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/ChangeLog +2 -0
- data/LICENSE +165 -0
- data/Manifest +8 -0
- data/README.rdoc +312 -0
- data/lib/svn_wc.rb +840 -0
- data/svn_wc_conf.yaml +7 -0
- data/test/svn_wc_test.rb +673 -0
- metadata +61 -0
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
|
+
|