Logo Search packages:      
Sourcecode: qgit version File versions

annotate.cpp

/*
      Description: file history annotation

      Author: Marco Costalba (C) 2005-2006

      Copyright: See COPYING file that comes with this distribution

*/
#include <qapplication.h>
#include "git.h"
#include "annotate.h"

#define MAX_AUTHOR_LEN 16

Annotate::Annotate(Git* parent, QObject* guiObj) : QObject(parent) {

      EM_INIT(exAnnCanceled, "Canceling annotation");

      git = parent;
      gui = guiObj;
      cancelingAnnotate = annotateRunning = annotateActivity = false;
      valid = canceled = false;

      patchProcBuf.reserve(1000000); // avoid repeated reallocation, will be big!

      processingTime.start();

      connect(&patchProc, SIGNAL(readyReadStdout()), this, SLOT(on_patchProc_readFromStdout()));
      connect(&patchProc, SIGNAL(processExited()), this, SLOT(on_patchProc_processExited()));
}

const FileAnnotation* Annotate::lookupAnnotation(SCRef sha, SCRef fn) {

      if (!valid || fileName != fn)
            return NULL;

      AnnotateHistory::const_iterator it = ah.find(sha);
      if (it != ah.constEnd())
            return &(it.data());

      // ok, we are not lucky. Check for an ancestor before to give up
      int shaIdx;
      const QString ancestorSha = getAncestor(sha, fileName, &shaIdx);
      if (!ancestorSha.isEmpty()) {
            it = ah.find(ancestorSha);
            if (it != ah.constEnd())
                  return &(it.data());
      }
      return NULL;
}

bool Annotate::startPatchProc(SCRef buf, SCRef fileName) {

      QString cmd("git diff-tree -r -m --patch-with-raw --no-commit-id --stdin --");
      QStringList args(QStringList::split(' ', cmd));
      args.append(fileName); // handle file name with spaces case
      patchProc.setArguments(args);
      patchProc.setWorkingDirectory(git->workDir);
      patchProcBuf = "";
      return patchProc.launch(buf);
}

void Annotate::on_patchProc_readFromStdout() {

      const QString tmp(patchProc.readStdout());
      annFilesNum += tmp.contains("diff --git ");
      if (annFilesNum > (int)histRevOrder.count())
            annFilesNum = histRevOrder.count();
      patchProcBuf.append(tmp);
}

void Annotate::deleteWhenDone() {

      if (!EM_IS_PENDING(exAnnCanceled))
            EM_RAISE(exAnnCanceled);

      if (annotateRunning)
            cancelingAnnotate = true;

      if (patchProc.isRunning())
            patchProc.tryTerminate();

      on_deleteWhenDone();
}

void Annotate::on_deleteWhenDone() {

      if (!(annotateRunning || EM_IS_PENDING(exAnnCanceled)))
            deleteLater();
      else
            QTimer::singleShot(20, this, SLOT(on_deleteWhenDone()));
}

void Annotate::on_progressTimer_timeout() {

      if (!cancelingAnnotate && !isError) {
            const QString n(QString::number(annFilesNum));
            QApplication::postEvent(gui, new AnnotateProgressEvent(n));
      }
}

bool Annotate::start(const FileHistory* _fh) {

      // could change during annotation, so save them
      fh = _fh;
      fileName = fh->fileName;
      histRevOrder = fh->revOrder;

      if (histRevOrder.isEmpty()) {
            valid = false;
            return false;
      }
      annotateRunning = true;

      // init AnnotateHistory
      annFilesNum = 0;
      annId = histRevOrder.count();
      annNumLen = QString::number(histRevOrder.count()).length();
      StrVect::const_iterator it(histRevOrder.constBegin());
      do
            ah.insert(*it, FileAnnotation(annId--));
      while (++it != histRevOrder.constEnd());

      // annotation is splitted in two parts.
      // first we get the list of sha's to feed git diff-tree.
      // This is very fast.
      isError = false;
      annotateFileHistory(fileName, true);

      if (isError || cancelingAnnotate) {
            slotComputeDiffs(); // clean-up in case of error/canceling
            return false;
      }
      connect(&progressTimer, SIGNAL(timeout()), this, SLOT(on_progressTimer_timeout()));
      progressTimer.start(500);

      // now we get the patches with an async call. This is the slowest part.
      return startPatchProc(patchScript, fileName);
}

