Logo Search packages:      
Sourcecode: qgit version File versions  Download package

git.cpp

/*
      Description: interface to git programs

      Author: Marco Costalba (C) 2005-2006

      Copyright: See COPYING file that comes with this distribution

*/
#include <sys/types.h> // used by chmod()
#include <sys/stat.h>  // used by chmod()
#include <qapplication.h>
#include <qdatetime.h>
#include <qtextcodec.h>
#include <qregexp.h>
#include <qsettings.h>
#include <qfile.h>
#include <qdir.h>
#include <qeventloop.h>
#include <qprocess.h>
#include <qtextcodec.h>
#include <qstylesheet.h>
#include "mainimpl.h"
#include "annotate.h"
#include "cache.h"
#include "dataloader.h"
#include "git.h"

using namespace QGit;

Git::Git(QWidget* p, const char* n) : QObject(p, n), par(p) {

      EM_INIT(exGitStopped, "Stopping connection with git");

      cacheNeedsUpdate = isMergeHead = false;
      isStGIT = isGIT = loadingUnAppliedPatches = false;
      errorReportingEnabled = true; // report errors if run() fails
      runningProcesses = 0;

      revs.resize(MAX_DICT_SIZE);
      revsFiles.resize(MAX_DICT_SIZE);
      revs.setAutoDelete(true);
      revsFiles.setAutoDelete(true);

      cache = new Cache(this);

      // control git version
      QString version;
      if (run("git --version", &version)) {

            version = version.section(' ', -1, -1).section('.', 0, 2);
            if (version < GIT_VERSION) {

                  // simply send information, the 'not compatible version'
                  // policy should be implemented upstream
                  const QString cmd("Current git version is " + version +
                        " but is required " + GIT_VERSION + " or better");
                  const QString errorDesc("Your installed git is too old."
                        "\nPlease upgrade to avoid possible misbehaviours.");
                  MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc);
                  QApplication::postEvent(p, e);
            }
      }
      errorReportingEnabled = false;
      isTextHighlighterFound = run("source-highlight -V", &version);
      if (isTextHighlighterFound)
            dbp("Found %1", version.section('\n', 0, 0));
      errorReportingEnabled = true;
}

void Git::setTextCodec(QTextCodec* tc) {

      QTextCodec::setCodecForCStrings(tc); // works also with tc == 0 (Latin1)
      QString mimeName((tc != NULL) ? tc->mimeName() : "Latin1");

      // workaround Qt issue of mime name diffrent from
      // standard http://www.iana.org/assignments/character-sets
      if (mimeName == "Big5-HKSCS")
            mimeName = "Big5";

      run("git repo-config i18n.commitencoding " + mimeName);
}

QTextCodec* Git::getTextCodec(bool* isGitArchive) {

      *isGitArchive = isGIT;
      if (!isGIT) // can be called also when not in an archive
            return NULL;

      QString runOutput;
      if (!run("git repo-config --get i18n.commitencoding", &runOutput))
            return NULL;

      if (runOutput.isEmpty()) // git docs says default is utf-8
            return QTextCodec::codecForName("utf8");

      return QTextCodec::codecForName(runOutput.stripWhiteSpace());
}

const QString Git::getLocalDate(SCRef gitDate) {

      QDateTime d;
      d.setTime_t(gitDate.toULong());
      return d.toString(Qt::LocalDate);
}

const QString Git::getRevInfo(SCRef sha, FileHistory* fh) {

      const Rev* c = revLookup(sha, fh);
      if (c == NULL)
            return "";

      QString refsInfo;
      if (c->isBranch) {
            if (c->isCurrentBranch)
                  refsInfo = "Head: " + c->branch;
            else
                  refsInfo = "Branch: " + c->branch;
      }
      if (c->isTag)
            refsInfo.append("   Tag: " + c->tag);
      if (c->isRef)
            refsInfo.append("   Ref: " + c->ref);
      if (c->isApplied || c->isUnApplied)
            refsInfo.append("   Patch: " + getPatchName(sha));

      refsInfo = refsInfo.stripWhiteSpace();

      if (!refsInfo.isEmpty()) {
            SCRef msg(getTagMsg(sha));
            if (!msg.isEmpty())
                  refsInfo.append("  [" + msg + "]");
      }
      return refsInfo;
}

const QString Git::getRefSha(SCRef refName) {

      if (tags.contains(refName))
            return tagsSHA[tags.findIndex(refName)];

      if (heads.contains(refName))
            return headsSHA[heads.findIndex(refName)];

      if (refs.contains(refName))
            return refsSHA[refs.findIndex(refName)];

      // if a ref was not found perhaps is an abbreviated form
      QString runOutput;
      if (!run("git rev-parse " + refName, &runOutput))
            return "";

      return runOutput.stripWhiteSpace();
}

