File Coverage

blib/lib/SVN/Rami.pm
Criterion Covered Total %
statement 18 116 15.5
branch 2 28 7.1
condition n/a
subroutine 6 13 46.1
pod 1 8 12.5
total 27 165 16.3


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