File Coverage

blib/lib/SVN/Rami.pm
Criterion Covered Total %
statement 18 110 16.3
branch 2 26 7.6
condition n/a
subroutine 6 12 50.0
pod 0 7 0.0
total 26 155 16.7


line stmt bran cond sub pod time code
1             package SVN::Rami;
2            
3 2     2   213329 use 5.10.1;
  2         18  
4 2     2   11 use strict;
  2         4  
  2         43  
5 2     2   9 use warnings;
  2         3  
  2         63  
6            
7 2     2   13 use File::Basename;
  2         4  
  2         257  
8 2     2   14 use File::Path qw(make_path remove_tree);
  2         3  
  2         3237  
9             #use File::Spec; # Will use later for Windows file names.
10             #use Path::Class; # Will use later for Windows file names.
11            
12             # TODO: clearly define the words "version", "revision", "branch". (Maybe "version" isn't the right word to use.)
13             # TODO: sanitize version names: they should contain only \w and hyphen.
14            
15            
16             =head1 NAME
17            
18             SVN::Rami - Automates merging to multiple branches
19            
20             =cut
21            
22             our $VERSION = '0.202';
23            
24            
25             =head1 SYNOPSIS
26            
27             Used by the script L. This is not (yet) a stand-alone module.
28            
29             =head1 SUBROUTINES/METHODS
30            
31             There are currently no publicly available methods because
32             this module is only intended to be used by the script
33             L.
34            
35             =cut
36            
37             #
38             #Reads a two-column CSV file and converts it to key-value pairs.
39             #For example, if the file contains one line consisting of "a,b",
40             #this method returns a mapping of "a" to "b".
41             #The file is assumed to have no headers.
42             #
43             #BUG: the file must be formatted precisely right: even an empty
44             #line in the middle of the file will result in an incorrect mapping.
45             #
46             #TODO: use Text::CSV instead.
47             #
48             sub load_csv_as_map {
49 0     0 0 0 my $filename = shift;
50 0         0 local $/ = undef; # Slurp mode
51 0 0       0 open(my $handle, '<', $filename) or die "Could not read $filename\n";
52 0         0 $_ = <$handle>;
53 0         0 close $handle;
54 0         0 my @contents = split( /,|\R/ );
55            
56             # Hack: the first two items are column headings.
57 0         0 (undef, undef, my @result) = @contents;
58 0         0 return @result;
59             }
60            
61             =head1 AUTHOR
62            
63             Dan Richter, C<< >>
64            
65             =head1 BUGS
66            
67             Please report any bugs or feature requests to C, or through
68             the web interface at L. I will be notified, and then you'll
69             automatically be notified of progress on your bug as I make changes.
70            
71            
72            
73            
74             =head1 SUPPORT
75            
76             You can find documentation for this module with the perldoc command.
77            
78             perldoc SVN::Rami
79            
80            
81             You can also look for information at:
82            
83             =over 4
84            
85             =item * RT: CPAN's request tracker (report bugs here)
86            
87             L
88            
89             =item * CPAN Ratings
90            
91             L
92            
93             =item * Search CPAN
94            
95             L
96            
97             =back
98            
99             =head1 SEE ALSO
100            
101             SVK::Merge
102            
103             =head1 LICENSE AND COPYRIGHT
104            
105             This software is copyright (c) 2023 by Dan Richter.
106            
107             This is free software; you can redistribute it and/or modify it under
108             the same terms as the Perl 5 programming language system itself.
109            
110            
111             =cut
112            
113             #
114             # This utility function simply dumps data to a file.
115             # Example: write_file('foo.txt', 'Hello world') creates a file
116             # named foo.txt whose complete contents are "Hello world".
117             # Note that you can pass a multi-line string as the second argument.
118             #
119             sub write_file {
120 0     0 0 0 my $filename = shift;
121 0         0 my $contents = shift;
122            
123 0 0       0 open(my $handle, '>', $filename) or die "Could not write to $filename\n";
124 0         0 print $handle $contents;
125 0         0 close $handle;
126             }
127            
128             #
129             # Usage: find_revision($revision, %branch_to_url)
130             # Queries SVN for information about the revision.
131             # Returns a hash that maps 'branch' to the branch on which the revision
132             # was committed (e.g., 6.2) and that maps 'commit_message' to the
133             # comment the user wrote to explain the commit.
134             #
135             sub find_revision {
136 0     0 0 0 my $rev = shift;
137 0         0 my %branch_to_url = @_;
138 0         0 foreach my $branch (keys %branch_to_url) {
139 0         0 my $url = $branch_to_url{$branch};
140 0         0 $_ = `svn log -c $rev $url`;
141            
142             # If the revision was NOT on $branch, then the output
143             # will be a single line of dashes: ----------
144             #
145             # But if the revision WAS on $branch, then
146             # the output will be something like this:
147             # ------------------------------------------------------------------------
148             # r84187 | bob.smith | 2023-03-27 10:53:40 -0400 (Mon, 27 Mar 2023) | 4 lines
149             #
150             # Fixed memory leak
151             # ------------------------------------------------------------------------
152 0 0       0 if ( m/^-----+\R* # match a bunch of dashes followed by a newline.
153             ^r.*?lines?\R+ # match line: r84187 | badelman | ... | 4 lines
154             ^(.*?)\R+ # Match the commit message.
155             ^-------+\R*\Z # Match a bunch of dashes followed by a newline.
156             /msx ) {
157            
158             #if ( m/^-+\r?\n?.*?lines(\r?\n)+(.*?)\r?\n-+(\r?\n)*$/sx ) {
159 0         0 return ('branch'=>$branch, 'commit_message'=>$1 );
160             }
161             }
162            
163             # Revision not found. Return an empty hash.
164 0         0 return ();
165             }
166            
167            
168             # HACK: define these as essentially global for now.
169             our $repo = 'default'; # TODO: support more than one repo.
170             our $rami_home_dir = glob("~/.rami/repo/$repo"); # TODO: use File::HomeDir instead of ~/
171             our $conf_dir = "$rami_home_dir/conf";
172             our $temp_dir = "$rami_home_dir/temp";
173             our $commit_message_file = "$temp_dir/message.txt";
174             our $path_csv_file = "$conf_dir/paths.csv"; # Not currently used.
175             our $root_work_dir = "$rami_home_dir/work"; # Holds a subdirectory for each branch.
176            
177            
178             #=head2 rami_main
179             #
180             #The main module. Currently takes one argument,
181             #which is the SVN revision which shoulde be merged.
182             #
183             #=cut
184            
185             sub rami_main {
186 0     0 0 0 my $source_revision = shift;
187            
188 0         0 my $conf_dir = $SVN::Rami::conf_dir;
189 0         0 my $commit_message_file = $SVN::Rami::commit_message_file;
190 0         0 my $path_csv_file = $SVN::Rami::path_csv_file;
191            
192             #print "### $commit_message_file\n";
193            
194 0 0       0 die "Expected directory $conf_dir\n" unless -d $conf_dir;
195            
196             # my %branch_to_path_on_filesystem = load_csv_as_map($path_csv_file);
197            
198             # We need the list of branches to be in order.
199 0         0 my @branch_to_url_array = load_csv_as_map("$conf_dir/urls.csv");
200 0         0 my %branch_to_url = @branch_to_url_array;
201 0         0 my @branches = grep( !/^http/, @branch_to_url_array); # HACK: remove URLs, leaving only versions, IN ORDER!
202            
203 0         0 my %branch_to_path_on_filesystem = map { $_ => convert_branches_to_filesystem_paths($_) } @branches;
  0         0  
204            
205 0         0 my %revision_details = find_revision($source_revision, %branch_to_url);
206 0 0       0 if ( ! %revision_details ) {
207 0         0 die "Unable to find revision $source_revision\n";
208             }
209            
210 0         0 my $source_branch = $revision_details{'branch'};
211             # The revision comment. We will later append something like "merge r 123 from 2.2.2"
212 0         0 my $base_commit_message = $revision_details{'commit_message'};
213 0         0 $base_commit_message =~ s/(\r\n\R\s)+$//g; # Remove trailing whitespace/newlines.
214            
215            
216 0         0 my $found_branch = 0; # Will be true when we loop to the source branch.
217             #my $previous_branch = '';
218 0         0 foreach my $target_branch (@branches) {
219 0 0       0 if ($target_branch eq $source_branch) {
    0          
220 0         0 $found_branch = 1;
221             # Perhaps instead of if-else we could use "redo", which is similar to Java's "continue"
222             } elsif ( $found_branch ) { # If we found the branch last time...
223 0         0 print "------------------------ Merging r $source_revision from $source_branch to $target_branch\n";
224 0         0 my $source_url = $branch_to_url{$source_branch};
225 0         0 my $working_dir = $branch_to_path_on_filesystem{$target_branch};
226            
227             # Get our working directory to the correct revision.
228 0         0 chdir($working_dir);
229 0         0 system("svn revert -R ."); # or die "Failed to revert $working_dir";
230 0         0 system("svn up"); # or die "Failed to update $working_dir";
231            
232             # Write the commit message to a file.
233 0         0 my $commit_message = "$base_commit_message (merge r $source_revision from $source_branch)";
234 0         0 write_file($commit_message_file, $commit_message);
235            
236 0         0 my $merge_command = "svn merge --accept postpone -c $source_revision $source_url .";
237 0         0 print "$merge_command\n";
238 0         0 my $output_from_merge = `$merge_command`;
239 0         0 print "$output_from_merge\n";
240 0 0       0 if ($output_from_merge =~ /Summary of conflicts/) { # If there were merge conflicts
241 0         0 print "Failed to merge r $source_revision from $source_branch to $target_branch.\n";
242 0         0 print "Merge conflicts in $working_dir\n";
243 0         0 die;
244             }
245            
246 0         0 my $commit_command = "svn commit --file $commit_message_file";
247 0         0 print "$commit_command\n";
248 0         0 my $output_from_commit = `$commit_command`;
249 0         0 my $target_revision;
250 0         0 print "$output_from_commit\n";
251 0 0       0 if ($output_from_commit =~ /Committed revision (\d+)\./) {
252 0         0 $target_revision = $1;
253             } else {
254 0         0 die "Failed to commit r $source_revision from $source_branch to $target_branch.\n";
255             }
256            
257 0         0 $source_branch = $target_branch;
258 0         0 $source_revision = $target_revision;
259             } else {
260 0         0 print "Skipping branch $target_branch because we don't need to merge it.\n";
261             }
262             }
263            
264 0 0       0 if ( ! $found_branch ) {
265 0         0 die "Unrecognized branch $source_branch\n";
266             }
267             }
268            
269             #=head2 load_config_from_SVN_url
270             #
271             #Secondary function: initializes based on a configuration
272             #file which is loaded from SVN.
273             #Takes one argument, which is the name of the config file.
274             #
275             #=cut
276            
277             sub load_config_from_SVN_url {
278 0     0 0 0 my $svn_url = shift; # URL of our config file in SVN.
279 0         0 my $rami_home_dir = $SVN::Rami::rami_home_dir;
280 0         0 my $conf_dir = $SVN::Rami::conf_dir;
281 0         0 my $temp_dir = $SVN::Rami::temp_dir;
282 0         0 my $root_work_dir = $SVN::Rami::root_work_dir;
283            
284             # TODO: don't wipe out the old configuration until we know that the new configuration exists.
285            
286 0 0       0 remove_tree $rami_home_dir if -d $rami_home_dir;
287 0         0 make_path $conf_dir, $temp_dir, $root_work_dir;
288            
289 0         0 my $result_of_export = EXEC("svn export --force $svn_url $conf_dir");
290 0 0       0 die "Failed to load configuration from SVN\n" unless ($result_of_export =~ m/Export complete|Exported revision/);
291            
292             # Load the branches.
293             # This code is COPY-AND-PASTED from rami_main().
294 0         0 my @branch_to_url_array = load_csv_as_map("$conf_dir/urls.csv");
295 0         0 my %branch_to_url = @branch_to_url_array;
296 0         0 my @branches = grep( !/^http/, @branch_to_url_array); # HACK: remove URLs, leaving only versions, IN ORDER!
297            
298             # Checkout the branches onto the local filesystem.
299 0         0 while (my($branch,$url) = each(%branch_to_url)) {
300 0         0 my $filesystem_path = convert_branches_to_filesystem_paths($branch);
301 0         0 my $command = "svn checkout $url $filesystem_path";
302 0         0 print "$command\n";
303 0         0 system($command);
304             }
305             }
306            
307             #
308             # Converts a list of logical branch names to the place on the filesystem where they should be.
309             #
310             # Example: convert_branches_to_filesystem_paths('trunk')
311             # might return '/home/bob/.rami/repo/default/work/bob'
312             #
313             # In list context, returns a list.
314             # In scalar context, returns the first item of the list.
315             #
316             sub convert_branches_to_filesystem_paths {
317 2     2 0 132 my $rami_home_dir = $SVN::Rami::rami_home_dir;
318 2         7 my @result = map { "$rami_home_dir/work/$_" } @_;
  3         12  
319 2 100       15 return wantarray ? @result : $result[0];
320             }
321            
322             #
323             # Executes on the command line and returns the output.
324             # Example: my $list = EXEC('ls');
325             #
326             # This is just a wrapper for back-tick, i.e.,
327             # my $result = `$command`;
328             # but it might later allow us to unit-test.
329             #
330             sub EXEC {
331 0     0 0   my $command = shift;
332             # my %args = ...
333 0           print "$command\n";
334 0           my $result = `$command`;
335 0           print "$result\n";
336 0           return $result;
337             }
338            
339            
340            
341             1; # End of SVN::Rami