bool Git::isPatchName(SCRef patchName) {

      return patchNames.values().contains(patchName);
}

bool Git::isTagName(SCRef tag) { return tags.contains(tag); }

bool Git::isCommittingMerge() { return isMergeHead; }

bool Git::isStGITStack() { return isStGIT; }

bool Git::isUnapplied(SCRef sha) { return unAppliedSHA.contains(sha); }

void Git::addExtraFileInfo(QString* rowName, SCRef sha, SCRef diffToSha, bool allMergeFiles) {

      const RevFile* files;
      if ((files = getFiles(sha, diffToSha, allMergeFiles)) == NULL)
            return;

      int idx;
      if ((idx = findFileIndex(*files, *rowName)) == -1)
            return;

      QString extSt(files->getExtendedStatus(idx));
      if (extSt.isEmpty())
            return;

      *rowName = extSt;
}

void Git::removeExtraFileInfo(QString* rowName) {

      if (rowName->contains(" --> ")) // return destination file name
            *rowName = rowName->section(" --> ", 1, 1).section(" (", 0, 0);
}

void Git::formatPatchFileHeader(QString* rowName, SCRef sha, SCRef diffToSha,
                                bool combined, bool allMergeFiles) {

      // return git patch format: diff --git a/<origFile> b/<destFile>
      QString prefix((combined) ? "diff --combined " : "diff --git a/");

      if (combined) { // TODO rename/copy still not supported in this case
            *rowName = prefix + *rowName;
            return;
      }
      // let's see if it's a rename/copy...
      addExtraFileInfo(rowName, sha, diffToSha, allMergeFiles);

      if (rowName->contains(" --> ")) { // ...it is!

            SCRef destFile(rowName->section(" --> ", 1, 1).section(" (", 0, 0));
            SCRef origFile(rowName->section(" --> ", 0, 0));
            *rowName = prefix + origFile + " b/" + destFile;
      } else
            *rowName = prefix + *rowName + " b/" + *rowName;
}

Annotate* Git::startAnnotate(FileHistory* fh, QObject* guiObj) { // non blocking

      Annotate* ann = new Annotate(this, guiObj);
      ann->start(fh); // non blocking call
      return ann;
}

void Git::cancelAnnotate(Annotate* ann) {

      if (ann)
            if (ann->stop()) // if still running will be deleted by annotateExited()
                  delete ann;
}

void Git::annotateExited(Annotate* ann) {

      if (ann->isCanceled()) {
            ann->deleteLater();
            return; // do not emit anything
      }
      // Annotate object will be deleted by cancelAnnotate()
      const QString msg = QString("Annotated %1 files in %2 ms")
                         .arg(ann->count()).arg(ann->elapsed());

      emit annotateReady(ann, ann->file(), ann->isValid(), msg);
}

const FileAnnotation* Git::lookupAnnotation(Annotate* ann, SCRef fileName, SCRef sha) {

      if (!ann)
            return NULL;

      return ann->lookupAnnotation(sha, fileName);
}

void Git::cancelDataLoading(const FileHistory* fh) {
// normally called when closing file viewer

      emit cancelLoading(fh); // non blocking
}

const Rev* Git::revLookup(SCRef sha, const FileHistory* fh) {

      const RevMap& r = (fh ? fh->revs : revs);
      return r[sha];
}

bool Git::run(SCRef runCmd, QString* runOutput, QObject* receiver, SCRef buf) {

      MyProcess p(par, this, workDir, errorReportingEnabled);
      return p.runSync(runCmd, runOutput, receiver, buf);
}

MyProcess* Git::runAsync(SCRef runCmd, QObject* receiver, SCRef buf) {

      MyProcess* p = new MyProcess(par, this, workDir, errorReportingEnabled);
      if (!p->runAsync(runCmd, receiver, buf)) {
            delete p;
            p = NULL;
      }
      return p; // auto-deleted when done
}

MyProcess* Git::runAsScript(SCRef runCmd, QObject* receiver, SCRef buf) {

      const QString scriptFile(workDir + "/qgit_script.sh");
      if (!writeToFile(scriptFile, runCmd))
            return NULL;

      chmod(scriptFile, 0755);
      MyProcess* p = runAsync(scriptFile, receiver, buf);
      if (p)
            connect(p, SIGNAL(eof()), this, SLOT(on_runAsScript_eof()));
      return p;
}

void Git::on_runAsScript_eof() {

      QDir dir(workDir);
      dir.remove("qgit_script.sh");
}

void Git::cancelProcess(MyProcess* p) {

      if (p)
            p->on_cancel(); // non blocking call
}

