annotate.cpp

Go to the documentation of this file.
00001 /*
00002         Description: file history annotation
00003 
00004         Author: Marco Costalba (C) 2005-2006
00005 
00006         Copyright: See COPYING file that comes with this distribution
00007 
00008 */
00009 #include <qapplication.h>
00010 #include "git.h"
00011 #include "annotate.h"
00012 
00013 #define MAX_AUTHOR_LEN 16
00014 
00015 Annotate::Annotate(Git* parent, QObject* guiObj) : QObject(parent) {
00016 
00017         EM_INIT(exAnnCanceled, "Canceling annotation");
00018 
00019         git = parent;
00020         gui = guiObj;
00021         cancelingAnnotate = annotateRunning = annotateActivity = false;
00022         valid = canceled = false;
00023 
00024         patchProcBuf.reserve(1000000); // avoid repeated reallocation, will be big!
00025 
00026         processingTime.start();
00027 
00028         connect(&patchProc, SIGNAL(readyReadStdout()), this, SLOT(on_patchProc_readFromStdout()));
00029         connect(&patchProc, SIGNAL(processExited()), this, SLOT(on_patchProc_processExited()));
00030 }
00031 
00032 const FileAnnotation* Annotate::lookupAnnotation(SCRef sha, SCRef fn) {
00033 
00034         if (!valid || fileName != fn)
00035                 return NULL;
00036 
00037         AnnotateHistory::const_iterator it = ah.find(sha);
00038         if (it != ah.constEnd())
00039                 return &(it.data());
00040 
00041         // ok, we are not lucky. Check for an ancestor before to give up
00042         int shaIdx;
00043         const QString ancestorSha = getAncestor(sha, fileName, &shaIdx);
00044         if (!ancestorSha.isEmpty()) {
00045                 it = ah.find(ancestorSha);
00046                 if (it != ah.constEnd())
00047                         return &(it.data());
00048         }
00049         return NULL;
00050 }
00051 
00052 bool Annotate::startPatchProc(SCRef buf, SCRef fileName) {
00053 
00054         QString cmd("git diff-tree --no-color -r -m --patch-with-raw --no-commit-id --stdin --");
00055         QStringList args(QStringList::split(' ', cmd));
00056         args.append(fileName); // handle file name with spaces case
00057         patchProc.setArguments(args);
00058         patchProc.setWorkingDirectory(git->workDir);
00059         patchProcBuf = "";
00060         return patchProc.launch(buf);
00061 }
00062 
00063 void Annotate::on_patchProc_readFromStdout() {
00064 
00065         const QString tmp(patchProc.readStdout());
00066         annFilesNum += tmp.contains("diff --git ");
00067         if (annFilesNum > (int)histRevOrder.count())
00068                 annFilesNum = histRevOrder.count();
00069         patchProcBuf.append(tmp);
00070 }
00071 
00072 void Annotate::deleteWhenDone() {
00073 
00074         if (!EM_IS_PENDING(exAnnCanceled))
00075                 EM_RAISE(exAnnCanceled);
00076 
00077         if (annotateRunning)
00078                 cancelingAnnotate = true;
00079 
00080         if (patchProc.isRunning())
00081                 patchProc.tryTerminate();
00082 
00083         on_deleteWhenDone();
00084 }
00085 
00086 void Annotate::on_deleteWhenDone() {
00087 
00088         if (!(annotateRunning || EM_IS_PENDING(exAnnCanceled)))
00089                 deleteLater();
00090         else
00091                 QTimer::singleShot(20, this, SLOT(on_deleteWhenDone()));
00092 }
00093 
00094 void Annotate::on_progressTimer_timeout() {
00095 
00096         if (!cancelingAnnotate && !isError) {
00097                 const QString n(QString::number(annFilesNum));
00098                 QApplication::postEvent(gui, new AnnotateProgressEvent(n));
00099         }
00100 }
00101 
00102 bool Annotate::start(const FileHistory* _fh) {
00103 
00104         // could change during annotation, so save them
00105         fh = _fh;
00106         fileName = fh->fileName;
00107         histRevOrder = fh->revOrder;
00108 
00109         if (histRevOrder.isEmpty()) {
00110                 valid = false;
00111                 return false;
00112         }
00113         annotateRunning = true;
00114 
00115         // init AnnotateHistory
00116         annFilesNum = 0;
00117         annId = histRevOrder.count();
00118         annNumLen = QString::number(histRevOrder.count()).length();
00119         StrVect::const_iterator it(histRevOrder.constBegin());
00120         do
00121                 ah.insert(*it, FileAnnotation(annId--));
00122         while (++it != histRevOrder.constEnd());
00123 
00124         // annotation is split in two parts.
00125         // first we get the list of sha's to feed git diff-tree.
00126         // This is very fast.
00127         isError = false;
00128         annotateFileHistory(fileName, true);
00129 
00130         if (isError || cancelingAnnotate) {
00131                 slotComputeDiffs(); // clean-up in case of error/canceling
00132                 return false;
00133         }
00134         connect(&progressTimer, SIGNAL(timeout()), this, SLOT(on_progressTimer_timeout()));
00135         progressTimer.start(500);
00136 
00137         // now we get the patches with an async call. This is the slowest part.
00138         return startPatchProc(patchScript, fileName);
00139 }
00140 
00141 void Annotate::on_patchProc_processExited() {
00142 
00143         // start computing diffs only on return from event handler
00144         QTimer::singleShot(1, this, SLOT(slotComputeDiffs()));
00145         progressTimer.stop();
00146 }
00147 
00148 void Annotate::slotComputeDiffs() {
00149 
00150         // when all the patches are loaded we compute the annotation.
00151         // this part is normally faster then getting the patches.
00152         if (!cancelingAnnotate) {
00153 
00154                 diffMap.clear();
00155                 AnnotateHistory::iterator it(ah.begin());
00156                 do
00157                         (*it).isValid = false; // reset flags
00158                 while (++it != ah.end());
00159 
00160                 // remove first 'next patch' marker
00161                 int first = patchProcBuf.find(':');
00162                 if (first != -1) {
00163                         nextFileSha = patchProcBuf.mid(first + 56, 40);
00164                         patchProcBuf.remove(0, first + 100);
00165                 }
00166                 annotateFileHistory(fileName, false); // now could call Qt event loop
00167         }
00168         valid = !(isError || cancelingAnnotate);
00169         canceled = cancelingAnnotate;
00170         cancelingAnnotate = annotateRunning = false;
00171         if (canceled)
00172                 deleteWhenDone();
00173         else
00174                 git->annotateExited(this);
00175 
00176 //      StrVect::const_iterator it(histRevOrder.constBegin());
00177 //      do {
00178 //              dbg(*it); dbg(ah[*it].fileSha);
00179 //      } while (++it != histRevOrder.constEnd());
00180 }
00181 
00182 void Annotate::annotateFileHistory(SCRef fileName, bool buildPatchScript) {
00183 
00184         // sweep from the oldest to newest so that parent
00185         // annotations are calculated before children
00186         StrVect::const_iterator it(histRevOrder.constEnd());
00187         do {
00188                 --it;
00189                 doAnnotate(fileName, *it, buildPatchScript);
00190         } while (it != histRevOrder.constBegin() && !isError && !cancelingAnnotate);
00191 }
00192 
00193 void Annotate::doAnnotate(SCRef fileName, SCRef sha, bool buildPatchScript) {
00194 // all the parents annotations must be valid here
00195 
00196         FileAnnotation* fa = getFileAnnotation(sha);
00197         if (fa == NULL || fa->isValid || isError || cancelingAnnotate)
00198                 return;
00199 
00200         const Rev* r = git->revLookup(sha, fh); // historyRevs
00201         if (r == NULL) {
00202                 dbp("ASSERT doAnnotate: no revision %1", sha);
00203                 isError = true;
00204                 return;
00205         }
00206         if (r->parentsCount() == 0) { // initial revision
00207                 if (!buildPatchScript)
00208                         setInitialAnnotation(fileName, sha, fa); // calls Qt event loop
00209                 fa->isValid = true;
00210                 return;
00211         }
00212         // now create a new annotation from first parent diffs
00213         const QStringList parents(r->parents());
00214         const QString& parSha = parents.first();
00215         FileAnnotation* pa = getFileAnnotation(parSha);
00216 
00217         if (!(pa && pa->isValid)) {
00218                 dbp("ASSERT in doAnnotate: annotation for %1 not valid", parSha);
00219                 isError = true;
00220                 return;
00221         }
00222         if (buildPatchScript) // just prepare patch script
00223                 updatePatchScript(sha, parSha);
00224         else {
00225                 const QString diff(getNextPatch(patchProcBuf, fileName, sha));
00226                 const QString author(setupAuthor(r->author(), fa->annId));
00227                 setAnnotation(diff, author, pa->lines, fa->lines);
00228         }
00229         // then add other parents diff if any
00230         QStringList::const_iterator it(parents.constBegin());
00231         ++it;
00232         while (it != parents.constEnd()) {
00233 
00234                 FileAnnotation* pa = getFileAnnotation(*it);
00235 
00236                 if (buildPatchScript) { // just prepare patch script
00237                         updatePatchScript(sha, *it);
00238                         ++it;
00239                         continue;
00240                 }
00241                 const QString diff(getNextPatch(patchProcBuf, fileName, sha));
00242                 QStringList tmpAnn;
00243                 setAnnotation(diff, "Merge", pa->lines, tmpAnn);
00244 
00245                 // the two annotations must be of the same length
00246                 if (fa->lines.count() != tmpAnn.count()) {
00247                         qDebug("ASSERT: merging annotations of different length\n"
00248                                " merging %s in %s", (*it).latin1(), sha.latin1());
00249                         isError = true;
00250                         return;
00251                 }
00252                 // finally we unify the annotations
00253                 unify(tmpAnn, fa->lines);
00254                 fa->lines = tmpAnn;
00255                 ++it;
00256         }
00257         fa->isValid = true;
00258 }
00259 
00260 FileAnnotation* Annotate::getFileAnnotation(SCRef sha) {
00261 
00262         AnnotateHistory::iterator it(ah.find(sha));
00263         if (it == ah.end()) {
00264                 dbp("ASSERT getFileAnnotation: no revision %1", sha);
00265                 isError = true;
00266                 return NULL;
00267         }
00268         return &(*it);
00269 }
00270 
00271 void Annotate::setInitialAnnotation(SCRef fileName, SCRef sha, FileAnnotation* fa) {
00272 
00273         QString fileSha;
00274         QByteArray fileData;
00275         git->getFile(fileName, sha, NULL, &fileData, &fileSha); // calls Qt event loop
00276         if (cancelingAnnotate)
00277                 return;
00278 
00279         if (fileSha.isEmpty()) {
00280                 dbp("ASSERT in setInitialAnnotation: empty file of initial rev %1", sha);
00281                 isError = true;
00282                 return;
00283         }
00284         ah[sha].fileSha = fileSha;
00285         QString fileTxt(fileData);
00286         int lineNum = fileTxt.contains('\n');
00287         if (!fileTxt.endsWith("\n")) // No newline at end of file
00288                 lineNum++;
00289 
00290         fa->lines.insert(fa->lines.end(), lineNum, "");
00291 }
00292 
00293 const QString Annotate::setupAuthor(SCRef origAuthor, int annId) {
00294 
00295         QString author(QString("%1.").arg(annId, annNumLen)); // first field is annotation id
00296         QString tmp(origAuthor.section('<', 0, 0).stripWhiteSpace()); // strip e-mail address
00297         if (tmp.isEmpty()) { // probably only e-mail
00298                 tmp = origAuthor;
00299                 tmp.remove('<').remove('>');
00300                 tmp = tmp.stripWhiteSpace();
00301                 tmp.truncate(MAX_AUTHOR_LEN);
00302         }
00303         // shrink author name if necessary
00304         if (tmp.length() > MAX_AUTHOR_LEN) {
00305                 SCRef firstName(tmp.section(' ', 0, 0));
00306                 SCRef surname(tmp.section(' ', 1));
00307                 if (!firstName.isEmpty() && !surname.isEmpty())
00308                         tmp = firstName.left(1) + ". " + surname;
00309                 tmp.truncate(MAX_AUTHOR_LEN);
00310         }
00311         author.append(tmp);
00312         return author;
00313 }
00314 
00315 void Annotate::unify(SList dst, SCList src) {
00316 
00317         QStringList::Iterator itd(dst.begin());
00318         QStringList::const_iterator its(src.constBegin());
00319         for ( ; itd != dst.end(); ++itd, ++its)
00320                 if (*itd == "Merge")
00321                         *itd = *its;
00322 }
00323 
00324 void Annotate::setAnnotation(SCRef diff, SCRef author, SCList prevAnn,
00325                              QStringList& newAnn, int ofs) {
00326         newAnn = prevAnn;
00327         QStringList::iterator cur(newAnn.begin());
00328         QString line;
00329         int idx = 0, num, lineNumStart, lineNumEnd;
00330         while (getNextSection(diff, idx, line, "\n")) {
00331                 char firstChar = line[0].latin1();
00332                 switch (firstChar) {
00333                 case '@':
00334                         // an unified diff fragment header has form '@@ -a,b +c,d @@'
00335                         // where 'a' is old file line number and 'b' is old file
00336                         // number of lines of the hunk, 'c' and 'd' are the same
00337                         // for new file. If the file does not have enough lines
00338                         // then also the form '@@ -a +c @@' is used.
00339                         lineNumStart = line.find('+') + 1;
00340                         lineNumEnd = line.find(',', lineNumStart);
00341                         if (lineNumEnd == -1) // small file case
00342                                 lineNumEnd = line.find(' ', lineNumStart);
00343 
00344                         num = line.mid(lineNumStart, lineNumEnd - lineNumStart).toInt();
00345                         num -= ofs; // offset for range filter computation
00346 
00347                         // diff lines start from 1, 0 is empty file,
00348                         // instead QValueList::at() starts from 0
00349                         if (num <= 0) { // file is deleted
00350                                 if (num < 0)
00351                                         dbp("ASSERT processDiff: start line number is %1", num);
00352                                 newAnn.clear();
00353                                 return;
00354                         }
00355                         cur = newAnn.at(num - 1);
00356                         break;
00357                 case '+':
00358                         if (cur != newAnn.end()) {
00359                                 cur = newAnn.insert(cur, author);
00360                                 ++cur;
00361                         } else {
00362                                 newAnn.append(author);
00363                                 cur = newAnn.end();
00364                         }
00365                         break;
00366                 case '-':
00367                         if (!newAnn.isEmpty()) {
00368                                 if (cur != newAnn.end())
00369                                         cur = newAnn.remove(cur);
00370                                 else {
00371                                         dbp("ASSERT processDiff: remove end of "
00372                                             "file, diff is %1", diff);
00373                                         isError = true;
00374                                         return;
00375                                 }
00376                         } else {
00377                                 dbp("ASSERT processDiff: remove line from "
00378                                     "empty annotation, diff is %1", diff);
00379                                 isError = true;
00380                                 return;
00381                         }
00382                         break;
00383                 case '\\':
00384                         // diff(1) produces a "\ No newline at end of file", but the
00385                         // message is locale dependent, so just test the space after '\'
00386                         if (line[1] == ' ')
00387                                 break;
00388 
00389                         // fall through
00390                 default:
00391                         ++cur;
00392                         break;
00393                 }
00394         }
00395 }
00396 
00397 void Annotate::updatePatchScript(SCRef sha, SCRef par) {
00398 
00399         if (sha == QGit::ZERO_SHA) // diff-tree --stdin doesn't work with working
00400                 return;            // dir patch will be directly fetched when needed
00401 
00402         const QString runCmd(sha + " " + par);
00403         patchScript.append(runCmd).append('\n');
00404 }
00405 
00406 const QString Annotate::getNextPatch(QString& patchFile, SCRef fileName, SCRef sha) {
00407 
00408         if (sha == QGit::ZERO_SHA) {
00409                 // diff-tree --stdin doesn't work with working dir so get it
00410                 // directly. We don't need file sha info because is ZERO_SHA
00411                 QString runOutput;
00412                 QString runCmd("git diff-index --no-color -r -m -p HEAD -- " + QGit::QUOTE_CHAR +
00413                                 fileName + QGit::QUOTE_CHAR);
00414 
00415                 git->run(runCmd, &runOutput);
00416                 if (cancelingAnnotate)
00417                         return "";
00418 
00419                 // restore removed head line of next patch
00420                 const QString nextHeader("\n:100644 100644 " + QGit::ZERO_SHA + ' '
00421                                          + nextFileSha + " M\t" + fileName + '\n');
00422                 runOutput.append(nextHeader);
00423                 patchFile.prepend(runOutput);
00424                 nextFileSha = QGit::ZERO_SHA;
00425         }
00426         // use patchProcBuf to get proper diff. Patches in patchProcBuf are
00427         // correctly ordered, so take the first patch
00428         ah[sha].fileSha = nextFileSha;
00429 
00430         bool noNewLine = (patchFile[0] == ':');
00431         if (noNewLine)
00432                 dbp("WARNING: No newline at the end of %1 patch", sha);
00433 
00434         int end = (noNewLine) ? 0 : patchFile.find("\n:");
00435         QString diff;
00436         if (end != -1) {
00437                 diff = patchFile.left(end + 1);
00438                 nextFileSha = patchFile.mid(end + 57 - (int)noNewLine, 40);
00439                 patchFile.remove(0, end + 100); // the whole line until file name
00440         } else
00441                 diff = patchFile;
00442 
00443         int start = diff.find('@');
00444         // handle a possible file mode only change and remove header
00445         diff = (start != -1) ? diff.mid(start) : "";
00446 
00447         int i = 0;
00448         while (diffMap.contains(Key(sha, i)))
00449                 i++;
00450         diffMap.insert(Key(sha, i), diff);
00451         return diff;
00452 }
00453 
00454 bool Annotate::getNextSection(SCRef d, int& idx, QString& sec, SCRef target) {
00455 
00456         if (idx >= (int)d.length())
00457                 return false;
00458 
00459         int newIdx = d.find(target, idx);
00460         if (newIdx == -1) // last section, take all
00461                 newIdx = d.length() - 1;
00462 
00463         sec = d.mid(idx, newIdx - idx + 1);
00464         idx = newIdx + 1;
00465         return true;
00466 }
00467 
00468 
00469 // ****************************** RANGE FILTER *****************************
00470 
00471 
00472 
00473 bool Annotate::getRange(SCRef sha, RangeInfo* r) {
00474 
00475         if (!rangeMap.contains(sha) || !valid || canceled) {
00476                 r->clear();
00477                 return false;
00478         }
00479         *r = rangeMap[sha]; // by copy
00480         return true;
00481 }
00482 
00483 void Annotate::updateCrossRanges(SCRef chunk, bool rev, int fileLen, int ofs, RangeInfo* r) {
00484 
00485 /* here the deal is to fake a file that will be modified by chunk, the
00486    file must contain also the whole output range.
00487 
00488    Then we apply an annotation step and see what happens...
00489 
00490    First we mark each line of the file with the corresponding line number,
00491    then apply the patch and check where the lines have been moved around.
00492 
00493    Now we have to cases:
00494 
00495    - In reverse case we infer the before-patch range knowing the after-patch range.
00496      So we check what lines we have in the region corresponding to after-patch range.
00497 
00498    - In forward case we infer the after-patch range knowing the before-patch range.
00499      So we scan the resulting annotation to find line numbers corresponding to the
00500      before-patch range.
00501 
00502 */
00503         // because of the padding the file first line number will be
00504         //
00505         // fileFirstLineNr = newLineId - beforePadding = fileOffset + 1
00506         QStringList beforeAnn, afterAnn;
00507         for (int lineNum = ofs + 1; lineNum <= ofs + fileLen; lineNum++)
00508                 beforeAnn.append(QString::number(lineNum));
00509 
00510         const QString fakedAuthor("*");
00511         setAnnotation(chunk, fakedAuthor, beforeAnn, afterAnn, ofs);
00512         int newStart = ofs + 1;
00513         int newEnd = ofs + fileLen;
00514 
00515         if (rev) {
00516                 // let's see what line number we have at given range interval limits.
00517                 // at() counts from 0.
00518                 QStringList::const_iterator itStart(afterAnn.at(r->start - ofs - 1));
00519                 QStringList::const_iterator itEnd(afterAnn.at(r->end - ofs - 1));
00520 
00521                 bool leftExtended = (*itStart == fakedAuthor);
00522                 bool rightExtended = (*itEnd == fakedAuthor);
00523 
00524                 // if range boundary is a line added by the patch
00525                 // we consider inclusive and extend the range
00526                 ++itStart;
00527                 do {
00528                         --itStart;
00529                         if (*itStart != fakedAuthor) {
00530                                 newStart = (*itStart).toInt();
00531                                 break;
00532                         }
00533                 } while (itStart != afterAnn.constBegin());
00534 
00535                 while (itEnd != afterAnn.constEnd()) {
00536 
00537                         if (*itEnd != fakedAuthor) {
00538                                 newEnd = (*itEnd).toInt();
00539                                 break;
00540                         }
00541                         ++itEnd;
00542                 }
00543                 if (leftExtended && *itStart != fakedAuthor)
00544                         newStart++;
00545 
00546                 if (rightExtended && itEnd != afterAnn.constEnd())
00547                         newEnd--;
00548 
00549                 r->modified = (leftExtended || rightExtended);
00550 
00551                 if (!r->modified) { // check for consecutive sequence
00552                         for (int i = r->start; i <= r->end; ++i, ++itStart)
00553                                 if (i - r->start != (*itStart).toInt() - newStart) {
00554                                         r->modified = true;
00555                                         break;
00556                                 }
00557                 }
00558                 if (newStart > newEnd) // selected range is whole inside new added lines
00559                         newStart = newEnd = 0;
00560 
00561         } else { // forward case
00562 
00563                 // scan afterAnn to check for before-patch range boundaries
00564                 QStringList::const_iterator itStart(afterAnn.constEnd());
00565                 QStringList::const_iterator itEnd(afterAnn.constEnd());
00566 
00567                 QStringList::const_iterator it(afterAnn.constBegin());
00568                 for (int lineNum = ofs + 1; it != afterAnn.constEnd(); ++lineNum, ++it) {
00569 
00570                         if (*it != fakedAuthor) {
00571 
00572                                 if ((*it).toInt() <= r->start) {
00573                                         newStart = lineNum;
00574                                         itStart = it;
00575                                 }
00576                                 if (  (*it).toInt() >= r->end
00577                                     && itEnd == afterAnn.constEnd()) { // one-shot
00578                                         newEnd = lineNum;
00579                                         itEnd = it;
00580                                 }
00581                         }
00582                 }
00583                 if (itStart != afterAnn.constEnd() && (*itStart).toInt() < r->start)
00584                         newStart++;
00585 
00586                 if (itEnd != afterAnn.constEnd() && (*itEnd).toInt() > r->end)
00587                         newEnd--;
00588 
00589                 r->modified = (itStart == afterAnn.constEnd() || itEnd == afterAnn.constEnd());
00590 
00591                 if (!r->modified) { // check for consecutive sequence
00592                         for (int i = r->start; i <= r->end; ++itStart, i++)
00593                                 if ((*itStart).toInt() != i) {
00594                                 r->modified = true;
00595                                 break;
00596                                 }
00597                 }
00598                 if (newStart > newEnd) // selected range has been deleted
00599                         newStart = newEnd = 0;
00600         }
00601         r->start = newStart;
00602         r->end = newEnd;
00603 }
00604 
00605 void Annotate::updateRange(RangeInfo* r, SCRef diff, bool reverse) {
00606 
00607         r->modified = false;
00608         if (r->start == 0)
00609                 return;
00610 
00611         // r(start, end) is updated after each chunk incrementally and
00612         // not at the end of the whole diff to be always in sync with
00613         // chunk headers that are updated in the same way by GNU diff.
00614         //
00615         // so in case of reverse we have to apply the chunks from last
00616         // one to first.
00617         int idx = 0;
00618         QString chunk;
00619         QStringList chunkList;
00620         while (getNextSection(diff, idx, chunk, "\n@"))
00621                 if (reverse)
00622                         chunkList.prepend(chunk);
00623                 else
00624                         chunkList.append(chunk);
00625 
00626         QStringList::const_iterator chunkIt(chunkList.constBegin());
00627         while (chunkIt != chunkList.constEnd()) {
00628 
00629                 // an unified diff fragment header has form '@@ -a,b +c,d @@'
00630                 // where 'a' is old file line number and 'b' is old file
00631                 // number of lines of the hunk, 'c' and 'd' are the same
00632                 // for new file. If the file does not have enough lines
00633                 // then also the form '@@ -a +c @@' is used.
00634                 chunk = *chunkIt++;
00635                 int m = chunk.find('-');
00636                 int c1 = chunk.find(',', m);
00637                 int p = chunk.find('+', c1);
00638                 int c2 = chunk.find(',', p);
00639                 int e = chunk.find(' ', c2);
00640 
00641                 int oldLineCnt = chunk.mid(c1 + 1, p - c1 - 2).toInt();
00642                 int newLineId = chunk.mid(p + 1, c2 - p - 1).toInt();
00643                 int newLineCnt = chunk.mid(c2 + 1, e - c2 - 1).toInt();
00644                 int lineNumDiff = newLineCnt - oldLineCnt;
00645 
00646                 // because r(start, end) is updated after each chunk we have to
00647                 // consider the updated patch delimiters to compare with r(start, end)
00648                 int patchStart = newLineId;
00649 
00650                 // patch end depends only to lines count so is...
00651                 int patchEnd = patchStart + ((reverse) ? newLineCnt : oldLineCnt);
00652                 patchEnd--; // with 1 line patch patchStart == patchEnd
00653 
00654                 // case 1: patch range after our range
00655                 if (patchStart > r->end)
00656                         continue;
00657 
00658                 // case 2: patch range before our range
00659                 if (patchEnd < r->start) {
00660                         r->start += ((reverse) ? -lineNumDiff : lineNumDiff);
00661                         r->end += ((reverse) ? -lineNumDiff : lineNumDiff);
00662                         continue;
00663                 }
00664                 // case 3: the patch is whole inside our range
00665                 if (patchStart >= r->start && patchEnd <= r->end) {
00666                         r->end += ((reverse) ? -lineNumDiff : lineNumDiff);
00667                         r->modified = true;
00668                         continue;
00669                 }
00670                 // case 4: ranges are crossing
00671                 // add padding so that resulting file is the UNION: selectRange U patchRange
00672 
00673                 // reverse independent
00674                 int beforePadding = (r->start > patchStart) ? 0 : patchStart - r->start;
00675 
00676                 // reverse dependent
00677                 int afterPadding = (patchEnd > r->end) ? 0 : r->end - patchEnd;
00678 
00679                 // file is the faked file on which we will apply the diff,
00680                 // so it is always the _old_ before the patch one.
00681                 int fileLenght = beforePadding + oldLineCnt + afterPadding;
00682 
00683                 // given the chunk header @@ -a,b +c,d @@, line nr. 'c' must correspond
00684                 // to the file line nr. 1 because we have only a partial file.
00685                 // More, there is also the file padding to consider.
00686                 // So we need that 'c' corresponds to file line nr. 'beforePadding + 1'
00687                 //
00688                 // the transformation in setAnnotation() is
00689                 //     newLineNum = 'c' - offset = beforePadding + 1
00690                 // so...
00691                 int fileOffset = newLineId - beforePadding - 1;
00692 
00693                 // because of the padding the file first line number will be
00694                 //     fileFirstLineNr = newLineId - beforePadding = fileOffset + 1
00695                 updateCrossRanges(chunk, reverse, fileLenght, fileOffset, r);
00696         }
00697 }
00698 
00699 const QString Annotate::getAncestor(SCRef sha, SCRef fileName, int* shaIdx) {
00700 
00701         QString fileSha;
00702 
00703         try {
00704                 annotateActivity = true;
00705                 EM_REGISTER(exAnnCanceled);
00706 
00707                 fileSha = git->getFileSha(fileName, sha); // calls qApp->processEvents()
00708                 if (fileSha.isEmpty()) {
00709                         dbp("ASSERT in getAncestor: empty file from %1", sha);
00710                         return "";
00711                 }
00712                 EM_REMOVE(exAnnCanceled);
00713                 annotateActivity = false;
00714 
00715         } catch(int i) {
00716 
00717                 EM_REMOVE(exAnnCanceled);
00718                 annotateActivity = false;
00719 
00720                 if (EM_MATCH(i, exAnnCanceled, "getting ancestor")) {
00721                         EM_THROW_PENDING;
00722                         return "";
00723                 }
00724                 const QString info("Exception \'" + EM_DESC(i) + "\' "
00725                                    "not handled in Annotation lookup...re-throw");
00726                 dbs(info);
00727                 throw;
00728         }
00729         // NOTE: more then one revision could have the same file sha as our
00730         // input revision. This happens if the patch is reverted or if the patch
00731         // modify only file mode but no content. From the point of view of code
00732         // range filtering this is equivalent, so we don't care to find the correct
00733         // ancestor, but just the first revision with the same file sha
00734         for (*shaIdx = 0; *shaIdx < (int)histRevOrder.count(); (*shaIdx)++) {
00735 
00736                 const FileAnnotation& fa(ah[histRevOrder[*shaIdx]]);
00737                 if (fa.fileSha == fileSha)
00738                         return histRevOrder[*shaIdx];
00739         }
00740         // ok still not found, this could happen if sha is an unapplied
00741         // stgit patch. In this case fall back on the first in the list
00742         // that is the newest.
00743         if (git->getAllRefSha(Git::UN_APPLIED).contains(sha))
00744                 return histRevOrder.first();
00745 
00746         dbp("ASSERT in getAncestor: ancestor of %1 not found", sha);
00747         return "";
00748 }
00749 
00750 bool Annotate::isDescendant(SCRef sha, SCRef target) {
00751 // quickly check if target is a direct descendant of sha, i.e. if starting
00752 // from target, sha could be reached walking along his parents. In case
00753 // a merge is found the search returns false because you'll need,
00754 // in general, all the previous ranges to compute the target one.
00755 
00756         const Rev* r = git->revLookup(sha, fh);
00757         if (!r)
00758                 return false;
00759 
00760         int shaIdx = r->orderIdx;
00761         r = git->revLookup(target, fh);
00762 
00763         while (r && r->orderIdx < shaIdx && r->parentsCount() == 1)
00764                 r = git->revLookup(r->parent(0), fh);
00765 
00766         return (r && r->orderIdx == shaIdx);
00767 }
00768 
00769 /*
00770   Range filtering uses only annotated added/removed lines to compute ranges,
00771   patch content is never checked, this algorithm is fast but fails in case of
00772   code shuffle; if a patch moves some code between two independents part of the
00773   same file this will be interpreted as a delete of origin code. New added code
00774   is not recognized as the same of old one because content is not checked.
00775 
00776   Only checking for copies would fix the corner case, but implementation
00777   is too difficult, so better accept this design limitation for now.
00778 */
00779 const QString Annotate::computeRanges(SCRef sha, int rangeStart, int rangeEnd, SCRef target) {
00780 
00781         rangeMap.clear();
00782 
00783         if (!valid || canceled) {
00784                 dbp("ASSERT in computeRanges: annotation from %1 not valid", sha);
00785                 return "";
00786         }
00787         QString ancestor(sha);
00788         int shaIdx;
00789         for (shaIdx = 0; shaIdx < (int)histRevOrder.count(); shaIdx++)
00790                 if (histRevOrder[shaIdx] == sha)
00791                         break;
00792 
00793         if (shaIdx == (int)histRevOrder.count()) { // not in history, find an ancestor
00794                 ancestor = getAncestor(sha, fileName, &shaIdx);
00795                 if (ancestor.isEmpty())
00796                         return "";
00797         }
00798         // insert starting one, always included by default, could be removed after
00799         rangeMap.insert(ancestor, RangeInfo(rangeStart, rangeEnd, true));
00800 
00801         // check if target is a descendant, so to skip back history walking
00802         bool isDirectDescendant = isDescendant(ancestor, target);
00803 
00804         // going back in history, to oldest following first parent lane
00805         const QString oldest(histRevOrder.last()); // causes a detach!
00806         const Rev* curRev = git->revLookup(ancestor, fh); // historyRevs
00807         QString curRevSha(curRev->sha());
00808         while (curRevSha != oldest && !isDirectDescendant) {
00809 
00810                 if (!diffMap.contains(Key(curRevSha, 0))) {
00811                         if (curRev->parentsCount() == 0)  // is initial
00812                                 break;
00813 
00814                         dbp("ASSERT in rangeFilter 1: diff for %1 not found", curRevSha);
00815                         return "";
00816                 }
00817                 RangeInfo r(rangeMap[curRevSha]);
00818                 updateRange(&r, diffMap[Key(curRevSha, 0)], true);
00819 
00820                 // special case for modified flag. Mark always the 'after patch' revision
00821                 // with modified flag, not the before patch. So we have to stick the flag
00822                 // to the newer revision.
00823                 rangeMap[curRevSha].modified = r.modified;
00824 
00825                 // if the second revision does not modify the range then r.modified == false
00826                 // and the first revision range is created with modified == false, if the
00827                 // first revision is initial then the loop is exited without update the flag
00828                 // and the first revision is missed.
00829                 //
00830                 // we want to always include first revision as a compare base also if
00831                 // does not modify anything. Of course range must be valid.
00832                 r.modified = (r.start != 0);
00833 
00834                 if (curRev->parentsCount() == 0)
00835                         break;
00836 
00837                 curRev = git->revLookup(curRev->parent(0), fh);
00838                 curRevSha = curRev->sha();
00839                 rangeMap.insert(curRevSha, r);
00840 
00841                 if (curRevSha == target) // stop now, no need to continue
00842                         return ancestor;
00843         }
00844         // now that we have initial revision go back and sweep up all the
00845         // remaining stuff, we are guaranteed a good parent is always
00846         // found but in case of an independent branch, see below
00847         if (!isDirectDescendant)
00848                 shaIdx = histRevOrder.count() - 1;
00849 
00850         for ( ; shaIdx >= 0; shaIdx--) {
00851 
00852                 SCRef sha(histRevOrder[shaIdx]);
00853 
00854                 if (!rangeMap.contains(sha)) {
00855 
00856                         curRev = git->revLookup(sha, fh);
00857 
00858                         if (curRev->parentsCount() == 0) {
00859                                 // the start of an independent branch is found in this case
00860                                 // insert an empty range, the whole branch will be ignored.
00861                                 // Merge of outside branches are very rare so this solution
00862                                 // seems enough if we don't want to dive in (useless) complications.
00863                                 rangeMap.insert(sha, RangeInfo());
00864                                 continue;
00865                         }
00866                         if (!diffMap.contains(Key(sha, 0))) {
00867                                 dbp("ASSERT in rangeFilter 2: diff for %1 not found", sha);
00868                                 return "";
00869                         }
00870                         SCRef parSha(curRev->parent(0));
00871 
00872                         if (!rangeMap.contains(parSha)) {
00873 
00874                                 if (isDirectDescendant) // we must be in a parallel lane, no need
00875                                         continue;       // to compute range info, simply go on
00876 
00877                                 dbp("ASSERT in rangeFilter: range info for %1 not found", parSha);
00878                                 return "";
00879                         }
00880                         RangeInfo r(rangeMap[parSha]);
00881                         updateRange(&r, diffMap[Key(sha, 0)], false);
00882                         rangeMap.insert(sha, r);
00883 
00884                         if (sha == target) // stop now, no need to continue
00885                                 return ancestor;
00886                 }
00887         }
00888         return ancestor;
00889 }
00890 
00891 bool Annotate::seekPosition(int* paraFrom, int* paraTo, SCRef fromSha, SCRef toSha) {
00892 
00893         if ((*paraFrom == 0 && *paraTo == 0) || fromSha == toSha)
00894                 return true;
00895 
00896         QMap<QString, RangeInfo> backup;
00897         backup = rangeMap;  // QMap is implicitly shared
00898 
00899         // paragraphs start from 0 but ranges from 1
00900         if (computeRanges(fromSha, *paraFrom + 1, *paraTo + 1, toSha).isEmpty())
00901                 goto fail;
00902 
00903         if (!rangeMap.contains(toSha))
00904                 goto fail;
00905 
00906         *paraFrom = rangeMap[toSha].start - 1;
00907         *paraTo = rangeMap[toSha].end - 1;
00908         rangeMap = backup;
00909         return true;
00910 
00911 fail:
00912         rangeMap = backup;
00913         return false;
00914 }

Generated on Fri Dec 7 21:57:36 2007 for QGit by  doxygen 1.5.3