void Annotate::on_patchProc_processExited() {

      // start computing diffs only on return from event handler
      QTimer::singleShot(1, this, SLOT(slotComputeDiffs()));
      progressTimer.stop();
}

void Annotate::slotComputeDiffs() {

      // when all the patches are loaded we compute the annotation.
      // this part is normally faster then getting the patches.
      if (!cancelingAnnotate) {

            diffMap.clear();
            AnnotateHistory::iterator it(ah.begin());
            do
                  (*it).isValid = false; // reset flags
            while (++it != ah.end());

            // remove first 'next patch' marker
            int first = patchProcBuf.find(':');
            if (first != -1) {
                  nextFileSha = patchProcBuf.mid(first + 56, 40);
                  patchProcBuf.remove(0, first + 100);
            }
            annotateFileHistory(fileName, false); // now could call Qt event loop
      }
      valid = !(isError || cancelingAnnotate);
      canceled = cancelingAnnotate;
      cancelingAnnotate = annotateRunning = false;
      if (canceled)
            deleteWhenDone();
      else
            git->annotateExited(this);

//    StrVect::const_iterator it(histRevOrder.constBegin());
//    do {
//          dbg(*it); dbg(ah[*it].fileSha);
//    } while (++it != histRevOrder.constEnd());
}

void Annotate::annotateFileHistory(SCRef fileName, bool buildPatchScript) {

      // sweep from the oldest to newest so that parent
      // annotations are calculated before childern
      StrVect::const_iterator it(histRevOrder.constEnd());
      do {
            --it;
            doAnnotate(fileName, *it, buildPatchScript);
      } while (it != histRevOrder.constBegin() && !isError && !cancelingAnnotate);
}

void Annotate::doAnnotate(SCRef fileName, SCRef sha, bool buildPatchScript) {
// all the parents annotations must be valid here

      FileAnnotation* fa = getFileAnnotation(sha);
      if (fa == NULL || fa->isValid || isError || cancelingAnnotate)
            return;

      const Rev* r = git->revLookup(sha, fh); // historyRevs
      if (r == NULL) {
            dbp("ASSERT doAnnotate: no revision %1", sha);
            isError = true;
            return;
      }
      if (r->parentsCount() == 0) { // initial revision
            if (!buildPatchScript)
                  setInitialAnnotation(fileName, sha, fa); // calls Qt event loop
            fa->isValid = true;
            return;
      }
      // now create a new annotation from first parent diffs
      const QStringList parents(r->parents());
      const QString& parSha = parents.first();
      FileAnnotation* pa = getFileAnnotation(parSha);

      if (!pa->isValid) {
            dbp("ASSERT in doAnnotate: annotation for %1 not valid", parSha);
            isError = true;
            return;
      }
      if (buildPatchScript) // just prepare patch script
            updatePatchScript(sha, parSha);
      else {
            const QString diff(getNextPatch(patchProcBuf, fileName, sha));
            const QString author(setupAuthor(r->author(), fa->annId));
            setAnnotation(diff, author, pa->lines, fa->lines);
      }
      // then add other parents diff if any
      QStringList::const_iterator it(parents.constBegin());
      ++it;
      while (it != parents.constEnd()) {

            FileAnnotation* pa = getFileAnnotation(*it);

            if (buildPatchScript) { // just prepare patch script
                  updatePatchScript(sha, *it);
                  ++it;
                  continue;
            }
            const QString diff(getNextPatch(patchProcBuf, fileName, sha));
            QStringList tmpAnn;
            setAnnotation(diff, "Merge", pa->lines, tmpAnn);

            // the two annotations must be of the same length
            if (fa->lines.count() != tmpAnn.count()) {
                  qDebug("ASSERT: merging annotations of different length\n"
                         " merging %s in %s", (*it).latin1(), sha.latin1());
                  isError = true;
                  return;
            }
            // finally we unify the annotations
            unify(tmpAnn, fa->lines);
            fa->lines = tmpAnn;
            ++it;
      }
      fa->isValid = true;
}

FileAnnotation* Annotate::getFileAnnotation(SCRef sha) {

      AnnotateHistory::iterator it(ah.find(sha));
      if (it == ah.end()) {
            dbp("ASSERT getFileAnnotation: no revision %1", sha);
            isError = true;
            return NULL;
      }
      return &(*it);
}

