3 #=======================================================================
5 # File ID: 7a1ba190-f743-11dd-a70f-000475e441b9
6 # Merges new changes into a file version controlled by Subversion.
9 # ©opyleft 2006– Øyvind A. Holm <sunny@sunbase.org>
10 # License: GNU General Public License version 2 or later, see end of
11 # file for legal stuff.
12 # This file is part of the svnutils project — http://svnutils.tigris.org
13 #=======================================================================
40 $progname =~ s/^.*\/(.*?)$/$1/;
41 our $VERSION = "0.00";
43 Getopt
::Long
::Configure
("bundling");
46 "alias|a=s" => \
$Opt{'alias'},
47 "conflict|C" => \
$Opt{'conflict'},
48 "debug" => \
$Opt{'debug'},
49 "diff|d" => \
$Opt{'diff'},
50 "dry-run" => \
$Opt{'dry-run'},
51 "help|h" => \
$Opt{'help'},
52 "log|l" => \
$Opt{'log'},
53 "set|s=s" => \
$Opt{'set'},
54 "to|t=s" => \
$Opt{'to'},
55 "verbose|v+" => \
$Opt{'verbose'},
56 "version" => \
$Opt{'version'},
58 ) || die("$progname: Option error. Use -h for help.\n");
62 my $PROP_NAME = "mergesvn";
64 $Opt{'debug'} && ($Debug = 1);
65 $Opt{'help'} && usage
(0);
66 if ($Opt{'version'}) {
73 if (!scalar(@Files)) {
84 if (length($Opt{'set'})) {
85 set_mergesvn_prop
($File, $Opt{'set'});
88 my $prop_val = `$CMD_SVN propget $PROP_NAME $File`;
89 if (!length($prop_val)) {
90 warn("$progname: $File: \"$PROP_NAME\" property not found, " .
95 my @Props = split(/\n/, $prop_val);
96 D
("Props = (\"" . join("\", \"", @Props) . "\")");
97 for my $Curr (@Props) {
98 if ($Curr =~ /^(\d+) (.+)$/) {
99 my ($last_merge, $orig_master) =
101 $orig_master =~ s/[\r\n]+$//;
102 my $master_file = length($Opt{'alias'})
105 my $curr_rev = highest_revision
($master_file, $Opt{'to'});
106 if (!legal_revision
($curr_rev)) {
107 warn("$progname: $master_file: " .
108 "Unable to get newest revision\n");
111 if ($Opt{'conflict'}) {
112 find_conflict
($master_file, $File, $last_merge, $curr_rev);
116 my $Repos = repos_url
($File);
119 repos_url
($master_file) . "\@$last_merge",
125 if ($last_merge == $curr_rev) {
126 print(STDERR
"No revisions found\n");
128 log_range
($master_file, $last_merge + 1, $curr_rev);
132 if ($Opt{'dry-run'}) {
134 $CMD_SVN, "merge", "-r$last_merge:$curr_rev",
139 if ($last_merge == $curr_rev) {
140 print(STDERR
"$progname: $File: No new revisions available (r$last_merge)\n");
144 $CMD_SVN, "merge", "-r$last_merge:$curr_rev",
148 push(@new_prop, "$curr_rev $orig_master");
150 "$progname: $File: Merged r$last_merge:$curr_rev (" .
151 join(", ", revisions
($master_file, $last_merge, $curr_rev)) .
155 warn("$File: \"$Curr\": Invalid property line\n");
160 mysyst
($CMD_SVN, "propset", $PROP_NAME, join("\n", @new_prop), $File);
165 # Check that a string is a legal revision number {{{
169 if ($Rev =~ /[^\d]/ || !length($Rev)) {
176 sub set_mergesvn_prop
{
177 # Define the $PROP_NAME property for an element. Finds the highest
178 # revision which had a change.
180 my ($File, $Str) = @_;
181 my ($Source, $source_rev) = ("", "");
183 if ($Str =~ /^(\S+)\@(\d*)$/) {
186 } elsif ($Str =~ /^(\S+)$/) {
188 $source_rev = "HEAD";
190 warn("$progname: $Str: Invalid source URL\n");
193 if ($source_rev =~ /[^\d]/ && $source_rev !~ /^HEAD$/i) {
194 die("$progname: $source_rev: Invalid source revision\n");
196 D
("set_mergesvn_prop(): Source = '$Source', source_rev = '$source_rev'");
197 my $work_source = length($Opt{'alias'}) ?
$Opt{'alias'} : $Source;
198 my $highest_rev = highest_revision
($work_source, $source_rev);
199 D
("set_mergesvn_prop(): highest_rev = '$highest_rev'");
200 if (!length($highest_rev)) {
201 die("$progname: $work_source: Unable to locate source revision\n");
203 if ($highest_rev ne $source_rev) {
204 warn("$progname: $File: Using revision $highest_rev instead of $source_rev\n");
206 mysyst
($CMD_SVN, "propset", $PROP_NAME, "$highest_rev $Source", $File);
208 } # set_mergesvn_prop()
211 # Scan a specific revision range for the first merge conflict and
212 # return the revision number
214 my ($Src, $Dest, $Start, $End) = @_;
216 D
("find_conflict('$Src', '$Dest', '$Start', '$End')");
217 print(STDERR
"$progname: $Dest: Scanning revision range r$Start:$End " .
219 my @Array = revisions
($Src, $Start, $End);
220 if (!scalar(@Array)) {
221 print(STDERR
"No revisions found.\n");
225 my $rev_count = scalar(@Array);
226 printf(STDERR
"$rev_count revision%s to check\n", $rev_count == 1 ?
"" : "s");
227 print(STDERR
"(" . join(", ", @Array) . ")\n");
230 my ($min_pos, $max_pos) = (0, $rev_count);
233 my $first_conflict = 0;
238 my $mid_pos = int(($min_pos + $max_pos) / 2);
239 last if ($has_checked && ($mid_pos == $last_mid));
240 my $Rev = $Array[$mid_pos];
241 printf(STDERR
"Checking revision %lu (%lu/%lu)...",
242 $Rev, $mid_pos + 1, $rev_count);
243 if (!has_conflict
($Src, $Dest, $Start, $Rev)) {
244 print(STDERR
"No conflict\n");
246 D
("min_pos set to '$mid_pos'");
247 if (!$last_good || ($Rev > $last_good)) {
251 print(STDERR
"Conflict\n");
253 D
("max_pos set to '$mid_pos'");
254 if (!$first_conflict || ($Rev < $first_conflict)) {
255 $first_conflict = $Rev;
259 $last_mid = $mid_pos;
261 print(STDERR
$first_conflict
262 ?
"First conflict at r$first_conflict. "
263 : "No conflicts found. "
265 print(STDERR
$last_good
266 ?
"Last revision without conflict at r$last_good.\n"
267 : "No revisions without conflicts found.\n"
275 my ($Src, $Dest, $Start, $End) = @_;
276 my ( $safe_src, $safe_dest) =
277 (escape_filename
($Src), escape_filename
($Dest));
279 D
("has_conflict('$Src', '$Dest', '$Start', '$End')");
280 if (open(ConflFP
, "$CMD_SVN merge --dry-run " .
281 "-r$Start:$End $safe_src $safe_dest |")) {
283 my $Stat = substr($_, 0, 2);
284 ($Stat =~ /C/) && ($Retval = 1);
293 # Return an array of revision numbers from a specific revision range
294 # for a version controlled element
296 my ($File, $Start, $End) = @_;
297 D
("revisions('$File', '$Start', '$End')");
298 my $safe_file = escape_filename
($File);
302 if (open(PipeFP
, "$CMD_SVN log --xml -r$Start:$End $safe_file |")) {
303 $Data = join("", <PipeFP
>);
305 $Data =~ s/<logentry\b.*?\brevision="(\d+)".*?>/push(@Revs, "$1")/egs;
307 if ($Revs[0] eq $Start) {
314 sub highest_revision
{
315 # Return the newest revision of a versioned element inside a
316 # specified revision range
318 my ($Path, $max_rev) = @_;
319 my $safe_path = escape_filename
($Path);
320 my $highest_rev = `$CMD_SVN log -r$max_rev:1 --limit 1 --xml $safe_path`; # FIXME
321 $highest_rev =~ s/^.*?<logentry.+?revision="(\d+)".*?>.*/$1/s;
322 legal_revision
($highest_rev) || ($highest_rev = "");
323 return($highest_rev);
325 } # highest_revision()
328 # Return the repository address of an element {{{
330 my $safe_file = escape_filename
($File);
331 my $Retval = `$CMD_SVN info --xml $safe_file`;
332 $Retval =~ s/^.*<url>(.*?)<\/url>.*$/$1/s
; # FIXME: Add XML parsing
338 # Customised system() {{{
340 my $system_txt = sprintf("system(\"%s\");", join("\", \"", @Args));
343 msg
(1, "Executing '@_'");
348 sub escape_filename
{
349 # Kludge for handling file names with spaces and characters that
350 # trigger shell functions
353 # $Name =~ s/\\/\\\\/g;
354 # $Name =~ s/([ \t;\|!&"'`#\$\(\)<>\*\?])/\\$1/g;
359 } # escape_filename()
362 # Show a revision log of the specified range {{{
363 my ($File, $Start, $End) = @_;
365 if ($Opt{'verbose'}) {
366 mysyst
($CMD_SVN, "log", "-r$Start:$End", "-v", $File);
368 mysyst
($CMD_SVN, "log", "-r$Start:$End", $File);
374 # Wait until Enter is pressed if --debug {{{
376 print(STDERR
"debug: Press ENTER...");
382 # Print program version {{{
383 print("$progname v$VERSION\n");
388 # Send the help message to stdout {{{
391 if ($Opt{'verbose'}) {
397 Merge changes between Subversion controlled files or directories.
398 Elements without the "$PROP_NAME" property will be ignored. If no
399 filenames are specified on the command line, it reads filenames from
402 Usage: $progname [options] [file [files [...]]]
407 Use x as alias for the master URL. The old value will still be
408 written to the $PROP_NAME property.
410 Do not merge, but search for the first revision a conflict will
411 occur when a merge is done. After the search is finished, the first
412 revision number of a troublesome patch is printed, and you can
413 choose by using the -t option if you want to include the conflicting
414 revision in the merge.
416 Instead of merging, show a repository diff between the master URL
417 and the versioned element.
419 Try operation without making any changes. Can be used to see if the
420 merge will result in conflicts.
424 Show a revision log of the remaining merges.
425 -s x[\@y], --set x[\@y]
426 Set merge source for all filenames on the command line. If \@y is
427 specified, revision y will be used as the merge source, otherwise
428 HEAD is used. The revision number actually used is the newest
429 revision the source changed.
431 Merge to revision x instead of HEAD.
433 Use the -v option together with svn commands that accepts it.
435 Print version information.
437 Print debugging messages.
445 # Print a status message to stderr based on verbosity level {{{
446 my ($verbose_level, $Txt) = @_;
448 if ($Opt{'verbose'} >= $verbose_level) {
449 print(STDERR
"$progname: $Txt\n");
455 # Print a debugging message if --debug {{{
457 my @call_info = caller;
458 chomp(my $Txt = shift);
459 my $File = $call_info[1];
461 $File =~ s
#^.*/(.*?)$#$1#;
462 print(STDERR
"$File:$call_info[2] $$ $Txt\n");
469 # Plain Old Documentation (POD) {{{
479 mergesvn [options] [file [files [...]]]
483 Merge changes between Subversion controlled files or directories.
484 Elements without the "mergesvn" property will be ignored.
485 If no filenames are specified on the command line, it reads filenames
488 Files or directories to be controlled by mergesvn must have the
489 following property set:
497 Contains one line for every place to merge from.
498 The line consists of two elements, the revision in the master file the
499 last merge was done, and path or URL to the master file.
500 These two fields are separated by exactly one space (U+0020).
506 =item B<-a>, B<--alias> I<x>
508 Use I<x> as alias for the master URL. The old value will still be
509 written to the mergesvn property.
511 =item B<-C>, B<--conflict>
513 Do not merge, but search for the first revision a conflict will occur
514 when a merge is done. After the search is finished, the first revision
515 number of a troublesome patch is printed, and you can choose by using
516 the -t option if you want to include the conflicting revision in the
519 =item B<-d>, B<--diff>
521 Instead of merging, show a repository diff between the master URL and
522 the versioned element.
526 Try operation without making any changes.
527 Can be used to see if the merge will result in conflicts.
529 =item B<-h>, B<--help>
531 Print a brief help summary.
533 =item B<-l>, B<--log>
535 Show a revision log of the remaining merges.
537 =item B<-s>, B<--set> I<x>[@I<y>]
539 Set merge source for all filenames on the command line.
540 If I<@y> is specified, revision y will be used as the merge source,
541 otherwise HEAD is used.
542 The revision number actually used is the newest revision the source
545 =item B<-t>, B<--to> I<x>
547 Merge to revision I<x> instead of HEAD.
549 =item B<-v>, B<--verbose>
551 Increase level of verbosity. Can be repeated.
555 Print version information.
557 =item B<-v>, B<--verbose>
559 Use the -v option together with svn commands that accepts it.
563 Print debugging messages.
571 =item The svn(1) client does not support diffs between different
572 repositories (yet), so the B<--diff> option will only work with elements
573 that has the master in the same repository.
579 Made by Øyvind A. Holm S<E<lt>sunny@sunbase.orgE<gt>>.
583 Copyleft © Øyvind A. Holm E<lt>sunny@sunbase.orgE<gt>
584 This is free software; see the file F<COPYING> for legalese stuff.
588 This program is free software: you can redistribute it and/or modify it
589 under the terms of the GNU General Public License as published by the
590 Free Software Foundation, either version 2 of the License, or (at your
591 option) any later version.
593 This program is distributed in the hope that it will be useful, but
594 WITHOUT ANY WARRANTY; without even the implied warranty of
595 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
596 See the GNU General Public License for more details.
598 You should have received a copy of the GNU General Public License along
600 If not, see L<http://www.gnu.org/licenses/>.
610 # vim: set fenc=UTF-8 ft=perl fdm=marker ts=4 sw=4 sts=4 et fo+=w :