int Git::findFileIndex(const RevFile& rf, SCRef name) {

      if (name.isEmpty())
            return -1;

      int idx = name.findRev('/') + 1;
      SCRef dr = name.left(idx);
      SCRef nm = name.mid(idx);

      for (uint i = 0; i < rf.names.count(); ++i) {
            if (fileNamesVec[rf.names[i]] == nm && dirNamesVec[rf.dirs[i]] == dr)
                  return i;
      }
      return -1;
}

const QString Git::getLaneParent(SCRef fromSHA, uint laneNum) {

      const Rev* rs = revLookup(fromSHA);
      if (!rs)
            return "";

      for (int idx = rs->orderIdx - 1; idx >= 0; idx--) {

            const Rev* r = revLookup(revOrder[idx]);
            if (laneNum >= r->lanes.count())
                  return "";

            if (!isFreeLane(r->lanes[laneNum])) {

                  int type = r->lanes[laneNum], parNum = 0;
                  while (!isMerge(type) && type != ACTIVE) {

                        if (isHead(type))
                              parNum++;

                        type = r->lanes[--laneNum];
                  }
                  return r->parent(parNum);
            }
      }
      return "";
}

const QStringList Git::getChilds(SCRef parent) {

      QStringList childs;
      const Rev* r = revLookup(parent);
      if (!r)
            return childs;

      for (uint i = 0; i < r->childs.count(); i++)
            childs.append(revOrder[r->childs[i]]);

      // reorder childs by loading order
      QStringList::iterator itC(childs.begin());
      for ( ; itC != childs.end(); ++itC) {
            const Rev* r = revLookup(*itC);
            (*itC).prepend(QString("%1 ").arg(r->orderIdx, 5));
      }
      childs.sort();
      for (itC = childs.begin(); itC != childs.end(); ++itC)
            (*itC) = (*itC).section(' ', -1, -1);

      return childs;
}

const QString Git::getShortLog(SCRef sha) {

      const Rev* r = revLookup(sha);
      return (r ? r->shortLog() : "");
}

const QStringList Git::getTagNames(bool onlyLoaded) {

      return (onlyLoaded) ? loadedTagNames : tags;
}

const QStringList Git::getBranchNames() {

      return loadedBranchNames;
}

const QString Git::getTagMsg(SCRef sha) {

      Rev* c = const_cast<Rev*>(revLookup(sha));
      if (!c)
            return "";

      if (!c->tagMsg.isEmpty())
            return c->tagMsg;

      QRegExp pgp = QRegExp("-----BEGIN PGP SIGNATURE*END PGP SIGNATURE-----", true, true);

      if (tagsObj.contains(sha)) {

            QString runOutput;
            if (run("git cat-file tag " + tagsObj[sha], &runOutput)) {

                  c->tagMsg = runOutput.section("\n\n", 1);
                  if (!c->tagMsg.isEmpty())
                        c->tagMsg = c->tagMsg.remove(pgp).stripWhiteSpace();
            }
            tagsObj.remove(sha);
      }
      return c->tagMsg;
}

MyProcess* Git::getDiff(SCRef sha, QObject* receiver, SCRef diffToSha, bool combined) {

      QString runCmd;
      if (sha != ZERO_SHA) {
            runCmd = "git diff-tree -r --patch-with-stat ";
            runCmd.append(combined ? "-c " : "-C -m "); // TODO rename for combined
            runCmd.append(diffToSha + " " + sha); // diffToSha could be empty
      } else
            runCmd = "git diff-index -r -m --patch-with-stat HEAD";

      return runAsync(runCmd, receiver);
}

const QString Git::getFileSha(SCRef file, SCRef revSha) {

      const QString sha(revSha == ZERO_SHA ? "HEAD" : revSha);
      QString runCmd("git ls-tree -r " + sha + " " + file), runOutput;
      bool ok = run(runCmd, &runOutput);
      if (!ok || runOutput.isEmpty()) // deleted file case
            return "";

      return runOutput.mid(12, 40);
}

MyProcess* Git::getFile(SCRef file, SCRef revSha, QObject* receiver, QString* result) {

      QString runCmd;
      if (revSha == ZERO_SHA)
            runCmd = "cat " + file;
      else {
            SCRef fileSha(getFileSha(file, revSha));
            if (!fileSha.isEmpty())
                  runCmd = "git cat-file blob " + fileSha;
            else
                  runCmd = "git diff-tree HEAD HEAD"; // fake an empty file reading
      }
      if (receiver == NULL) {
            run(runCmd, result);
            return NULL; // in case of sync call we ignore run() return value
      }
      return runAsync(runCmd, receiver);
}