void Annotate::setInitialAnnotation(SCRef fileName, SCRef sha, FileAnnotation* fa) {

      QString f;
      SCRef fileSha(git->getFileSha(fileName, sha)); // calls Qt event loop
      if (cancelingAnnotate)
            return;

      if (fileSha.isEmpty()) {
            dbp("ASSERT in setInitialAnnotation: empty file of initial rev %1", sha);
            isError = true;
            return;
      }
      ah[sha].fileSha = fileSha;

      git->getFile(fileName, sha, NULL, &f); // blocking call, calls Qt event loop
      if (cancelingAnnotate)
            return;

      int lineNum = f.contains('\n');
      if (!f.endsWith("\n")) // No newline at end of file
            lineNum++;

      fa->lines.insert(fa->lines.end(), lineNum, "");
}

const QString Annotate::setupAuthor(SCRef origAuthor, int annId) {

      QString author(QString("%1.").arg(annId, annNumLen)); // first field is annotation id
      QString tmp(origAuthor.section('<', 0, 0).stripWhiteSpace()); // strip e-mail address
      if (tmp.isEmpty()) { // probably only e-mail
            tmp = origAuthor;
            tmp.remove('<').remove('>');
            tmp = tmp.stripWhiteSpace();
            tmp.truncate(MAX_AUTHOR_LEN);
      }
      // shrink author name if necessary
      if (tmp.length() > MAX_AUTHOR_LEN) {
            SCRef firstName(tmp.section(' ', 0, 0));
            SCRef surname(tmp.section(' ', 1));
            if (!firstName.isEmpty() && !surname.isEmpty())
                  tmp = firstName.left(1) + ". " + surname;
            tmp.truncate(MAX_AUTHOR_LEN);
      }
      author.append(tmp);
      return author;
}

void Annotate::unify(SList dst, SCList src) {

      QStringList::Iterator itd(dst.begin());
      QStringList::const_iterator its(src.constBegin());
      for ( ; itd != dst.end(); ++itd, ++its)
            if (*itd == "Merge")
                  *itd = *its;
}

void Annotate::setAnnotation(SCRef diff, SCRef author, SCList prevAnn,
                             QStringList& newAnn, int ofs) {
      newAnn = prevAnn;
      QStringList::iterator cur(newAnn.begin());
      QString line;
      int idx = 0, num, lineNumStart, lineNumEnd;
      while (getNextSection(diff, idx, line, "\n")) {
            char firstChar = line[0].latin1();
            switch (firstChar) {
            case '@':
                  // an unified diff fragment header has form '@@ -a,b +c,d @@'
                  // where 'a' is old file line number and 'b' is old file
                  // number of lines of the hunk, 'c' and 'd' are the same
                  // for new file. If the file does not have enough lines
                  // then also the form '@@ -a +c @@' is used.
                  lineNumStart = line.find('+') + 1;
                  lineNumEnd = line.find(',', lineNumStart);
                  if (lineNumEnd == -1) // small file case
                        lineNumEnd = line.find(' ', lineNumStart);

                  num = line.mid(lineNumStart, lineNumEnd - lineNumStart).toInt();
                  num -= ofs; // offset for range filter computation

                  // diff lines start from 1, 0 is empty file,
                  // instead QValueList::at() starts from 0
                  if (num <= 0) { // file is deleted
                        if (num < 0)
                              dbp("ASSERT processDiff: start line number is %1", num);
                        newAnn.clear();
                        return;
                  }
                  cur = newAnn.at(num - 1);
                  break;
            case '+':
                  if (cur != newAnn.end()) {
                        cur = newAnn.insert(cur, author);
                        ++cur;
                  } else {
                        newAnn.append(author);
                        cur = newAnn.end();
                  }
                  break;
            case '-':
                  if (!newAnn.isEmpty()) {
                        if (cur != newAnn.end())
                              cur = newAnn.remove(cur);
                        else {
                              dbp("ASSERT processDiff: remove end of "
                                  "file, diff is %1", diff);
                              isError = true;
                              return;
                        }
                  } else {
                        dbp("ASSERT processDiff: remove line from "
                            "empty annotation, diff is %1", diff);
                        isError = true;
                        return;
                  }
                  break;
            case '\\':
                  // diff(1) produces a "\ No newline at end of file", but the
                  // message is locale dependent, so just test the space after '\'
                  if (line[1] == ' ')
                        break;
                  else
                        ; // fall through
            default:
                  ++cur;
                  break;
            }
      }
}

