File indexing completed on 2024-03-24 05:44:52

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();