MyProcess* Git::getHighlightedFile(SCRef file, SCRef sha, QObject* receiver, QString* result){

      if (!isTextHighlighter()) {
            dbs("ASSERT in getHighlightedFile: highlighter not found");
            return NULL;
      }
      QString ext(file.section('.', -1, -1, QString::SectionIncludeLeadingSep));
      QString inputFile(workDir + "/qgit_hlght_input" + ext);
      if (!saveFile(file, sha, inputFile))
            return NULL;

      QString runCmd("source-highlight --failsafe -f html -i " + inputFile);

      if (receiver == NULL) {
            run(runCmd, result);
            on_getHighlightedFile_eof();
            return NULL; // in case of sync call we ignore run() return value
      }
      MyProcess* p = runAsync(runCmd, receiver);
      if (p)
            connect(p, SIGNAL(eof()), this, SLOT(on_getHighlightedFile_eof()));
      return p;
}

void Git::on_getHighlightedFile_eof() {

      QDir dir(workDir);
      QStringList sl(dir.entryList("qgit_hlght_input*"));
      loopList(it, sl)
            dir.remove(*it);
}

bool Git::saveFile(SCRef file, SCRef sha, SCRef path) {

      QString fileData;
      getFile(file, sha, NULL, &fileData); // sync call
      return writeToFile(path, fileData);
}

void Git::getTree(SCRef treeSha, SList names, SList shas,
                  SList types, bool isWorkingDir, SCRef treePath) {

      QStringList newFiles, unfiles, delFiles, dummy;
      if (isWorkingDir) { // retrieve unknown and deleted files under treePath

            getWorkDirFiles(UNKNOWN, unfiles, dummy);
            loopList(it, unfiles) { // don't add unknown files under other directories
                  QFileInfo f(*it);
                  SCRef d(f.dirPath(false));
                  if (d == treePath || (treePath.isEmpty() && d == "."))
                        newFiles.append(f.fileName());
            }
            getWorkDirFiles(DELETED, delFiles, dummy);
      }
      // if needed fake a working directory tree starting from HEAD tree
      const QString tree(treeSha == ZERO_SHA ? "HEAD" : treeSha);
      QString runOutput;
      if (!run("git ls-tree " + tree, &runOutput))
            return;

      const QStringList sl(QStringList::split('\n', runOutput));
      loopList(it, sl) {
            // insert in order any good unknown file to the list,
            // newFiles must be already sorted
            SCRef fn((*it).section('\t', 1, 1));
            while (!newFiles.empty() && newFiles.first() < fn) {
                  names.append(newFiles.first());
                  shas.append("");
                  types.append(NEW);
                  newFiles.pop_front();
            }
            // append any not deleted file
            SCRef fp(treePath.isEmpty() ? fn : treePath + '/' + fn);
            if (delFiles.empty() || (delFiles.findIndex(fp) == -1)) {
                  names.append(fn);
                  shas.append((*it).mid(12, 40));
                  types.append((*it).mid(7, 4));
            }
      }
      while (!newFiles.empty()) { // append any remaining unknown file
            names.append(newFiles.first());
            shas.append("");
            types.append(NEW);
            newFiles.pop_front();
      }
}

void Git::getWorkDirFiles(const QChar& status, SList files, SList dirs) {

      files.clear();
      dirs.clear();
      const RevFile* f = getFiles(ZERO_SHA);
      if (!f)
            return;

      for (uint i = 0; i < f->names.count(); i++) {

            if (f->statusCmp(i, status)) {

                  SCRef fp(filePath(*f, i));
                  files.append(fp);

                  for (int j = 0; j < fp.contains('/'); j++) {

                        SCRef dir(fp.section('/', 0, j));
                        if (dirs.findIndex(dir) == -1)
                              dirs.append(dir);
                  }
            }
      }
}

bool Git::isTreeModified(SCRef sha) {

      const RevFile* f = getFiles(sha);
      if (f) {
            for (uint i = 0; i < f->names.count(); ++i)
                  if (f->statusCmp(i, DELETED) || f->statusCmp(i, NEW))
                        return true;
      }
      return false;
}

bool Git::isParentOf(SCRef par, SCRef child) {

      const Rev* c = revLookup(child);
      if (!c || c->parentsCount() != 1) // we don't handle merges
            return false;

      return (c->parent(0) == par);
}

bool Git::isSameFiles(SCRef tree1Sha, SCRef tree2Sha) {

      // early skip common case of browsing with up and down arrows, i.e.
      // going from parent(child) to child(parent). In this case we can
      // check RevFileMap and skip a costly 'git diff-tree' call.
      if (isParentOf(tree1Sha, tree2Sha))
            return !isTreeModified(tree2Sha);

      if (isParentOf(tree2Sha, tree1Sha))
            return !isTreeModified(tree1Sha);

      const QString runCmd("git diff-tree -r " + tree1Sha + " " + tree2Sha);
      QString runOutput;
      if (!run(runCmd, &runOutput))
            return false;

      bool isChanged = (runOutput.find(" A\t") != -1 || runOutput.find(" D\t") != -1);
      return !isChanged;
}