void Annotate::updatePatchScript(SCRef sha, SCRef par) {

      if (sha == QGit::ZERO_SHA) // diff-tree --stdin doesn't work with working
            return;            // dir patch will be directly fetched when needed

      const QString runCmd(sha + " " + par);
      patchScript.append(runCmd).append('\n');
}

const QString Annotate::getNextPatch(QString& patchFile, SCRef fileName, SCRef sha) {

      if (sha == QGit::ZERO_SHA) {
            // diff-tree --stdin doesn't work with working dir so get it
            // directly. We don't need file sha info because is ZERO_SHA
            QString runOutput;
            QString runCmd("git diff-index -r -m -p HEAD -- " + QGit::QUOTE_CHAR +
                        fileName + QGit::QUOTE_CHAR);

            git->run(runCmd, &runOutput);
            if (cancelingAnnotate)
                  return "";

            // restore removed head line of next patch
            const QString nextHeader("\n:100644 100644 " + QGit::ZERO_SHA + ' '
                                     + nextFileSha + " M\t" + fileName + '\n');
            runOutput.append(nextHeader);
            patchFile.prepend(runOutput);
            nextFileSha = QGit::ZERO_SHA;
      }
      // use patchProcBuf to get proper diff. Patches in patchProcBuf are
      // correctly ordered, so take the first patch
      ah[sha].fileSha = nextFileSha;

      bool noNewLine = (patchFile[0] == ':');
      if (noNewLine)
            dbp("WARNING: No newline at the end of %1 patch", sha);

      int end = (noNewLine) ? 0 : patchFile.find("\n:");
      QString diff;
      if (end != -1) {
            diff = patchFile.left(end + 1);
            nextFileSha = patchFile.mid(end + 57 - (int)noNewLine, 40);
            patchFile.remove(0, end + 100); // the whole line until file name
      } else
            diff = patchFile;

      int start = diff.find('@');
      // handle a possible file mode only change and remove header
      diff = (start != -1) ? diff.mid(start) : "";

      int i = 0;
      while (diffMap.contains(Key(sha, i)))
            i++;
      diffMap.insert(Key(sha, i), diff);
      return diff;
}

bool Annotate::getNextSection(SCRef d, int& idx, QString& sec, SCRef target) {

      if (idx >= (int)d.length())
            return false;

      int newIdx = d.find(target, idx);
      if (newIdx == -1) // last section, take all
            newIdx = d.length() - 1;

      sec = d.mid(idx, newIdx - idx + 1);
      idx = newIdx + 1;
      return true;
}


// ****************************** RANGE FILTER *****************************



bool Annotate::getRange(SCRef sha, RangeInfo* r) {

      if (!rangeMap.contains(sha) || !valid || canceled) {
            r->clear();
            return false;
      }
      *r = rangeMap[sha]; // by copy
      return true;
}

