File indexing completed on 2025-01-26 05:13:51
0001 #!/usr/bin/perl 0002 # 0003 # Copyright (C) 2006 Thiago Macieira <thiago@kde.org> 0004 # 0005 # This file is distributed under the Artistic License version 2.1 0006 # 0007 0008 # Usage: 0009 # svnintegrate [-n] [-v] [-q] [-b branchname] [filename] 0010 # 0011 # Where: 0012 # -n dry run - do not actually run commands that modify sources 0013 # can be used to determine the changeset that would be backported 0014 # -v verbose - show the commands being executed 0015 # -q quiet - do not output some unnecessary messages 0016 # -b branch the branch to merge into [default: trunk] 0017 # filename the file whose last change should be integrated 0018 # if unspecified, the last change for the current directory will 0019 # be used 0020 # 0021 # Also note that svnintegrate will try to locate ALL files modified 0022 # in the changeset being integrated. So you don't need to specify all 0023 # of them in the command line. However, they must all be present in 0024 # the current directory or a subdirectory. 0025 # (If they cannot be found, svnintegrate will tell you) 0026 # 0027 # About branch names: 0028 # svnintegrate tries hard to understand the KDE branch-naming 0029 # scheme. So, in many times, it is enough to simply tell it the 0030 # version of the branch you're integrating into. 0031 # 0032 # Examples: 0033 # switch integrating from integrate to 0034 # -b trunk /branches/KDE/3.5 /trunk/KDE 0035 # -b trunk/extragear/graphics 0036 # /branches/amarok/1.3 /trunk/extragear/graphics/amarok 0037 # -b 3.5 /trunk/KDE/kdelibs /branches/KDE/3.5/kdelibs 0038 # -b 3.5 /branches/KDE/3.4 /branches/KDE/3.5 0039 # -b tags/3.5.0 /branches/KDE/3.5 /tags/KDE/3.5.0 0040 # -b work/my-branch /branches/KDE/3.4 /branches/work/my-branch 0041 # -b work/my-branch /branches/KDE/3.4/kdelibs /branches/work/my-branch 0042 # -b work/my-branch /trunk/KDE/kdepim/kmail /branches/work/my-branch 0043 # 0044 0045 # 0046 # Wanted features: 0047 # Better branch guessing: 0048 # - support for integrating from trunk/{playground,kdereview,extragear} 0049 # - support for integrating from branches/work 0050 # Other features: 0051 # - support for integrating to checked-out branch 0052 # - support for integrating multiple revisions 0053 # 0054 0055 use XML::DOM; 0056 use strict; 0057 0058 my $dirname; 0059 my $svnroot; 0060 my $lastDirCommitRevision; 0061 my $revision; 0062 my $lastCommitAuthor; 0063 my $lastCommitMsg; 0064 my $lastCommitDate; 0065 my @changedPaths; 0066 my @filenames; 0067 my @switchedFilenames; 0068 my @conflictedFilenames; 0069 my $dryRun; 0070 my $quiet; 0071 my $verbose; 0072 my $branch; 0073 my $from; 0074 my $to; 0075 my $EDITOR; 0076 my $PAGER; 0077 0078 sub getSvnInfo() 0079 { 0080 open(INFO, "-|", "svn", "info", "--xml") or die("Could not run svn"); 0081 my $info; 0082 while (<INFO>) 0083 { 0084 $info .= $_; 0085 } 0086 close(INFO); 0087 0088 # now parse 0089 my $parser = new XML::DOM::Parser; 0090 my $doc = $parser->parse($info); 0091 0092 $dirname = $doc->getElementsByTagName("url")->item(0)->getFirstChild()->toString(); 0093 $svnroot = $doc->getElementsByTagName("root")->item(0)->getFirstChild()->toString(); 0094 $lastDirCommitRevision = $doc->getElementsByTagName("commit")->item(0)->getAttribute("revision"); 0095 0096 # trim the root 0097 $dirname = substr $dirname, length($svnroot); 0098 0099 $doc->dispose(); 0100 } 0101 0102 sub getLastCommitInfo($) 0103 { 0104 my $target = @_[0]; 0105 my $rev = "-rCOMMITTED"; 0106 $rev = "-r$revision" if (defined($revision)); 0107 0108 open(LOG, "-|", "svn", "log", "--xml", "-v", $rev, $target) 0109 or die("Could not run svn"); 0110 0111 my $log; 0112 while (<LOG>) 0113 { 0114 $log .= $_; 0115 } 0116 close(LOG); 0117 0118 # now parse it 0119 my $parser = new XML::DOM::Parser; 0120 my $doc = $parser->parse($log); 0121 0122 unless ($doc->getElementsByTagName("logentry")->getLength()) 0123 { 0124 print STDERR "Cannot find revision $revision in the current directory.\n"; 0125 exit(1); 0126 } 0127 0128 $revision = $doc->getElementsByTagName("logentry")->item(0)->getAttribute("revision"); 0129 $lastCommitMsg = $doc->getElementsByTagName("msg")->item(0)->getFirstChild()->toString(); 0130 $lastCommitAuthor = $doc->getElementsByTagName("author")->item(0)->getFirstChild()->toString(); 0131 $lastCommitDate = $doc->getElementsByTagName("date")->item(0)->getFirstChild()->toString(); 0132 0133 my @changed = $doc->getElementsByTagName("path"); 0134 foreach my $path (@changed) 0135 { 0136 push(@changedPaths, $path->getFirstChild()->toString()); 0137 } 0138 0139 $doc->dispose(); 0140 } 0141 0142 sub transformToBranch($) 0143 { 0144 my $path = @_[0]; 0145 0146 if ($path =~ m,^$from(/.*)?,) 0147 { 0148 $path = $to . $1; 0149 } 0150 else 0151 { 0152 print STDERR "Could not apply conversion \"$from\" -> \"$to\" in $dirname\n"; 0153 exit 1; 0154 } 0155 } 0156 0157 sub checkBranches() 0158 { 0159 if (!$from || !$to) 0160 { 0161 $branch = "trunk" unless ($branch); 0162 if ($branch eq "trunk") 0163 { 0164 $to = "/trunk/KDE"; 0165 if ($dirname =~ m,^(/branches/KDE/[^/]+)/,) 0166 { 0167 $from = $1; 0168 } 0169 else 0170 { 0171 print STDERR "Cannot apply automatic conversion to trunk on $dirname\n"; 0172 print STDERR "Please use the -f and -t options\n"; 0173 exit 1; 0174 } 0175 } 0176 else 0177 { 0178 if ($dirname =~ m,^/trunk,) 0179 { 0180 $from = "/trunk"; 0181 } 0182 elsif ($dirname =~ m,^/branches/KDE/([^/]+)/,) 0183 { 0184 $from = "/branches/KDE/$1"; 0185 } 0186 else 0187 { 0188 print STDERR "Cannot apply automatic conversion to branch $branch on $dirname\n"; 0189 print STDERR "Please use the -f and -t options\n"; 0190 exit 1; 0191 } 0192 $to = "/branches/KDE/$branch"; 0193 } 0194 } 0195 my $target = transformToBranch($dirname); 0196 print "Porting from $dirname to $target\n"; 0197 0198 if ($target eq $dirname) 0199 { 0200 print STDERR "Source and target branches are the same.\n"; 0201 exit 1; 0202 } 0203 } 0204 0205 sub showLog() 0206 { 0207 my $prettyDate = $lastCommitDate; 0208 0209 $prettyDate =~ s/^(.*)T(.*)\..*Z$/\1 \2 +0000/; 0210 # mimic svn log: 0211 print "------------------------------------------------------------------------\n"; 0212 print "r$revision | $lastCommitAuthor | $prettyDate\n"; 0213 print "\n"; 0214 print $lastCommitMsg; 0215 print "\n\nChanged files:\n"; 0216 0217 # file list shown by findAllFiles 0218 } 0219 0220 sub findAllFiles() 0221 { 0222 foreach my $path (@changedPaths) 0223 { 0224 my $entry = $path; 0225 if (!($entry =~ s#^$dirname/##o)) 0226 { 0227 print STDERR "Cannot find file \'$svnroot$entry\' in current directory.\n"; 0228 print STDERR "Maybe you need to go up?\n"; 0229 exit 1; 0230 } 0231 0232 print " $entry\n"; 0233 push(@filenames, $entry); 0234 0235 # check that it is clean 0236 open(STATUS, "-|", "svn", "status", $entry); 0237 my $status; 0238 while (<STATUS>) 0239 { 0240 $status .= $_; 0241 } 0242 close(STATUS); 0243 0244 if (length($status)) 0245 { 0246 print STDERR "File \'$entry\' has local modifications or is not clean. Cannot continue.\n"; 0247 exit 1; 0248 } 0249 } 0250 0251 print "------------------------------------------------------------------------\n"; 0252 } 0253 0254 sub run(@) 0255 { 0256 if ($dryRun || $verbose) 0257 { 0258 print join(" ", @_) . "\n"; 0259 } 0260 if (!$dryRun) 0261 { 0262 if ((scalar @_) > 1) 0263 { 0264 open(PIPE, "-|", @_); 0265 } 0266 else 0267 { 0268 my $command = @_[0]; 0269 system($command); 0270 return ""; 0271 } 0272 my $output; 0273 while (<PIPE>) 0274 { 0275 $output .= $_; 0276 } 0277 print $output unless ($quiet); 0278 return $output; 0279 } 0280 return ""; 0281 } 0282 0283 sub rollback() 0284 { 0285 print "\nRolling back changes\n"; 0286 for my $file (@switchedFilenames) 0287 { 0288 run("svn", "revert", $file); 0289 run("svn", "switch", "-r", $revision, "$svnroot$dirname/$file", $file); 0290 } 0291 0292 run("rm", "svn-commit.tmp") if (-e "svn-commit.tmp"); 0293 run("rm", "svn-commit.tmp~") if (-e "svn-commit.tmp~"); 0294 } 0295 0296 sub switchAllFiles() 0297 { 0298 my $target = transformToBranch($dirname); 0299 print "Switching files to branch $target\n" 0300 unless ($quiet); 0301 0302 foreach my $file (@filenames) 0303 { 0304 my $output = run("svn", "switch", $svnroot . $target . "/$file", $file); 0305 push(@switchedFilenames, $file); 0306 } 0307 } 0308 0309 sub handleConflict($) 0310 { 0311 my $file = @_[0]; 0312 my $target = transformToBranch($dirname); 0313 my $leftname = "$file.merge-left.r" . ($revision - 1); 0314 my $rightname = "$file.merge-right.r$revision"; 0315 my $workingname = "$file.working"; 0316 0317 my $showmenu = 1; 0318 while (1) 0319 { 0320 if ($showmenu) 0321 { 0322 print "\nFile \'$file\' has conflicts while merging.\n"; 0323 print "What do you want to do?\n"; 0324 print "\t1. Edit file with $EDITOR\n"; 0325 print "\t2. Show current diff to be committed\n"; 0326 print "\t3. Show the change between " . ($revision - 1) . " and $revision\n"; 0327 print "\t4. View original $dirname/$file (\"left\": before the change)\n"; 0328 print "\t5. View current $dirname/$file (\"right\": after the change)\n"; 0329 print "\t6. View current $target/$file (\"working\")\n"; 0330 print "\t7. Accept original $dirname/$file (\"left\")\n"; 0331 print "\t8. Accept current $dirname/$file (\"right\")\n"; 0332 print "\t9. Accept current $target/$file (\"working\": do not apply merge)\n"; 0333 print "\t0. Revert all and quit\n"; 0334 } 0335 $showmenu = 0; 0336 0337 my $answer = <STDIN>; 0338 $answer =~ s/\r?\n$//; 0339 if ($answer eq "1") 0340 { 0341 run($EDITOR, $file); 0342 print "Is it resolved? (Y/n)"; 0343 $answer = <STDIN>; 0344 print "\n"; 0345 if ($answer =~ /y/i or length($answer) == 0) 0346 { 0347 run("svn", "resolved", $file); 0348 return; 0349 } 0350 } 0351 elsif ($answer eq "2") 0352 { 0353 run("svn diff $file | $PAGER"); 0354 } 0355 elsif ($answer eq "3") 0356 { 0357 run("diff -u $leftname $rightname | $PAGER"); 0358 } 0359 elsif ($answer eq "4") 0360 { 0361 run($PAGER, "$leftname"); 0362 } 0363 elsif ($answer eq "5") 0364 { 0365 run($PAGER, "$rightname"); 0366 } 0367 elsif ($answer eq "6") 0368 { 0369 run($PAGER, "$workingname"); 0370 } 0371 elsif ($answer eq "7") 0372 { 0373 run("mv", "$leftname", $file); 0374 run("svn", "resolved", $file); 0375 return; 0376 } 0377 elsif ($answer eq "8") 0378 { 0379 run("mv", "$rightname", $file); 0380 run("svn", "resolved", $file); 0381 return; 0382 } 0383 elsif ($answer eq "9") 0384 { 0385 run("mv", "$workingname", $file); 0386 run("svn", "resolved", $file); 0387 return; 0388 } 0389 elsif ($answer eq "0") 0390 { 0391 rollback(); 0392 exit 0; 0393 } 0394 else 0395 { 0396 $showmenu = 1; 0397 } 0398 } 0399 } 0400 0401 sub mergeRevision() 0402 { 0403 print "Merging revision $revision\n" 0404 unless ($quiet); 0405 my $output = run("svn", "merge", "-r", ($revision - 1) . ":" . $revision, $svnroot . $dirname); 0406 my @lines = split(/\r?\n/, $output); 0407 0408 if (scalar @lines) 0409 { 0410 foreach my $line (@lines) 0411 { 0412 if ($line =~ /^C +(.+)$/) 0413 { 0414 push(@conflictedFilenames, $1) 0415 } 0416 if ($line =~ /^D +(.+)$/) 0417 { 0418 print STDERR "I cannot handle file deletions, sorry (\'$1\').\n"; 0419 print STDERR "You will probably have to run \'svn up\' to recover the file.\n"; 0420 rollback(); 0421 exit 1; 0422 } 0423 } 0424 } 0425 else 0426 { 0427 print "No files were changed: this changeset has already been integrated.\n"; 0428 rollback(); 0429 exit(0); 0430 } 0431 0432 foreach my $conflict (@conflictedFilenames) 0433 { 0434 handleConflict($conflict); 0435 } 0436 } 0437 0438 sub createLogMessage() 0439 { 0440 open(LOG, ">svn-commit.tmp"); 0441 print LOG "INTEGRATION:$dirname $revision\n"; 0442 0443 my @lines = split(/\r?\n/, $lastCommitMsg); 0444 foreach my $line (@lines) 0445 { 0446 next if ($line =~ /^CC.?MAIL:.*/); # don't resend emails 0447 next if ($line =~ /^GUI:/); 0448 0449 $line =~ s/^(BUG|FEATURE):/CCBUG:/; # change bug closing to comment 0450 print LOG "$line\n"; 0451 } 0452 close(LOG); 0453 } 0454 0455 sub editLogMessage() 0456 { 0457 print "\nMerging successful\n"; 0458 while (1) 0459 { 0460 print "Press (C) to commit, (D) to see the diff, (Q) to quit or (E) to edit the log\n"; 0461 0462 my $answer = <STDIN>; 0463 $answer =~ s/\r?\n$//; 0464 $answer =~ tr/A-Z/a-z/; 0465 if ($answer eq "q") 0466 { 0467 rollback(); 0468 exit(0); 0469 } 0470 elsif ($answer eq "e") 0471 { 0472 run($EDITOR, "svn-commit.tmp"); 0473 } 0474 elsif ($answer eq "d") 0475 { 0476 run("svn diff ". join(" ", @filenames) . " | $PAGER"); 0477 } 0478 elsif ($answer eq "c") 0479 { 0480 return; 0481 } 0482 } 0483 } 0484 0485 sub commit() 0486 { 0487 print "Committing changes\n"; 0488 run("svn commit -F svn-commit.tmp " . join(" ", @filenames)); 0489 0490 print "Restoring old state\n"; 0491 rollback(); 0492 } 0493 0494 $EDITOR = $ENV{"EDITOR"}; 0495 $PAGER = $ENV{"PAGER"}; 0496 $EDITOR = "vi" unless ($EDITOR); 0497 $PAGER = "less" unless ($PAGER); 0498 0499 $dryRun = 0; 0500 $quiet = 0; 0501 $verbose = 0; 0502 while (@ARGV) 0503 { 0504 my $arg = shift @ARGV; 0505 if ($arg eq "-n") 0506 { 0507 $dryRun = 1; 0508 } 0509 elsif ($arg eq "-v") 0510 { 0511 $verbose = 1; 0512 } 0513 elsif ($arg eq "-q") 0514 { 0515 $quiet = 1; 0516 } 0517 elsif ($arg eq "-b") 0518 { 0519 unless (@ARGV) 0520 { 0521 print STDERR "Option -b requires an argument\n"; 0522 exit 1; 0523 } 0524 0525 $branch = shift @ARGV; 0526 } 0527 elsif ($arg eq "-r") 0528 { 0529 unless (@ARGV) 0530 { 0531 print STDERR "Option -r requires an argument\n"; 0532 exit 1; 0533 } 0534 0535 $revision = shift @ARGV; 0536 } 0537 elsif ($arg eq "-f") 0538 { 0539 unless (@ARGV) 0540 { 0541 print STDERR "Option -f requires an argument\n"; 0542 exit 1; 0543 } 0544 0545 $from = shift @ARGV; 0546 } 0547 elsif ($arg eq "-t") 0548 { 0549 unless (@ARGV) 0550 { 0551 print STDERR "Option -t requires an argument\n"; 0552 exit 1; 0553 } 0554 0555 $to = shift @ARGV; 0556 } 0557 else 0558 { 0559 unshift @ARGV, $arg; 0560 last; 0561 } 0562 } 0563 0564 getSvnInfo(); 0565 getLastCommitInfo($ARGV[0]); 0566 checkBranches(); 0567 showLog(); 0568 findAllFiles(); 0569 switchAllFiles(); 0570 0571 exit(0) if ($dryRun); 0572 0573 mergeRevision(); 0574 createLogMessage(); 0575 editLogMessage(); 0576 commit();