const QStringList Git::getDescendantBranches(SCRef sha) {

      QStringList tl;
      const Rev* r = revLookup(sha);
      if (!r || (r->descBrnMaster == -1))
            return tl;

      const QValueVector<int>& nr = revLookup(revOrder[r->descBrnMaster])->descBranches;

      for (uint i = 0; i < nr.count(); i++) {

            SCRef sha = revOrder[nr[i]];
            int idx = headsSHA.findIndex(sha);
            if (idx != -1) {
                  SCRef branchName = heads[idx];
                  const QString t = branchName + " (" + sha + ")";
                  tl.append(t);
            }
      }
      return tl;
}

const QStringList Git::getNearTags(bool goDown, SCRef sha) {

      QStringList tl;
      const Rev* r = revLookup(sha);
      if (!r)
            return tl;

      int nearRefsMaster = (goDown ? r->descRefsMaster : r->ancRefsMaster);
      if (nearRefsMaster == -1)
            return tl;

      const QValueVector<int>& nr = goDown ? revLookup(revOrder[nearRefsMaster])->descRefs :
                                             revLookup(revOrder[nearRefsMaster])->ancRefs;

      for (uint i = 0; i < nr.count(); i++) {

            SCRef sha = revOrder[nr[i]];
            int idx = tagsSHA.findIndex(sha);
            if (idx != -1)
                  tl.append(tags[idx] + " (" + sha + ")");
      }
      return tl;
}

const QString Git::getDefCommitMsg() {

      SCRef sha(appliedSHA.empty() ? ZERO_SHA : appliedSHA.last());
      const Rev* c = revLookup(sha);
      if (c == NULL) {
            dbp("ASSERT: getDefCommitMsg sha <%1> not found", sha);
            return "";
      }
      if (sha == ZERO_SHA)
            return c->longLog();

      return c->shortLog() + '\n' + c->longLog().stripWhiteSpace();
}

const QString Git::colorMatch(SCRef txt, QRegExp& regExp) {

      QString text(txt);
      if (regExp.isEmpty())
            return text;

      // we use $_1 and $_2 instead of '<' and '>' to avoid later substitutions
      SCRef startCol(QString::fromLatin1("$_1b$_2$_1font color=\"red\"$_2"));
      SCRef endCol(QString::fromLatin1("$_1/font$_2$_1/b$_2"));
      int pos = 0;
      while ((pos = text.find(regExp, pos)) != -1) {

            SCRef match(regExp.cap(0));
            const QString coloredText(startCol + match + endCol);
            text.replace(pos, match.length(), coloredText);
            pos += coloredText.length();
      }
      return text;
}

const QString Git::getDesc(SCRef sha, QRegExp& shortLogRE, QRegExp& longLogRE) {

      if (sha.isEmpty())
            return "";

      const Rev* c = revLookup(sha);
      if (c == NULL) {
            dbp("ASSERT in Git::getDesc: sha <%1> not found", sha);
            return "";
      }

      // set temporary Latin-1 to avoid using QString::fromLatin1() everywhere
      QTextCodec* tc = QTextCodec::codecForCStrings();
      QTextCodec::setCodecForCStrings(0);

      QString text;
      if (c->isDiffCache)
            text = c->longLog();
      else {
            text = QString("Author: " + c->author() + "\nDate:   ");
            text.append(getLocalDate(c->authorDate()));
            if (!c->isUnApplied) {
                  text.append("\nParent: ").append(c->parents().join("\nParent: "));

                  QStringList sl = getChilds(sha);
                  if (!sl.isEmpty())
                        text.append("\nChild: ").append(sl.join("\nChild: "));

                  sl = getDescendantBranches(sha);
                  if (!sl.empty())
                        text.append("\nBranch: ").append(sl.join("\nBranch: "));

                  sl = getNearTags(!optGoDown, sha);
                  if (!sl.isEmpty())
                        text.append("\nFollows: ").append(sl.join(", "));

                  sl = getNearTags(optGoDown, sha);
                  if (!sl.isEmpty())
                        text.append("\nPrecedes: ").append(sl.join(", "));
            }
            text.append("\n\n    " + colorMatch(c->shortLog(), shortLogRE) +
                        '\n' + colorMatch(c->longLog(), longLogRE));
      }
      text = QStyleSheet::convertFromPlainText(text); // this puppy needs Latin-1

      // hightlight SHA's
      int pos = 0;
      QRegExp reSHA("[0-9a-f]{40}", false);
      reSHA.setMinimal(true);
      while ((pos = text.find(reSHA, pos)) != -1) {

            SCRef sha(reSHA.cap(0));
            const Rev* r = revLookup(sha);
            QString slog(r ? r->shortLog() : sha);
            if (slog.isEmpty()) // very rare but possible
                  slog = sha;
            if (slog.length() > 60)
                  slog = slog.left(57).stripWhiteSpace().append("...");

            slog = QStyleSheet::escape(slog);
            const QString link("<a href=\"" + sha + "\">" + slog + "</a>");
            text.replace(pos, sha.length(), link);
            pos += link.length();
      }
      text.replace("$_1", "<"); // see colorMatch()
      text.replace("$_2", ">");

      QTextCodec::setCodecForCStrings(tc); // restore codec
      return text;
}