void Annotate::updateCrossRanges(SCRef chunk, bool rev, int fileLen, int ofs, RangeInfo* r) {

/* here the deal is to fake a file that will be modified by chunk, the
   file must contain also the whole output range.

   Then we apply an annotation step and see what happens...

   First we mark each line of the file with the corresponding line number,
   then apply the patch and check where the lines have been moved around.

   Now we have to cases:

   - In reverse case we infer the before-patch range knowing the after-patch range.
     So we check what lines we have in the region corresponding to after-patch range.

   - In forward case we infer the after-patch range knowing the before-patch range.
     So we scan the resulting annotation to find line numbers corresponding to the
     before-patch range.

*/
      // because of the padding the file first line number will be
      //
      // fileFirstLineNr = newLineId - beforePadding = fileOffset + 1
      QStringList beforeAnn, afterAnn;
      for (int lineNum = ofs + 1; lineNum <= ofs + fileLen; lineNum++)
            beforeAnn.append(QString::number(lineNum));

      const QString fakedAuthor("*");
      setAnnotation(chunk, fakedAuthor, beforeAnn, afterAnn, ofs);
      int newStart = ofs + 1;
      int newEnd = ofs + fileLen;

      if (rev) {
            // let's see what line number we have at given range interval limits.
            // at() counts from 0.
            QStringList::const_iterator itStart(afterAnn.at(r->start - ofs - 1));
            QStringList::const_iterator itEnd(afterAnn.at(r->end - ofs - 1));

            bool leftExtended = (*itStart == fakedAuthor);
            bool rightExtended = (*itEnd == fakedAuthor);

            // if range boundary is a line added by the patch
            // we consider inclusive and extend the range
            ++itStart;
            do {
                  --itStart;
                  if (*itStart != fakedAuthor) {
                        newStart = (*itStart).toInt();
                        break;
                  }
            } while (itStart != afterAnn.constBegin());

            while (itEnd != afterAnn.constEnd()) {

                  if (*itEnd != fakedAuthor) {
                        newEnd = (*itEnd).toInt();
                        break;
                  }
                  ++itEnd;
            }
            if (leftExtended && *itStart != fakedAuthor)
                  newStart++;

            if (rightExtended && itEnd != afterAnn.constEnd())
                  newEnd--;

            r->modified = (leftExtended || rightExtended);

            if (!r->modified) { // check for consecutive sequence
                  for (int i = r->start; i <= r->end; ++i, ++itStart)
                        if (i - r->start != (*itStart).toInt() - newStart) {
                              r->modified = true;
                              break;
                        }
            }
            if (newStart > newEnd) // selected range is whole inside new added lines
                  newStart = newEnd = 0;

      } else { // forward case

            // scan afterAnn to check for before-patch range boundaries
            QStringList::const_iterator itStart(afterAnn.constEnd());
            QStringList::const_iterator itEnd(afterAnn.constEnd());

            QStringList::const_iterator it(afterAnn.constBegin());
            for (int lineNum = ofs + 1; it != afterAnn.constEnd(); ++lineNum, ++it) {

                  if (*it != fakedAuthor) {

                        if ((*it).toInt() <= r->start) {
                              newStart = lineNum;
                              itStart = it;
                        }
                        if (  (*it).toInt() >= r->end
                            && itEnd == afterAnn.constEnd()) { // one-shot
                              newEnd = lineNum;
                              itEnd = it;
                        }
                  }
            }
            if (itStart != afterAnn.constEnd() && (*itStart).toInt() < r->start)
                  newStart++;

            if (itEnd != afterAnn.constEnd() && (*itEnd).toInt() > r->end)
                  newEnd--;

            r->modified = (itStart == afterAnn.constEnd() || itEnd == afterAnn.constEnd());

            if (!r->modified) { // check for consecutive sequence
                  for (int i = r->start; i <= r->end; ++itStart, i++)
                        if ((*itStart).toInt() != i) {
                        r->modified = true;
                        break;
                        }
            }
            if (newStart > newEnd) // selected range has been deleted
                  newStart = newEnd = 0;
      }
      r->start = newStart;
      r->end = newEnd;
}

