svn-transform 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,264 @@
1
+ require 'pathname'
2
+ require 'svn-fixture'
3
+
4
+ class SvnTransform
5
+ VERSION = '0.1.0'
6
+
7
+ class << self
8
+ # Use diff to compare two repositories (on local file system)
9
+ # Where reasonable, this (or something like it) should be run to verify
10
+ # expected results. I recommend trying a direct copy first to ensure your
11
+ # original repo doesn't have any featurs that SvnPropsToYaml won't
12
+ # understand.
13
+ #
14
+ # http://www.coderetard.com/2009/02/17/compare-directories-and-file-content-in-linux-without-dircmp/
15
+ #
16
+ # ==== Gotchas
17
+ # svn:entry fields aren't directly copied, but seem to match.
18
+ # repo uuid is different, but not relevant, so that file is ignored.
19
+ # other differences may also exist if the in_repo is an older format.
20
+ #
21
+ # ==== Parameters
22
+ # old_dir<String>:: FS Path to original (in) repository.
23
+ # new_dir<String>:: FS Path to generated (out) repository.
24
+ #
25
+ # Note that these are filesystem paths, not Subversion URI's
26
+ #
27
+ # ==== Returns
28
+ # True, False::
29
+ # Whether the directories are the same (except db/uuid file)
30
+ # If False, puts the result of running the diff command.
31
+ def compare(old_dir, new_dir)
32
+ ret = `diff --brief --exclude=uuid -r "#{old_dir}" "#{new_dir}"`
33
+ if ret.empty?
34
+ return true
35
+ else
36
+ puts ret
37
+ return false
38
+ end
39
+ end
40
+ end
41
+
42
+ # Setup and SvnTransform with in (existing) repository URI, a name for the
43
+ # out (transformed) repository, and options.
44
+ #
45
+ # ==== Parameters
46
+ # in_repo_uri<String>::
47
+ # URI of existing repository(e.g. file:///home/jm81/repo,
48
+ # svn://localhost/repo)
49
+ # out_repo_name<String>::
50
+ # Name only of out repository (e.g. "out"). See options[:out_repos_path]
51
+ # for specifying full path (on local filesystem only)
52
+ #
53
+ # ==== Options
54
+ # :username<String>:: Username for in (existing) repository
55
+ # :password<String>:: Password for in (existing) repository
56
+ # :out_repos_path<String>::
57
+ # Full path for out repository (defaults to
58
+ # "#{SvnFixture.config[:base_path]}/repo_#{out_repo_name}")
59
+ # :out_wc_path<String>::
60
+ # Full path for out working copy (used by SvnFixture; defaults to
61
+ # "#{SvnFixture.config[:base_path]}/wc_#{out_repo_name}")
62
+ def initialize(in_repo_uri, out_repo_name = nil, options = {})
63
+ @in_username = options[:username]
64
+ @in_password = options[:password]
65
+ @in_repo_uri = in_repo_uri
66
+ @out_repo_name = out_repo_name
67
+ @out_repos_path = options[:out_repos_path]
68
+ @out_wc_path = options[:out_wc_path]
69
+ @file_transforms = []
70
+ @dir_transforms = []
71
+ end
72
+
73
+ # Add a transform to be run on files. This can either be a class or a block
74
+ # (see Parameters). Each file at revision is given as an SvnTransform::File
75
+ # to each transform, which can alter the basename, body and/or properties
76
+ # of the file prior to its being committed to the new Repository.
77
+ #
78
+ # ==== Parameters
79
+ # klass<Class>::
80
+ # A class whose #initialize method accepts a SvnTransform::File as the first
81
+ # argument and which responds to #run.
82
+ # args<Array>::
83
+ # Additional arguments to pass to klass#initialize
84
+ # block<Proc>::
85
+ # A block that accepts one argument (a SvnTransform::File). If a klass is
86
+ # also given, the block is ignored
87
+ #
88
+ # ==== Returns
89
+ # Array:: The current @file_transforms Array
90
+ #
91
+ # ==== Raises
92
+ # ArgumentError:: Neither a Class nor a block was given.
93
+ def file_transform(klass = nil, *args, &block)
94
+ if klass
95
+ @file_transforms << [klass, args]
96
+ elsif block_given?
97
+ @file_transforms << block
98
+ else
99
+ raise(ArgumentError, "Class or Block required")
100
+ end
101
+ end
102
+
103
+ # Add a transform to be run on directories. See +file_transform+
104
+ def dir_transform(klass = nil, *args, &block)
105
+ if klass
106
+ @dir_transforms << [klass, args]
107
+ elsif block_given?
108
+ @dir_transforms << block
109
+ else
110
+ raise(ArgumentError, "Class or Block required")
111
+ end
112
+ end
113
+
114
+ # Run the conversion. This method sets up the connection to the existing
115
+ # repo and the SvnFixture that will generate the final transformed repo, then
116
+ # calls +changesets+ to do the actual work. Finally, commit the SvnFixture
117
+ # (out repo) and update its rev 0 date to match the in repo
118
+ def convert
119
+ in_repo_session = Session.new(@in_repo_uri, @in_username, @out_username)
120
+ @in_repo = in_repo_session.session
121
+ @ctx = in_repo_session.context
122
+ @out_repo = SvnFixture.repo(@out_repo_name, @out_repos_path, @out_wc_path)
123
+
124
+ # Process changesets and commit
125
+ changesets
126
+ @out_repo.commit
127
+
128
+ # Update rev 0 date
129
+ r0_date = @ctx.revprop_list(@in_repo_uri, 0)[0]['svn:date']
130
+ @out_repo.repos.fs.set_prop('svn:date', SvnFixture.svn_time(r0_date), 0)
131
+ end
132
+
133
+ # Process the existing changesets and generate a SvnFixture::Revision for
134
+ # each.
135
+ #
136
+ # TODO This is a massive mess. It works, at least for my purposes. But it is
137
+ # a mess. Ideally, it should be multiple methods. Part of this is due to how
138
+ # I set up the SvnFixture::Revision class, which accepts a block at initialize
139
+ # that is process only when its #commit method is called.
140
+ def changesets
141
+ args = ['', 1, @in_repo.latest_revnum, 0, true, nil]
142
+ path_renames = {}
143
+
144
+ @in_repo.log(*args) do |changes, rev_num, author, date, msg|
145
+ # Sort so that files are processed first (for benefit of PropsToYaml),
146
+ # and deletes are last
147
+ changes = changes.sort { |a,b| sort_for(a, rev_num) <=> sort_for(b, rev_num) }
148
+ # Get revision properties
149
+ rev_props = @ctx.revprop_list(@in_repo_uri, rev_num)[0]
150
+ # Create Revision, including all revprops. Note that svn:author and
151
+ # svn:date are revprops. SvnFixture::Revision allows these without the
152
+ # svn: prefix (as Symbol), but revprops are written last, and so this
153
+ # should be completely accurate.
154
+ in_repo = @in_repo
155
+ out_wc_path = @out_repo.wc_path
156
+ svn_transform = self
157
+ @out_repo.revision(rev_num, msg, rev_props) do
158
+ # Now go through all the changes. Setup directorie structure for each
159
+ # node. This is easier to understand, in my opinion.
160
+
161
+ changes.each do |full_path, change|
162
+ full_path = Pathname.new(full_path.sub(/\A\//, ''))
163
+ # Descend to parent directory
164
+ parent_dir = self
165
+ full_path.dirname.descend do |path|
166
+ unless path.basename == '.'
167
+ parent_dir = parent_dir.dir(path.basename.to_s)
168
+ end
169
+ end
170
+
171
+ # TODO Replaces
172
+
173
+ if change.action == 'D'
174
+ del_path = path_renames['/' + full_path.to_s] || full_path.to_s
175
+ @ctx.delete(::File.join(out_wc_path, del_path))
176
+ elsif in_repo.stat(full_path.to_s, rev_num).file?
177
+ data = in_repo.file(full_path.to_s, rev_num)
178
+ transform_file = ::SvnTransform::File.new(full_path, data, rev_num, rev_props)
179
+ original_path = transform_file.path
180
+ svn_transform.__send__(:process_file_transforms, transform_file)
181
+
182
+ if change.copyfrom_path
183
+ from_path = path_renames[change.copyfrom_path] || change.copyfrom_path
184
+ @ctx.cp(
185
+ ::File.join(out_wc_path, from_path),
186
+ ::File.join(out_wc_path, transform_file.path.to_s)
187
+ )
188
+ end
189
+
190
+ unless transform_file.skip?
191
+ parent_dir.file(transform_file.basename) do
192
+ body(transform_file.body)
193
+ transform_file.properties.each_pair do |prop_k, prop_v|
194
+ prop(prop_k, prop_v) unless prop_k =~ /\Asvn:entry/
195
+ end
196
+ end
197
+ # For benefit of copies
198
+ if original_path != transform_file.path
199
+ path_renames['/' + original_path] = '/' + transform_file.path.to_s
200
+ end
201
+ end
202
+ else # directory
203
+ parent_dir.dir(full_path.basename.to_s) do
204
+ data = in_repo.dir(full_path.to_s, rev_num)
205
+ transform_dir = ::SvnTransform::Dir.new(full_path, data, rev_num, rev_props, in_repo, self)
206
+ svn_transform.__send__(:process_dir_transforms, transform_dir)
207
+ transform_dir.properties.each_pair do |prop_k, prop_v|
208
+ prop(prop_k, prop_v) unless prop_k =~ /\Asvn:entry/
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ # Process @file_transforms against the given File
220
+ #
221
+ # ==== Parameters
222
+ # file<SvnTransform::File>:: A file in the original repo at a given revision
223
+ def process_file_transforms(file)
224
+ @file_transforms.each do |transform|
225
+ if transform.is_a?(Proc)
226
+ transform.call(file)
227
+ else
228
+ transform[0].new(file, *transform[1]).run
229
+ end
230
+ end
231
+ end
232
+
233
+ # Process @dir_transforms against the given Dir
234
+ #
235
+ # ==== Parameters
236
+ # dir<SvnTransform::Dir>:: A directory in the original repo at a given revision
237
+ def process_dir_transforms(dir)
238
+ @dir_transforms.each do |transform|
239
+ if transform.is_a?(Proc)
240
+ transform.call(dir)
241
+ else
242
+ transform[0].new(dir, *transform[1]).run
243
+ end
244
+ end
245
+ end
246
+
247
+ # Return an Integer such that file changes are first, directories second
248
+ # and deleted nodes last (to minimize the chance of a Transformation being
249
+ # overridden.
250
+ def sort_for(change, rev_num)
251
+ return 2 if change[1].action == 'D'
252
+ return 0 if @in_repo.stat(change[0].sub(/\A\//, ''), rev_num).file?
253
+ return 1
254
+ end
255
+ end # SvnTransform
256
+
257
+ require 'svn-transform/session'
258
+ require 'svn-transform/file'
259
+ require 'svn-transform/dir'
260
+
261
+ # Require predefined transforms
262
+ %w{extension newline noop props_to_yaml}.each do |filename|
263
+ require 'svn-transform/transform/' + filename
264
+ end
@@ -0,0 +1,65 @@
1
+ class SvnTransform
2
+ # A directory in original Subversion Repository, at a given changeset.
3
+ # Instances are initialized by SvnTransform#changesets.
4
+ #
5
+ # An instance for each file in the original repo at each revision will be
6
+ # passed to any directory transform blocks (TODO more info)
7
+ #
8
+ # Although more could theoretically be done (see #initialize fixture_dir
9
+ # param), the main thing intended to be alterable are the properties.
10
+ class Dir
11
+ # Initialize Dir instance using data passed by SvnTransform#changesets.
12
+ # This is data that will be available to Transformation blocks. It's
13
+ # relevant to remember that all this happens within a block given to an
14
+ # SvnFixture::Revision.
15
+ #
16
+ # ==== Parameters
17
+ # path<Pathname>::
18
+ # Full path within original Repository
19
+ # node_data<Array[String, Hash]>::
20
+ # Array returned by SWIG::TYPE_p_svn_ra_session_t#dir. First element is
21
+ # a Hash of directory entries, second is Hash of properties.
22
+ # rev_num<Integer>::
23
+ # Number of current revision
24
+ # rev_props<Hash>::
25
+ # Properties for current revision
26
+ # repos<Svn::Ra::Session>::
27
+ # Repo session (made available for PropsToYaml)
28
+ # fixture_dir<SvnFixture::Directory>::
29
+ # The SvnFixture::Directory representing this directory. This could be
30
+ # used to add, delete files, subdirs, etc, but doing much of that is
31
+ # likely to lead to weird results. I certainly don't intend to test this
32
+ # outside of one use case (PropsToYaml)
33
+ def initialize(path, node_data, rev_num, rev_props, repos, fixture_dir)
34
+ @path = path.kind_of?(Pathname) ? path : Pathname.new(path)
35
+ @entries = node_data[0]
36
+ @properties = node_data[1]
37
+ @rev_num = rev_num
38
+ @rev_props = rev_props
39
+ @repos = repos
40
+ @fixture_dir = fixture_dir
41
+ end
42
+
43
+ attr_reader :path, :entries, :properties, :rev_num, :rev_props, :repos, :fixture_dir
44
+
45
+ # Assign a new properties Hash to the node
46
+ #
47
+ # ==== Parameters
48
+ # hsh<~each_pair>::
49
+ # A Hash (or other object) responding to #each_pair, where keys are
50
+ # svn property keys, and values are the corresponding property values.
51
+ # This method does not verify that keys are "human-readable"
52
+ # (See http://svnbook.red-bean.com/en/1.0/ch07s02.html)
53
+ def properties=(hsh)
54
+ unless hsh.respond_to?(:each_pair)
55
+ raise ArgumentError, "Argument must respond to #each_pair, such as a Hash"
56
+ end
57
+ @properties = hsh
58
+ end
59
+
60
+ # Get the base of the File
61
+ def basename
62
+ @path.basename.to_s
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,79 @@
1
+ class SvnTransform
2
+ # A file in original Subversion Repository, at a given changeset. Instances
3
+ # are initialized by SvnTransform#changesets.
4
+ #
5
+ # An instance for each file in the original repo at each revision will be
6
+ # passed to any file transform blocks (TODO more info)
7
+ class File
8
+ # Initialize File instance using data passed by SvnTransform#changesets.
9
+ # This is data that will be available to Transformation blocks. It's
10
+ # relevant to remember that all this happens within a block given to an
11
+ # SvnFixture::Revision
12
+ #
13
+ # ==== Parameters
14
+ # path<Pathname>::
15
+ # Full path within original Repository
16
+ # node_data<Array[String, Hash]>::
17
+ # Array returned by SWIG::TYPE_p_svn_ra_session_t#file. First element is
18
+ # node body, second is hash of properties.
19
+ # rev_num<Integer>::
20
+ # Number of current revision
21
+ # rev_props<Hash>::
22
+ # Properties for current revision
23
+ def initialize(path, node_data, rev_num, rev_props)
24
+ @path = path.kind_of?(Pathname) ? path : Pathname.new(path)
25
+ @body = node_data[0]
26
+ @properties = node_data[1]
27
+ @rev_num = rev_num
28
+ @rev_props = rev_props
29
+ end
30
+
31
+ attr_reader :path, :body, :properties, :rev_num, :rev_props
32
+
33
+ # Change the body of the node. The new body will be placed in the new
34
+ # repository.
35
+ #
36
+ # ==== Parameters
37
+ # value<String>:: New body
38
+ attr_writer :body
39
+
40
+ # Assign a new properties Hash to the node
41
+ #
42
+ # ==== Parameters
43
+ # hsh<~each_pair>::
44
+ # A Hash (or other object) responding to #each_pair, where keys are
45
+ # svn property keys, and values are the corresponding property values.
46
+ # This method does not verify that keys are "human-readable"
47
+ # (See http://svnbook.red-bean.com/en/1.0/ch07s02.html)
48
+ def properties=(hsh)
49
+ unless hsh.respond_to?(:each_pair)
50
+ raise ArgumentError, "Argument must respond to #each_pair, such as a Hash"
51
+ end
52
+ @properties = hsh
53
+ end
54
+
55
+ # Get the base of the File
56
+ def basename
57
+ @path.basename.to_s
58
+ end
59
+
60
+ # Change the basename of the File. Alters the #path
61
+ #
62
+ # ==== Parameters
63
+ # val<String>:: New basename
64
+ def basename=(val)
65
+ @path = @path.dirname + val
66
+ end
67
+
68
+ # Skip this file at this revision (that is, don't commit it to new repo).
69
+ def skip!
70
+ @skip = true
71
+ end
72
+
73
+ # Whether this file should be skipped (not committed to new repo at this
74
+ # revision)
75
+ def skip?
76
+ @skip == true
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,62 @@
1
+ class SvnTransform
2
+ # A simplistic wrapper for Svn::Ra::Session. This takes care of setting up
3
+ # the context and callbacks as well as making the actual connection.
4
+ class Session
5
+ # Setup repository information
6
+ #
7
+ # ==== Parameters
8
+ # uri<String>:: URI of the repository (e.g. svn://example.com/repo)
9
+ # username<String>:: Username, if needed
10
+ # password<String>:: Password, if needed
11
+ def initialize(uri, username = nil, password = nil)
12
+ @uri = uri
13
+ @username = username
14
+ @password = password
15
+ end
16
+
17
+ # Open and return the actual Svn::Ra::Session
18
+ #
19
+ # ==== Returns
20
+ # Svn::Ra::Session:: A remote access session to a repository.
21
+ def session
22
+ # This will raise some error if connection fails for whatever reason.
23
+ # I don't currently see a reason to handle connection errors here, as I
24
+ # assume the best handling would be to raise another error.
25
+ @session ||= ::Svn::Ra::Session.open(@uri, {}, self.callbacks)
26
+ end
27
+
28
+ # Setup, if needed, and return the working context (I don't really
29
+ # understand all this, but it's required to work with the working copy).
30
+ #
31
+ # ==== Returns
32
+ # Svn::Client::Context:: Context for working with working copy
33
+ def context
34
+ @context || begin
35
+ # Client::Context, which paticularly holds an auth_baton.
36
+ @context = ::Svn::Client::Context.new
37
+ if @username && @password
38
+ # TODO: What if another provider type is needed? Is this plausible?
39
+ @context.add_simple_prompt_provider(0) do |cred, realm, username, may_save|
40
+ cred.username = @username
41
+ cred.password = @password
42
+ end
43
+ elsif URI.parse(@uri).scheme == "file"
44
+ @context.add_username_prompt_provider(0) do |cred, realm, username, may_save|
45
+ cred.username = @username || "ANON"
46
+ end
47
+ else
48
+ @context.auth_baton = ::Svn::Core::AuthBaton.new()
49
+ end
50
+ @context
51
+ end
52
+ end
53
+
54
+ # Setup callbacks for Svn::Ra::Session.open.
55
+ #
56
+ # ==== Returns
57
+ # Svn::Ra::Callbacks
58
+ def callbacks
59
+ ::Svn::Ra::Callbacks.new(context.auth_baton)
60
+ end
61
+ end
62
+ end