const RevFile* Git::getAllMergeFiles(const Rev* r) {

      SCRef mySha(ALL_MERGE_FILES + r->sha());
      RevFile* rf = revsFiles[mySha];
      if (rf == NULL) {
            QString runCmd("git diff-tree -r -m -C " + r->sha()), runOutput;
            if (!run(runCmd, &runOutput))
                  return NULL;

            revsFiles.insert(mySha, new RevFile());
            rf = revsFiles[mySha];
            parseDiffFormat(*rf, runOutput);
      }
      return rf;
}

const RevFile* Git::getFiles(SCRef sha, SCRef diffToSha, bool allFiles, SCRef path) {

      const Rev* r = revLookup(sha);
      if (r == NULL)
            return NULL;

      if (r->parentsCount() == 0) // skip initial rev
            return NULL;

      if (r->parentsCount() > 1 && diffToSha.isEmpty() && allFiles)
            return getAllMergeFiles(r);

      if (!diffToSha.isEmpty() && (sha != ZERO_SHA)) {

            QString runCmd("git diff-tree -r -m -C ");
            runCmd.append(diffToSha + " " + sha);
            if (!path.isEmpty())
                  runCmd.append(" " + path);

            QString runOutput;
            if (!run(runCmd, &runOutput))
                  return NULL;

            // we insert a dummy revision file object. It will be
            // overwritten at each request but we don't care.
            revsFiles.insert(CUSTOM_SHA, new RevFile());
            RevFile* rf = revsFiles[CUSTOM_SHA];
            parseDiffFormat(*rf, runOutput);
            return rf;
      }
      RevFile* rf = revsFiles[sha]; // ZERO_SHA search arrives here
      if (rf == NULL) {

            if (sha == ZERO_SHA) {
                  dbs("ASSERT in Git::getFiles, ZERO_SHA not found");
                  return NULL;
            }
            QString runCmd("git diff-tree -r -c -C " + sha), runOutput;
            if (!run(runCmd, &runOutput))
                  return false;

            if (!revsFiles.find(sha)) { // has been created in the mean time?
                  revsFiles.insert(sha, new RevFile());
                  rf = revsFiles[sha];
                  parseDiffFormat(*rf, runOutput);
                  cacheNeedsUpdate = true;
            } else
                  return revsFiles[sha];
      }
      return rf;
}

void Git::startFileHistory(FileHistory* fh) {

      startRevList(fh->fileName, fh);
}

void Git::getFileFilter(SCRef path, QMap<QString, bool>& shaMap) {

      shaMap.clear();
      QRegExp rx(path, false, true); // not case sensitive and with wildcard
      loop(StrVect, itr, revOrder) {
            RevFile* rf = revsFiles[*itr];
            if (rf == NULL)
                  continue;

            // case insensitive, wildcard search
            for (uint i = 0; i < rf->names.count(); ++i)
                  if (rx.search(filePath(*rf, i)) != -1) {
                        shaMap.insert(*itr, true);
                        break;
                  }
      }
}

bool Git::getPatchFilter(SCRef exp, bool isRegExp, QMap<QString, bool>& shaMap) {

      shaMap.clear();
      QString buf;
      loop(StrVect, it, revOrder)
            if (*it != ZERO_SHA)
                  buf.append(*it).append('\n');

      if (buf.isEmpty())
            return true;

      QString runCmd("git diff-tree -r -s --stdin "), runOutput;
      if (isRegExp)
            runCmd.append("--pickaxe-regex ");

      runCmd.append(QUOTE_CHAR + "-S" + exp + QUOTE_CHAR);
      if (!run(runCmd, &runOutput, NULL, buf))
            return false;

      QStringList sl(QStringList::split('\n', runOutput));
      loopList(it2, sl)
            shaMap.insert(*it2, true);

      return true;
}

bool Git::resetCommits(int parentDepth) {

      QString runCmd("git reset --soft HEAD~");
      runCmd.append(QString::number(parentDepth));
      return run(runCmd);
}

bool Git::applyPatchFile(SCRef patchPath, bool commit, bool fold, bool sign) {

      const QString quotedPath(QUOTE_CHAR + patchPath + QUOTE_CHAR);

      if (commit && isStGIT) {
            if (fold)
                  return run("stg fold " + quotedPath);

            return run("stg import --mail " + quotedPath);
      }
      QString runCmd("git am -k -u --3way ");

      if (testFlag(SIGN_PATCH_F) && sign)
            runCmd.append("--signoff ");

      return run(runCmd + quotedPath);
}