void Annotate::updateRange(RangeInfo* r, SCRef diff, bool reverse) {

      r->modified = false;
      if (r->start == 0)
            return;

      // r(start, end) is updated after each chunk incrementally and
      // not at the end of the whole diff to be always in sync with
      // chunk headers that are updated in the same way by GNU diff.
      //
      // so in case of reverse we have to apply the chunks from last
      // one to first.
      int idx = 0;
      QString chunk;
      QStringList chunkList;
      while (getNextSection(diff, idx, chunk, "\n@"))
            if (reverse)
                  chunkList.prepend(chunk);
            else
                  chunkList.append(chunk);

      QStringList::const_iterator chunkIt(chunkList.constBegin());
      while (chunkIt != chunkList.constEnd()) {

            // an unified diff fragment header has form '@@ -a,b +c,d @@'
            // where 'a' is old file line number and 'b' is old file
            // number of lines of the hunk, 'c' and 'd' are the same
            // for new file. If the file does not have enough lines
            // then also the form '@@ -a +c @@' is used.
            chunk = *chunkIt++;
            int m = chunk.find('-');
            int c1 = chunk.find(',', m);
            int p = chunk.find('+', c1);
            int c2 = chunk.find(',', p);
            int e = chunk.find(' ', c2);

            int oldLineCnt = chunk.mid(c1 + 1, p - c1 - 2).toInt();
            int newLineId = chunk.mid(p + 1, c2 - p - 1).toInt();
            int newLineCnt = chunk.mid(c2 + 1, e - c2 - 1).toInt();
            int lineNumDiff = newLineCnt - oldLineCnt;

            // because r(start, end) is updated after each chunk we have to
            // consider the updated patch delimiters to compare with r(start, end)
            int patchStart = newLineId;

            // patch end depends only to lines count so is...
            int patchEnd = patchStart + ((reverse) ? newLineCnt : oldLineCnt);
            patchEnd--; // with 1 line patch patchStart == patchEnd

            // case 1: patch range after our range
            if (patchStart > r->end)
                  continue;

            // case 2: patch range before our range
            if (patchEnd < r->start) {
                  r->start += ((reverse) ? -lineNumDiff : lineNumDiff);
                  r->end += ((reverse) ? -lineNumDiff : lineNumDiff);
                  continue;
            }
            // case 3: the patch is whole inside our range
            if (patchStart >= r->start && patchEnd <= r->end) {
                  r->end += ((reverse) ? -lineNumDiff : lineNumDiff);
                  r->modified = true;
                  continue;
            }
            // case 4: ranges are crossing
            // add padding so that resulting file is the UNION: selectRange U patchRange

            // reverse independent
            int beforePadding = (r->start > patchStart) ? 0 : patchStart - r->start;

            // reverse dependent
            int afterPadding = (patchEnd > r->end) ? 0 : r->end - patchEnd;

            // file is the faked file on which we will apply the diff,
            // so it is always the _old_ before the patch one.
            int fileLenght = beforePadding + oldLineCnt + afterPadding;

            // given the chunk header @@ -a,b +c,d @@, line nr. 'c' must correspond
            // to the file line nr. 1 because we have only a partial file.
            // More, there is also the file padding to consider.
            // So we need that 'c' corresponds to file line nr. 'beforePadding + 1'
            //
            // the transformation in setAnnotation() is
            //     newLineNum = 'c' - offset = beforePadding + 1
            // so...
            int fileOffset = newLineId - beforePadding - 1;

            // because of the padding the file first line number will be
            //     fileFirstLineNr = newLineId - beforePadding = fileOffset + 1
            updateCrossRanges(chunk, reverse, fileLenght, fileOffset, r);
      }
}

const QString Annotate::getAncestor(SCRef sha, SCRef fileName, int* shaIdx) {

      QString fileSha;

      try {
            annotateActivity = true;
            EM_REGISTER(exAnnCanceled);

            fileSha = git->getFileSha(fileName, sha); // calls qApp->processEvents()
            if (fileSha.isEmpty()) {
                  dbp("ASSERT in getAncestor: empty file from %1", sha);
                  return "";
            }
            EM_REMOVE(exAnnCanceled);
            annotateActivity = false;

      } catch(int i) {

            EM_REMOVE(exAnnCanceled);
            annotateActivity = false;

            if (EM_MATCH(i, exAnnCanceled, "getting ancestor")) {
                  EM_THROW_PENDING;
                  return "";
            }
            const QString info("Exception \'" + EM_DESC(i) + "\' "
                               "not handled in Annotation lookup...re-throw");
            dbs(info);
            throw;
      }
      // NOTE: more then one revision could have the same file sha as our
      // input revision. This happens if the patch is reverted or if the patch
      // modify only file mode but no content. From the point of view of code
      // range filtering this is equivalent, so we don't care to find the correct
      // ancestor, but just the first revision with the same file sha
      for (*shaIdx = 0; *shaIdx < (int)histRevOrder.count(); (*shaIdx)++) {

            const FileAnnotation& fa(ah[histRevOrder[*shaIdx]]);
            if (fa.fileSha == fileSha)
                  return histRevOrder[*shaIdx];
      }
      dbp("ASSERT in getAncestor: ancestor of %1 not found", sha);
      return "";
}

