svn-transform 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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