bool Git::formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir) {

      bool remote = !remoteDir.isEmpty();
      QSettings settings;
      const QString FPArgs = settings.readEntry(APP_KEY + FPATCH_ARGS_KEY, "");

      QString runCmd("git format-patch");
      if (testFlag(NUMBERS_F) && !remote)
            runCmd.append(" -n");

      if (remote)
            runCmd.append(" --keep-subject");

      runCmd.append(" -o " + dirPath);
      if (!FPArgs.isEmpty())
            runCmd.append(" " + FPArgs);

      runCmd.append(" --start-number=");

      const QString tmp(workDir);
      if (remote)
            workDir = remoteDir; // run() uses workDir value

      int n = 1;
      bool ret = false;
      loopList(it, shaList) { // shaList is ordered by newest to oldest
            const QString cmd(runCmd + QString::number(n) + " " +
                              *it + QString::fromLatin1("^..") + *it);
            n++;
            ret = run(cmd);
            if (!ret)
                  break;
      }
      workDir = tmp;
      return ret;
}

bool Git::updateIndex(SCList selFiles) {

      if (selFiles.empty())
            return true;

      QString runCmd("git update-index --add --remove --replace -- ");
      runCmd.append(selFiles.join(" "));
      return run(runCmd);
}

bool Git::commitFiles(SCList selFiles, SCRef msg) {
/*
      Part of this code is taken from Fredrik Kuivinen "Gct"
      tool. I have just translated from Python to C++
*/
      const QString msgFile(gitDir + "/qgit_cmt_msg");
      if (!writeToFile(msgFile, msg)) // early skip
            return false;

      // add user selectable commit options
      QSettings settings;
      const QString CMArgs = settings.readEntry(APP_KEY + CMT_ARGS_KEY, "");

      QString cmtOptions;
      if (!CMArgs.isEmpty())
            cmtOptions.append(" " + CMArgs);

      if (testFlag(SIGN_CMT_F))
            cmtOptions.append(" -s");

      if (testFlag(VERIFY_CMT_F))
            cmtOptions.append(" -v");

      // extract not selected files already updated
      // in index, save them to restore at the end
      QStringList notSelInIndexFiles(getOtherFiles(selFiles, optOnlyInIndex));

      // extract selected NOT to be deleted files to
      // later feed git commit. Files to be deleted
      // should avoid going through 'git commit'
      QStringList selNotDelFiles;
      const RevFile* files = getFiles(ZERO_SHA); // files != NULL
      loopList(it, selFiles) {
            int idx = findFileIndex(*files, *it);
            if (!files->statusCmp(idx, DELETED))
                  selNotDelFiles.append(*it);
      }
      // test if we need a git read-tree to temporary
      // remove not selected files from index
      if (!notSelInIndexFiles.empty())
            if (!run("git read-tree --reset HEAD"))
                  return false;

      // before to commit we have to update index with all
      // the selected files because git commit doesn't
      // use --add flag
      updateIndex(selFiles);

      // now we can commit, 'git commit' will update index
      // with selected (not to be deleted) files for us
      QString runCmd("git commit -i" + cmtOptions + " -F " + msgFile);
      runCmd.append(" " + selNotDelFiles.join(" "));
      if (!run(runCmd))
            return false;

      // finally restore not selected files in index
      if (!notSelInIndexFiles.empty())
            if (!updateIndex(notSelInIndexFiles))
                  return false;

      QDir dir(workDir);
      dir.remove(msgFile);
      return true;
}

bool Git::mkPatchFromIndex(SCRef msg, SCRef patchFile) {

      QString runOutput;
      if (!run("git diff-index --cached -p HEAD", &runOutput))
            return false;

      const QString patch("Subject: " + msg + "\n---\n" + runOutput);
      return writeToFile(patchFile, patch);
}

const QStringList Git::getOtherFiles(SCList selFiles, bool onlyInIndex) {

      const RevFile* files = getFiles(ZERO_SHA); // files != NULL
      QStringList notSelFiles;
      for (uint i = 0; i < files->names.count(); ++i) {
            SCRef fp = filePath(*files, i);
            if (selFiles.find(fp) == selFiles.constEnd()) { // not selected...
                  if (!onlyInIndex)
                        notSelFiles.append(fp);
                  else if (files->isInIndex(i))
                        notSelFiles.append(fp);
            }
      }
      return notSelFiles;
}

void Git::removeFiles(SCList selFiles, SCRef workDir, SCRef ext) {

      QDir d(workDir);
      loopList(it, selFiles)
            d.rename(*it, *it + ext);
}

void Git::restoreFiles(SCList selFiles, SCRef workDir, SCRef ext) {

      QDir d(workDir);
      loopList(it, selFiles)
            d.rename(*it + ext, *it); // overwrites any existent file
}