/*
  Range filtering uses only annotated added/removed lines to compute ranges,
  patch content is never checked, this algorithm is fast but fails in case of
  code shuffle; if a patch moves some code between two independents part of the
  same file this will be interpreted as a delete of origin code. New added code
  is not recognized as the same of old one because content is not checked.

  Only checking for copies would fix the corner case, but implementation
  is too difficult, so better accept this design limitation for now.
*/
const QString Annotate::computeRanges(SCRef sha, int rangeStart, int rangeEnd, SCRef target) {

      rangeMap.clear();

      if (!valid || canceled) {
            dbp("ASSERT in computeRanges: annotation from %1 not valid", sha);
            return "";
      }
      QString ancestor(sha);
      int shaIdx;
      for (shaIdx = 0; shaIdx < (int)histRevOrder.count(); shaIdx++)
            if (histRevOrder[shaIdx] == sha)
                  break;

      if (shaIdx == (int)histRevOrder.count()) { // not in history, find an ancestor
            ancestor = getAncestor(sha, fileName, &shaIdx);
            if (ancestor.isEmpty())
                  return "";
      }
      // insert starting one, always included by default, could be removed after
      rangeMap.insert(ancestor, RangeInfo(rangeStart, rangeEnd, true));

      // going back in history, to oldest following first parent lane
      const QString oldest(histRevOrder.last()); // causes a detach!
      const Rev* curRev = git->revLookup(ancestor, fh); // historyRevs
      QString curRevSha(curRev->sha());
      while (curRevSha != oldest) {

            if (!diffMap.contains(Key(curRevSha, 0))) {
                  if (curRev->parentsCount() == 0)  // is initial
                        break;

                  dbp("ASSERT in rangeFilter 1: diff for %1 not found", curRevSha);
                  return "";
            }
            RangeInfo r(rangeMap[curRevSha]);
            updateRange(&r, diffMap[Key(curRevSha, 0)], true);

            // special case for modified flag. Mark always the 'after patch' revision
            // with modified flag, not the before patch. So we have to stick the flag
            // to the newer revision.
            rangeMap[curRevSha].modified = r.modified;

            // if the second revision does not modify the range then r.modified == false
            // and the first revision range is created with modified == false, if the
            // first revision is initial then the loop is exited without update the flag
            // and the first revision is missed.
            //
            // we want to always include first revision as a compare base also if
            // does not modify anything. Of course range must be valid.
            r.modified = (r.start != 0);

            if (curRev->parentsCount() == 0)
                  break;

            curRev = git->revLookup(curRev->parent(0), fh);
            curRevSha = curRev->sha();
            rangeMap.insert(curRevSha, r);

            if (curRevSha == target) // stop now, no need to continue
                  return ancestor;
      }
      // now that we have initial revision go back and sweep up all the
      // remaining stuff, we are guaranteed a good parent is always
      // found but in case of an indipendent branch, see below
      for (shaIdx = histRevOrder.count() - 1; shaIdx >= 0; shaIdx--) {

            SCRef sha(histRevOrder[shaIdx]);

            if (!rangeMap.contains(sha)) {

                  curRev = git->revLookup(sha, fh);

                  if (curRev->parentsCount() == 0) {
                        // the start of an indipendent branch is found in this case
                        // insert an empty range, the whole branch will be ignored.
                        // Merge of outside branches are very rare so this solution
                        // seems enough if we don't want to dive in (useless) complications.
                        rangeMap.insert(sha, RangeInfo());
                        continue;
                  }
                  if (!diffMap.contains(Key(sha, 0))) {
                        dbp("ASSERT in rangeFilter 2: diff for %1 not found", sha);
                        return "";
                  }
                  SCRef parSha(curRev->parent(0));

                  if (!rangeMap.contains(parSha)) {
                        dbp("ASSERT in rangeFilter: range info for %1 not found", parSha);
                        return "";
                  }
                  RangeInfo r(rangeMap[parSha]);
                  updateRange(&r, diffMap[Key(sha, 0)], false);
                  rangeMap.insert(sha, r);

                  if (sha == target) // stop now, no need to continue
                        return ancestor;
            }
      }
      return ancestor;
}

bool Annotate::seekPosition(int* paraFrom, int* paraTo, SCRef fromSha, SCRef toSha) {

      if (paraFrom == 0 || fromSha == toSha)
            return true;

      QMap<QString, RangeInfo> backup;
      backup = rangeMap;  // QMap is implicitly shared

      // paragrahps start from 0 but ranges from 1
      if (computeRanges(fromSha, *paraFrom + 1, *paraTo + 1, toSha).isEmpty())
            goto fail;

      if (!rangeMap.contains(toSha))
            goto fail;

      *paraFrom = rangeMap[toSha].start - 1;
      *paraTo = rangeMap[toSha].end - 1;
      rangeMap = backup;
      return true;

fail:
      rangeMap = backup;
      return false;
}

Generated by  Doxygen 1.6.0   Back to index