void Git::removeDeleted(SCList selFiles) {

      QDir dir(workDir);
      const RevFile* files = getFiles(ZERO_SHA); // files != NULL
      loopList(it, selFiles) {
            int idx = findFileIndex(*files, *it);
            if (files->statusCmp(idx, DELETED))
                  dir.remove(*it);
      }
}

bool Git::stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold) {

      // here the deal is to create a patch with the diffs between the
      // updated index and HEAD, then resetting the index and working
      // dir to HEAD so to have a clean tree, then import/fold the patch
      bool retval = true;
      const QString patchFile(gitDir + "/qgit_tmp_patch");
      const QString extNS(".qgit_removed_not_selected");
      const QString extS(".qgit_removed_selected");

      // we have selected modified files in selFiles, we still need
      // to know the not selected but modified files and, among
      // theese the cached ones to proper restore state at the end.
      QStringList notSelFiles = getOtherFiles(selFiles, !optOnlyInIndex);
      QStringList notSelInIndexFiles = getOtherFiles(selFiles, optOnlyInIndex);

      // update index with selected files
      if (!run("git read-tree --reset HEAD"))
            goto error;
      if (!updateIndex(selFiles))
            goto error;

      // create a patch with diffs between index and HEAD
      if (!mkPatchFromIndex(msg, patchFile))
            goto error;

      // temporary remove files according to their type
      removeFiles(selFiles, workDir, extS); // to use in case of rollback
      removeFiles(notSelFiles, workDir, extNS); // to restore at the end

      // checkout index to have a clean tree
      if (!run("git read-tree --reset HEAD"))
            goto error;
      if (!run("git checkout-index -q -f -u -a"))
            goto rollback;

      // finally import/fold the patch
      if (fold) {
            // update patch message before to fold so to use refresh only as a rename tool
            if (!msg.isEmpty()) {
                  if (!run("stg refresh --message \"" + msg.stripWhiteSpace() + "\""))
                        goto rollback;
            }
            if (!run("stg fold " + patchFile))
                  goto rollback;
            if (!run("stg refresh")) // refresh needed after fold
                  goto rollback;
      } else {
             if (!run("stg import --mail --name " + patchName + " " + patchFile))
                  goto rollback;
      }
      goto exit;

rollback:
      restoreFiles(selFiles, workDir, extS);
      removeDeleted(selFiles); // remove files to be deleted from working tree

error:
      retval = false;

exit:
      // it is safe to call restore() also if back-up files don't
      // exsist, so we can 'goto exit' from anywhere.
      restoreFiles(notSelFiles, workDir, extNS);
      updateIndex(notSelInIndexFiles);
      QDir dir(workDir);
      dir.remove(patchFile);
      loopList(it, selFiles)
            dir.remove(*it + extS); // remove temporary backup rollback files
      return retval;
}

bool Git::makeTag(SCRef sha, SCRef tagName, SCRef msg) {

      if (msg.isEmpty())
            return run("git tag " + tagName + " " + sha);

      return run("git tag -m \"" + msg + "\" " + tagName + " " + sha);
}

bool Git::deleteTag(SCRef sha) {

      const Rev* r = revLookup(sha);
      if (!r || !r->isTag)
            return false;

      // r->tag could contain more then one tag, separated by '\n'
      return run("git tag -d " + r->tag.section('\n', 0, 0));
}

bool Git::stgPush(SCRef sha) {

      const QString quotedPath(QUOTE_CHAR + patchNames[sha] + QUOTE_CHAR);
      bool ret = run("stg push " + quotedPath);
      if (!ret)
            run("stg push --undo");

      return ret;
}

bool Git::stgPop(SCRef sha) {

      if (sha == appliedSHA.last()) // top of the stack
            return run("stg pop");

      if (sha == appliedSHA.first())
            return run("stg pop --all");

      const QString quotedPath(QUOTE_CHAR + patchNames[sha] + QUOTE_CHAR);
      if (!run("stg pop --to=" + quotedPath))
            return false;

      return run("stg pop");
}

bool Git::writeToFile(SCRef fileName, SCRef data) {

      QFile file(QFile::encodeName(fileName));
      if (!file.open(IO_WriteOnly)) {
            dbp("ERROR: unable to write file %1", fileName);
            return false;
      }
      QTextStream stream(&file);
      stream << data;
      file.close();
      return true;
}

bool Git::readFromFile(SCRef fileName, QString& data) {

      data = "";
      QFile file(QFile::encodeName(fileName));
      if (!file.open(IO_ReadOnly)){
            dbp("ERROR: unable to read file %1", fileName);
            return false;
      }
      QTextStream stream(&file);
      data = stream.read();
      file.close();
      return true;
}

Generated by  Doxygen 1.6.0   Back to index