00001 
00002 
00003 
00004 
00005 
00006 
00007 
00008 
00009 #include <stdlib.h>    
00010 #include <qapplication.h>
00011 #include <qdatetime.h>
00012 #include <qtextcodec.h>
00013 #include <qregexp.h>
00014 #include <qsettings.h>
00015 #include <qfile.h>
00016 #include <qdir.h>
00017 #include <qeventloop.h>
00018 #include <qprocess.h>
00019 #include <qstylesheet.h>
00020 #include <qobjectlist.h>
00021 #include "lanes.h"
00022 #include "myprocess.h"
00023 #include "annotate.h"
00024 #include "cache.h"
00025 #include "git.h"
00026 
00027 using namespace QGit;
00028 
00029 FileHistory::FileHistory() {
00030 
00031         lns = new Lanes();
00032         revs.setAutoDelete(true);
00033         clear("");
00034         revs.resize(QGit::MAX_DICT_SIZE);
00035         rowData.setAutoDelete(true);
00036 }
00037 
00038 FileHistory::~FileHistory() {
00039 
00040         delete lns;
00041 }
00042 
00043 void FileHistory::clear(SCRef name) {
00044 
00045         revs.clear();
00046         revOrder.clear();
00047         firstFreeLane = 0;
00048         lns->clear();
00049         fileName = name;
00050         rowData.clear();
00051 }
00052 
00053 Git::Git(QWidget* p) : QObject(p) {
00054 
00055         EM_INIT(exGitStopped, "Stopping connection with git");
00056 
00057         fileCacheAccessed = cacheNeedsUpdate = isMergeHead = false;
00058         isStGIT = isGIT = loadingUnAppliedPatches = isTextHighlighterFound = false;
00059         errorReportingEnabled = true; 
00060         curDomain = NULL;
00061 
00062         revsFiles.resize(MAX_DICT_SIZE);
00063         revsFiles.setAutoDelete(true);
00064 }
00065 
00066 void Git::checkEnvironment() {
00067 
00068         QString version;
00069         if (run("git --version", &version)) {
00070 
00071                 version = version.section(' ', -1, -1).section('.', 0, 2);
00072                 if (version < GIT_VERSION) {
00073 
00074                         
00075                         
00076                         const QString cmd("Current git version is " + version +
00077                               " but is required " + GIT_VERSION + " or better");
00078 
00079                         const QString errorDesc("Your installed git is too old."
00080                               "\nPlease upgrade to avoid possible misbehaviours.");
00081 
00082                         MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc);
00083                         QApplication::postEvent(parent(), e);
00084                 }
00085         }
00086         errorReportingEnabled = false;
00087         isTextHighlighterFound = run("source-highlight -V", &version);
00088         errorReportingEnabled = true;
00089         if (isTextHighlighterFound)
00090                 dbp("Found %1", version.section('\n', 0, 0));
00091 }
00092 
00093 void Git::userInfo(SList info) {
00094 
00095 
00096 
00097 
00098 
00099 
00100 
00101 
00102         QString user(getenv("GIT_AUTHOR_NAME"));
00103         QString email(getenv("GIT_AUTHOR_EMAIL"));
00104 
00105         info.clear();
00106         info << "Environment" << user << email;
00107 
00108         errorReportingEnabled = false; 
00109 
00110         run("git repo-config user.name", &user);
00111         run("git repo-config user.email", &email);
00112         info << "Local config" << user << email;
00113 
00114         run("git repo-config --global user.name", &user);
00115         run("git repo-config --global user.email", &email);
00116         info << "Global config" << user << email;
00117 
00118         errorReportingEnabled = true;
00119 }
00120 
00121 bool Git::isBinaryFile(SCRef file) {
00122 
00123         static const char* binaryFileExtensions[] = {"bmp", "gif", "jpeg", "jpg",
00124                            "png", "svg", "tiff", "pcx", "xcf", "xpm",
00125                            "bz", "bz2", "rar", "tar", "z", "gz", "tgz", "zip", 0};
00126 
00127         const QString ext(file.section('.', -1).lower());
00128         int i = 0;
00129         while (binaryFileExtensions[i] != 0)
00130                 if (ext == binaryFileExtensions[i++])
00131                         return true;
00132         return false;
00133 }
00134 
00135 void Git::setThrowOnStop(bool b) {
00136 
00137         if (b)
00138                 EM_REGISTER(exGitStopped);
00139         else
00140                 EM_REMOVE(exGitStopped);
00141 }
00142 
00143 bool Git::isThrowOnStopRaised(int excpId, SCRef curContext) {
00144 
00145         return EM_MATCH(excpId, exGitStopped, curContext);
00146 }
00147 
00148 bool Git::allProcessDeleted() {
00149 
00150         QObjectList* l = this->queryList("QProcess");
00151         bool allProcessAreDeleted = (l->count() == 0);
00152         delete l;
00153         return allProcessAreDeleted;
00154 }
00155 
00156 void Git::setTextCodec(QTextCodec* tc) {
00157 
00158         QTextCodec::setCodecForCStrings(tc); 
00159         QString mimeName(tc ? tc->mimeName() : "Latin1");
00160 
00161         
00162         
00163         if (mimeName == "Big5-HKSCS")
00164                 mimeName = "Big5";
00165 
00166         run("git repo-config i18n.commitencoding " + mimeName);
00167 }
00168 
00169 QTextCodec* Git::getTextCodec(bool* isGitArchive) {
00170 
00171         *isGitArchive = isGIT;
00172         if (!isGIT) 
00173                 return NULL;
00174 
00175         QString runOutput;
00176         if (!run("git repo-config --get i18n.commitencoding", &runOutput))
00177                 return NULL;
00178 
00179         if (runOutput.isEmpty()) 
00180                 return QTextCodec::codecForName("utf8");
00181 
00182         return QTextCodec::codecForName(runOutput.stripWhiteSpace());
00183 }
00184 
00185 const QString Git::quote(SCRef nm) {
00186 
00187         return (QUOTE_CHAR + nm + QUOTE_CHAR);
00188 }
00189 
00190 const QString Git::quote(SCList sl) {
00191 
00192         QString q(sl.join(QUOTE_CHAR + ' ' + QUOTE_CHAR));
00193         q.prepend(QUOTE_CHAR).append(QUOTE_CHAR);
00194         return q;
00195 }
00196 
00197 const QString Git::getLocalDate(SCRef gitDate) {
00198 
00199         QDateTime d;
00200         d.setTime_t(gitDate.toULong());
00201         return d.toString(Qt::LocalDate);
00202 }
00203 
00204 uint Git::checkRef(SCRef sha, uint mask) const {
00205 
00206         QMap<QString, Reference>::const_iterator it(refsShaMap.find(sha));
00207         return (it != refsShaMap.constEnd() ? (*it).type & mask : 0);
00208 }
00209 
00210 const QStringList Git::getRefName(SCRef sha, RefType type, QString* curBranch) const {
00211 
00212         if (!checkRef(sha, type))
00213                 return QStringList();
00214 
00215         const Reference& rf = refsShaMap[sha];
00216 
00217         if (curBranch)
00218                 *curBranch = rf.currentBranch;
00219 
00220         if (type == TAG)
00221                 return rf.tags;
00222 
00223         else if (type == BRANCH)
00224                 return rf.branches;
00225 
00226         else if (type == RMT_BRANCH)
00227                 return rf.remoteBranches;
00228 
00229         else if (type == REF)
00230                 return rf.refs;
00231 
00232         else if (type == APPLIED || type == UN_APPLIED)
00233                 return QStringList(rf.stgitPatch);
00234 
00235         return QStringList();
00236 }
00237 
00238 const QStringList Git::getAllRefSha(uint mask) {
00239 
00240         QStringList shas;
00241         FOREACH (RefMap, it, refsShaMap)
00242                 if ((*it).type & mask)
00243                         shas.append(it.key());
00244         return shas;
00245 }
00246 
00247 const QString Git::getRefSha(SCRef refName, RefType type, bool askGit) {
00248 
00249         bool any = (type == ANY_REF);
00250 
00251         FOREACH (RefMap, it, refsShaMap) {
00252 
00253                 const Reference& rf = *it;
00254 
00255                 if ((any || type == TAG) && rf.tags.contains(refName))
00256                         return it.key();
00257 
00258                 else if ((any || type == BRANCH) && rf.branches.contains(refName))
00259                         return it.key();
00260 
00261                 else if ((any || type == RMT_BRANCH) && rf.remoteBranches.contains(refName))
00262                         return it.key();
00263 
00264                 else if ((any || type == REF) && rf.refs.contains(refName))
00265                         return it.key();
00266 
00267                 else if ((any || type == APPLIED || type == UN_APPLIED) && rf.stgitPatch == refName)
00268                         return it.key();
00269         }
00270         if (!askGit)
00271                 return "";
00272 
00273         
00274         QString runOutput;
00275         errorReportingEnabled = false;
00276         bool ok = run("git rev-parse " + refName, &runOutput);
00277         errorReportingEnabled = true;
00278         return (ok ? runOutput.stripWhiteSpace() : "");
00279 }
00280 
00281 void Git::appendNamesWithId(QStringList& names, SCRef sha, SCList data, bool onlyLoaded) {
00282 
00283         const Rev* r = revLookup(sha);
00284         if (onlyLoaded && !r)
00285                 return;
00286 
00287         if (onlyLoaded) { 
00288                 SCRef cap = QString("%1 ").arg(r->orderIdx, 6);
00289                 FOREACH_SL (it, data)
00290                         names.append(cap + *it);
00291         } else
00292                 names += data;
00293 }
00294 
00295 const QStringList Git::getAllRefNames(uint mask, bool onlyLoaded) {
00296 
00297 
00298         QStringList names;
00299         FOREACH (RefMap, it, refsShaMap) {
00300 
00301                 if (mask & TAG)
00302                         appendNamesWithId(names, it.key(), (*it).tags, onlyLoaded);
00303 
00304                 if (mask & BRANCH)
00305                         appendNamesWithId(names, it.key(), (*it).branches, onlyLoaded);
00306 
00307                 if (mask & RMT_BRANCH)
00308                         appendNamesWithId(names, it.key(), (*it).remoteBranches, onlyLoaded);
00309 
00310                 if (mask & REF)
00311                         appendNamesWithId(names, it.key(), (*it).refs, onlyLoaded);
00312 
00313                 if ((mask & (APPLIED | UN_APPLIED)) && !onlyLoaded)
00314                         names.append((*it).stgitPatch); 
00315         }
00316         if (onlyLoaded) {
00317                 names.sort();
00318                 QStringList::iterator itN(names.begin());
00319                 for ( ; itN != names.end(); ++itN) 
00320                         (*itN) = (*itN).section(' ', -1, -1);
00321         }
00322         return names;
00323 }
00324 
00325 const QString Git::getRevInfo(SCRef sha) {
00326 
00327         uint type = checkRef(sha);
00328         if (type == 0)
00329                 return "";
00330 
00331         QString refsInfo;
00332         if (type & BRANCH) {
00333                 const QString cap(type & CUR_BRANCH ? "Head: " : "Branch: ");
00334                 refsInfo =  cap + getRefName(sha, BRANCH).join(" ");
00335         }
00336         if (type & RMT_BRANCH)
00337                 refsInfo.append("   Remote branch: " + getRefName(sha, RMT_BRANCH).join(" "));
00338 
00339         if (type & TAG)
00340                 refsInfo.append("   Tag: " + getRefName(sha, TAG).join(" "));
00341 
00342         if (type & REF)
00343                 refsInfo.append("   Ref: " + getRefName(sha, REF).join(" "));
00344 
00345         if (type & APPLIED)
00346                 refsInfo.append("   Patch: " + getRefName(sha, APPLIED).join(" "));
00347 
00348         if (type & UN_APPLIED)
00349                 refsInfo.append("   Patch: " + getRefName(sha, UN_APPLIED).join(" "));
00350 
00351         if (type & TAG) {
00352                 SCRef msg(getTagMsg(sha));
00353                 if (!msg.isEmpty())
00354                         refsInfo.append("  [" + msg + "]");
00355         }
00356         return refsInfo.stripWhiteSpace();
00357 }
00358 
00359 const QString Git::getTagMsg(SCRef sha) {
00360 
00361         if (!checkRef(sha, TAG)) {
00362                 dbs("ASSERT in Git::getTagMsg, tag not found");
00363                 return "";
00364         }
00365         Reference& rf = refsShaMap[sha];
00366 
00367         if (!rf.tagMsg.isEmpty())
00368                 return rf.tagMsg;
00369 
00370         QRegExp pgp("-----BEGIN PGP SIGNATURE*END PGP SIGNATURE-----", true, true);
00371 
00372         if (!rf.tagObj.isEmpty()) {
00373                 QString ro;
00374                 if (run("git cat-file tag " + rf.tagObj, &ro))
00375                         rf.tagMsg = ro.section("\n\n", 1).remove(pgp).stripWhiteSpace();
00376         }
00377         return rf.tagMsg;
00378 }
00379 
00380 bool Git::isPatchName(SCRef nm) {
00381 
00382         if (!getRefSha(nm, UN_APPLIED, false).isEmpty())
00383                 return true;
00384 
00385         return !getRefSha(nm, APPLIED, false).isEmpty();
00386 }
00387 
00388 void Git::addExtraFileInfo(QString* rowName, SCRef sha, SCRef diffToSha, bool allMergeFiles) {
00389 
00390         const RevFile* files = getFiles(sha, diffToSha, allMergeFiles);
00391         if (!files)
00392                 return;
00393 
00394         int idx = findFileIndex(*files, *rowName);
00395         if (idx == -1)
00396                 return;
00397 
00398         QString extSt(files->extendedStatus(idx));
00399         if (extSt.isEmpty())
00400                 return;
00401 
00402         *rowName = extSt;
00403 }
00404 
00405 void Git::removeExtraFileInfo(QString* rowName) {
00406 
00407         if (rowName->contains(" --> ")) 
00408                 *rowName = rowName->section(" --> ", 1, 1).section(" (", 0, 0);
00409 }
00410 
00411 void Git::formatPatchFileHeader(QString* rowName, SCRef sha, SCRef diffToSha,
00412                                 bool combined, bool allMergeFiles) {
00413         if (combined) {
00414                 rowName->prepend("diff --combined ");
00415                 return; 
00416         }
00417         
00418         addExtraFileInfo(rowName, sha, diffToSha, allMergeFiles);
00419 
00420         if (rowName->contains(" --> ")) { 
00421 
00422                 SCRef destFile(rowName->section(" --> ", 1, 1).section(" (", 0, 0));
00423                 SCRef origFile(rowName->section(" --> ", 0, 0));
00424                 *rowName = "diff --git a/" + origFile + " b/" + destFile;
00425         } else
00426                 *rowName = "diff --git a/" + *rowName + " b/" + *rowName;
00427 }
00428 
00429 Annotate* Git::startAnnotate(FileHistory* fh, QObject* guiObj) { 
00430 
00431         Annotate* ann = new Annotate(this, guiObj);
00432         ann->start(fh); 
00433         return ann; 
00434 }
00435 
00436 void Git::cancelAnnotate(Annotate* ann) {
00437 
00438         if (ann)
00439                 ann->deleteWhenDone();
00440 }
00441 
00442 void Git::annotateExited(Annotate* ann) {
00443 
00444         SCRef msg = QString("Annotated %1 files in %2 ms")
00445                     .arg(ann->count()).arg(ann->elapsed());
00446 
00447         emit annotateReady(ann, ann->file(), ann->isValid(), msg);
00448 }
00449 
00450 const FileAnnotation* Git::lookupAnnotation(Annotate* ann, SCRef fileName, SCRef sha) {
00451 
00452         return (ann ? ann->lookupAnnotation(sha, fileName) : NULL);
00453 }
00454 
00455 void Git::cancelDataLoading(const FileHistory* fh) {
00456 
00457 
00458         emit cancelLoading(fh); 
00459 }
00460 
00461 const Rev* Git::revLookup(SCRef sha, const FileHistory* fh) const {
00462 
00463         const RevMap& r = (fh ? fh->revs : revData.revs);
00464         return r[sha];
00465 }
00466 
00467 bool Git::run(SCRef runCmd, QString* runOutput, QObject* receiver, SCRef buf, QStringList* env) {
00468 
00469         if (!runOutput)
00470                 return run(NULL, runCmd, receiver, buf, env);
00471 
00472         *runOutput = "";
00473         QByteArray ba;
00474         bool ret = run(&ba, runCmd, receiver, buf, env);
00475         if (!ba.isEmpty())
00476                 runOutput->append(ba);
00477 
00478         return ret;
00479 }
00480 
00481 bool Git::run(QByteArray* runOutput, SCRef runCmd, QObject* receiver, SCRef buf, QStringList* env) {
00482 
00483         MyProcess p(parent(), this, workDir, errorReportingEnabled);
00484         return p.runSync(runCmd, runOutput, receiver, buf, env);
00485 }
00486 
00487 MyProcess* Git::runAsync(SCRef runCmd, QObject* receiver, SCRef buf, QStringList* env) {
00488 
00489         MyProcess* p = new MyProcess(parent(), this, workDir, errorReportingEnabled);
00490         if (!p->runAsync(runCmd, receiver, buf, env)) {
00491                 delete p;
00492                 p = NULL;
00493         }
00494         return p; 
00495 }
00496 
00497 MyProcess* Git::runAsScript(SCRef runCmd, QObject* receiver, SCRef buf, QStringList* env) {
00498 
00499         const QString scriptFile(workDir + "/qgit_script.sh");
00500         if (!writeToFile(scriptFile, runCmd, true))
00501                 return NULL;
00502 
00503         MyProcess* p = runAsync(scriptFile, receiver, buf, env);
00504         if (p)
00505                 connect(p, SIGNAL(eof()), this, SLOT(on_runAsScript_eof()));
00506         return p;
00507 }
00508 
00509 void Git::on_runAsScript_eof() {
00510 
00511         QDir dir(workDir);
00512         dir.remove("qgit_script.sh");
00513 }
00514 
00515 void Git::cancelProcess(MyProcess* p) {
00516 
00517         if (p)
00518                 p->on_cancel(); 
00519 }
00520 
00521 int Git::findFileIndex(const RevFile& rf, SCRef name) {
00522 
00523         if (name.isEmpty())
00524                 return -1;
00525 
00526         int idx = name.findRev('/') + 1;
00527         SCRef dr = name.left(idx);
00528         SCRef nm = name.mid(idx);
00529 
00530         for (uint i = 0, cnt = rf.count(); i < cnt; ++i) {
00531                 if (fileNamesVec[rf.names[i]] == nm && dirNamesVec[rf.dirs[i]] == dr)
00532                         return i;
00533         }
00534         return -1;
00535 }
00536 
00537 const QString Git::getLaneParent(SCRef fromSHA, uint laneNum) {
00538 
00539         const Rev* rs = revLookup(fromSHA);
00540         if (!rs)
00541                 return "";
00542 
00543         for (int idx = rs->orderIdx - 1; idx >= 0; idx--) {
00544 
00545                 const Rev* r = revLookup(revData.revOrder[idx]);
00546                 if (laneNum >= r->lanes.count())
00547                         return "";
00548 
00549                 if (!isFreeLane(r->lanes[laneNum])) {
00550 
00551                         int type = r->lanes[laneNum], parNum = 0;
00552                         while (!isMerge(type) && type != ACTIVE) {
00553 
00554                                 if (isHead(type))
00555                                         parNum++;
00556 
00557                                 type = r->lanes[--laneNum];
00558                         }
00559                         return r->parent(parNum);
00560                 }
00561         }
00562         return "";
00563 }
00564 
00565 const QStringList Git::getChilds(SCRef parent) {
00566 
00567         QStringList childs;
00568         const Rev* r = revLookup(parent);
00569         if (!r)
00570                 return childs;
00571 
00572         for (uint i = 0; i < r->childs.count(); i++)
00573                 childs.append(revData.revOrder[r->childs[i]]);
00574 
00575         
00576         QStringList::iterator itC(childs.begin());
00577         for ( ; itC != childs.end(); ++itC) {
00578                 const Rev* r = revLookup(*itC);
00579                 (*itC).prepend(QString("%1 ").arg(r->orderIdx, 6));
00580         }
00581         childs.sort();
00582         for (itC = childs.begin(); itC != childs.end(); ++itC)
00583                 (*itC) = (*itC).section(' ', -1, -1);
00584 
00585         return childs;
00586 }
00587 
00588 const QString Git::getShortLog(SCRef sha) {
00589 
00590         const Rev* r = revLookup(sha);
00591         return (r ? r->shortLog() : "");
00592 }
00593 
00594 MyProcess* Git::getDiff(SCRef sha, QObject* receiver, SCRef diffToSha, bool combined) {
00595 
00596         if (sha.isEmpty())
00597                 return NULL;
00598 
00599         QString runCmd;
00600         if (sha != ZERO_SHA) {
00601                 runCmd = "git diff-tree --no-color -r --patch-with-stat ";
00602                 runCmd.append(combined ? "-c " : "-C -m "); 
00603                 runCmd.append(diffToSha + " " + sha); 
00604         } else
00605                 runCmd = "git diff-index --no-color -r -m --patch-with-stat HEAD";
00606 
00607         return runAsync(runCmd, receiver);
00608 }
00609 
00610 const QString Git::getFileSha(SCRef file, SCRef revSha) {
00611 
00612         if (revSha == ZERO_SHA) {
00613                 QStringList files, dummy;
00614                 getWorkDirFiles(files, dummy, RevFile::ANY);
00615                 if (files.contains(file))
00616                         return ZERO_SHA; 
00617         }
00618         const QString sha(revSha == ZERO_SHA ? "HEAD" : revSha);
00619         QString runCmd("git ls-tree -r " + sha + " " + quote(file)), runOutput;
00620         if (!run(runCmd, &runOutput))
00621                 return "";
00622 
00623         return runOutput.mid(12, 40); 
00624 }
00625 
00626 MyProcess* Git::getFile(SCRef file, SCRef revSha, QObject* receiver,
00627                         QByteArray* result, QString* fSha) {
00628 
00629         QString runCmd;
00630         
00631 
00632 
00633 
00634 
00635 
00636 
00637 
00638 
00639 
00640         const QString fileSha(getFileSha(file, revSha));
00641         if (fileSha == ZERO_SHA)
00642                 runCmd = "cat " + quote(file);
00643         else {
00644                 if (fileSha.isEmpty()) 
00645                         runCmd = "git diff-tree HEAD HEAD"; 
00646                 else
00647                         runCmd = "git cat-file blob " + fileSha;
00648         }
00649         if (fSha)
00650                 *fSha = fileSha;
00651 
00652         if (!receiver) {
00653                 run(result, runCmd);
00654                 return NULL; 
00655         }
00656         return runAsync(runCmd, receiver);
00657 }
00658 
00659 MyProcess* Git::getHighlightedFile(SCRef file, SCRef sha, QObject* receiver, QString* result) {
00660 
00661         if (!isTextHighlighter()) {
00662                 dbs("ASSERT in getHighlightedFile: highlighter not found");
00663                 return NULL;
00664         }
00665         QString ext(file.section('.', -1, -1, QString::SectionIncludeLeadingSep));
00666         QString inputFile(workDir + "/qgit_hlght_input" + ext);
00667         if (!saveFile(file, sha, inputFile))
00668                 return NULL;
00669 
00670         QString runCmd("source-highlight --failsafe -f html -i " + quote(inputFile));
00671 
00672         if (!receiver) {
00673                 run(runCmd, result);
00674                 on_getHighlightedFile_eof();
00675                 return NULL; 
00676         }
00677         MyProcess* p = runAsync(runCmd, receiver);
00678         if (p)
00679                 connect(p, SIGNAL(eof()), this, SLOT(on_getHighlightedFile_eof()));
00680         return p;
00681 }
00682 
00683 void Git::on_getHighlightedFile_eof() {
00684 
00685         QDir dir(workDir);
00686         const QStringList sl(dir.entryList("qgit_hlght_input*"));
00687         FOREACH_SL (it, sl)
00688                 dir.remove(*it);
00689 }
00690 
00691 bool Git::saveFile(SCRef file, SCRef sha, SCRef path) {
00692 
00693         QByteArray fileData;
00694         getFile(file, sha, NULL, &fileData); 
00695         if (isBinaryFile(file))
00696                 return writeToFile(path, fileData);
00697 
00698         return writeToFile(path, QString(fileData));
00699 }
00700 
00701 bool Git::getTree(SCRef treeSha, SList names, SList shas,
00702                   SList types, bool isWorkingDir, SCRef treePath) {
00703 
00704         QStringList newFiles, unfiles, delFiles, dummy;
00705         if (isWorkingDir) { 
00706 
00707                 getWorkDirFiles(unfiles, dummy, RevFile::UNKNOWN);
00708                 FOREACH_SL (it, unfiles) { 
00709                         QFileInfo f(*it);
00710                         SCRef d(f.dirPath(false));
00711                         if (d == treePath || (treePath.isEmpty() && d == "."))
00712                                 newFiles.append(f.fileName());
00713                 }
00714                 getWorkDirFiles(delFiles, dummy, RevFile::DELETED);
00715         }
00716         
00717         const QString tree(treeSha == ZERO_SHA ? "HEAD" : treeSha);
00718         QString runOutput;
00719         if (!run("git ls-tree " + tree, &runOutput))
00720                 return false;
00721 
00722         const QStringList sl(QStringList::split('\n', runOutput));
00723         FOREACH_SL (it, sl) {
00724                 
00725                 
00726                 SCRef fn((*it).section('\t', 1, 1));
00727                 while (!newFiles.empty() && newFiles.first() < fn) {
00728                         names.append(newFiles.first());
00729                         shas.append("");
00730                         types.append("?"); 
00731                         newFiles.pop_front();
00732                 }
00733                 
00734                 SCRef fp(treePath.isEmpty() ? fn : treePath + '/' + fn);
00735                 if (delFiles.empty() || (delFiles.findIndex(fp) == -1)) {
00736                         names.append(fn);
00737                         shas.append((*it).mid(12, 40));
00738                         types.append((*it).mid(7, 4));
00739                 }
00740         }
00741         while (!newFiles.empty()) { 
00742                 names.append(newFiles.first());
00743                 shas.append("");
00744                 types.append("?"); 
00745                 newFiles.pop_front();
00746         }
00747         return true;
00748 }
00749 
00750 void Git::getWorkDirFiles(SList files, SList dirs, RevFile::StatusFlag status) {
00751 
00752         files.clear();
00753         dirs.clear();
00754         const RevFile* f = getFiles(ZERO_SHA);
00755         if (!f)
00756                 return;
00757 
00758         for (int i = 0; i < f->count(); i++) {
00759 
00760                 if (f->statusCmp(i, status)) {
00761 
00762                         SCRef fp(filePath(*f, i));
00763                         files.append(fp);
00764                         for (int j = 0, cnt = fp.contains('/'); j < cnt; j++) {
00765 
00766                                 SCRef dir(fp.section('/', 0, j));
00767                                 if (dirs.findIndex(dir) == -1)
00768                                         dirs.append(dir);
00769                         }
00770                 }
00771         }
00772 }
00773 
00774 bool Git::isNothingToCommit() {
00775 
00776         const RevFile* rf = revsFiles[ZERO_SHA];
00777         return (!rf || ((uint)rf->count() == _wd.otherFiles.count()));
00778 }
00779 
00780 bool Git::isTreeModified(SCRef sha) {
00781 
00782         const RevFile* f = getFiles(sha);
00783         if (!f)
00784                 return true; 
00785 
00786         for (int i = 0; i < f->count(); ++i)
00787                 if (!f->statusCmp(i, RevFile::MODIFIED))
00788                         return true;
00789 
00790         return false;
00791 }
00792 
00793 bool Git::isParentOf(SCRef par, SCRef child) {
00794 
00795         const Rev* c = revLookup(child);
00796         return (c && c->parentsCount() == 1 && c->parent(0) == par); 
00797 }
00798 
00799 bool Git::isSameFiles(SCRef tree1Sha, SCRef tree2Sha) {
00800 
00801         
00802         
00803         
00804         if (isParentOf(tree1Sha, tree2Sha))
00805                 return !isTreeModified(tree2Sha);
00806 
00807         if (isParentOf(tree2Sha, tree1Sha))
00808                 return !isTreeModified(tree1Sha);
00809 
00810         const QString runCmd("git diff-tree --no-color -r " + tree1Sha + " " + tree2Sha);
00811         QString runOutput;
00812         if (!run(runCmd, &runOutput))
00813                 return false;
00814 
00815         bool isChanged = (runOutput.find(" A\t") != -1 || runOutput.find(" D\t") != -1);
00816         return !isChanged;
00817 }
00818 
00819 const QStringList Git::getDescendantBranches(SCRef sha) {
00820 
00821         QStringList tl;
00822         const Rev* r = revLookup(sha);
00823         if (!r || (r->descBrnMaster == -1))
00824                 return tl;
00825 
00826         const QValueVector<int>& nr = revLookup(revData.revOrder[r->descBrnMaster])->descBranches;
00827 
00828         for (uint i = 0; i < nr.count(); i++) {
00829 
00830                 SCRef sha = revData.revOrder[nr[i]];
00831                 SCRef cap = " (" + sha + ") ";
00832                 RefMap::const_iterator it(refsShaMap.find(sha));
00833                 if (it == refsShaMap.constEnd())
00834                         continue;
00835 
00836                 if (!(*it).branches.empty())
00837                         tl.append((*it).branches.join(cap).append(cap));
00838 
00839                 if (!(*it).remoteBranches.empty())
00840                         tl.append((*it).remoteBranches.join(cap).append(cap));
00841         }
00842         return tl;
00843 }
00844 
00845 const QStringList Git::getNearTags(bool goDown, SCRef sha) {
00846 
00847         QStringList tl;
00848         const Rev* r = revLookup(sha);
00849         if (!r)
00850                 return tl;
00851 
00852         int nearRefsMaster = (goDown ? r->descRefsMaster : r->ancRefsMaster);
00853         if (nearRefsMaster == -1)
00854                 return tl;
00855 
00856         const QValueVector<int>& nr = goDown ? revLookup(revData.revOrder[nearRefsMaster])->descRefs :
00857                                                revLookup(revData.revOrder[nearRefsMaster])->ancRefs;
00858 
00859         for (uint i = 0; i < nr.count(); i++) {
00860 
00861                 SCRef sha = revData.revOrder[nr[i]];
00862                 SCRef cap = " (" + sha + ")";
00863                 RefMap::const_iterator it(refsShaMap.find(sha));
00864                 if (it != refsShaMap.constEnd())
00865                         tl.append((*it).tags.join(cap).append(cap));
00866         }
00867         return tl;
00868 }
00869 
00870 const QString Git::getDefCommitMsg() {
00871 
00872         QString sha(ZERO_SHA);
00873         if (isStGIT && !getAllRefSha(APPLIED).isEmpty()) {
00874                 QString top;
00875                 if (run("stg top", &top))
00876                         sha = getRefSha(top.stripWhiteSpace(), APPLIED, false);
00877         }
00878         const Rev* c = revLookup(sha);
00879         if (!c) {
00880                 dbp("ASSERT: getDefCommitMsg sha <%1> not found", sha);
00881                 return "";
00882         }
00883         if (sha == ZERO_SHA)
00884                 return c->longLog();
00885 
00886         return c->shortLog() + '\n' + c->longLog().stripWhiteSpace();
00887 }
00888 
00889 const QString Git::colorMatch(SCRef txt, QRegExp& regExp) {
00890 
00891         QString text(txt);
00892         if (regExp.isEmpty())
00893                 return text;
00894 
00895         
00896         SCRef startCol(QString::fromLatin1("$_1b$_2$_1font color=\"red\"$_2"));
00897         SCRef endCol(QString::fromLatin1("$_1/font$_2$_1/b$_2"));
00898         int pos = 0;
00899         while ((pos = text.find(regExp, pos)) != -1) {
00900 
00901                 SCRef match(regExp.cap(0));
00902                 const QString coloredText(startCol + match + endCol);
00903                 text.replace(pos, match.length(), coloredText);
00904                 pos += coloredText.length();
00905         }
00906         return text;
00907 }
00908 
00909 const QString Git::getDesc(SCRef sha, QRegExp& shortLogRE, QRegExp& longLogRE, bool showHeader) {
00910 
00911         if (sha.isEmpty())
00912                 return "";
00913 
00914         const Rev* c = revLookup(sha);
00915         if (!c)            
00916                 return ""; 
00917 
00918         QString text;
00919         if (c->isDiffCache)
00920                 text = c->longLog();
00921         else {
00922                 if (showHeader) {
00923                         text = QString("Author: " + c->author() + "\nDate:   ");
00924                         text.append(getLocalDate(c->authorDate()));
00925                         if (!c->isUnApplied && !c->isApplied) {
00926                                 text.append("\nParent: ").append(c->parents().join("\nParent: "));
00927 
00928                                 QStringList sl = getChilds(sha);
00929                                 if (!sl.isEmpty())
00930                                         text.append("\nChild: ").append(sl.join("\nChild: "));
00931 
00932                                 sl = getDescendantBranches(sha);
00933                                 if (!sl.empty())
00934                                         text.append("\nBranch: ").append(sl.join("\nBranch: "));
00935 
00936                                 sl = getNearTags(!optGoDown, sha);
00937                                 if (!sl.isEmpty())
00938                                         text.append("\nFollows: ").append(sl.join(", "));
00939 
00940                                 sl = getNearTags(optGoDown, sha);
00941                                 if (!sl.isEmpty())
00942                                         text.append("\nPrecedes: ").append(sl.join(", "));
00943                         }
00944                         text.append("\n\n    " + colorMatch(c->shortLog(), shortLogRE) + "\n");
00945                 }
00946                 const QString log(colorMatch(c->longLog(), longLogRE));
00947                 if (!showHeader && log.stripWhiteSpace().isEmpty())
00948                         text.append(colorMatch(c->shortLog(), shortLogRE));
00949                 else
00950                         text.append(log);
00951         }
00952         
00953         QTextCodec* tc = QTextCodec::codecForCStrings();
00954         QTextCodec::setCodecForCStrings(0);
00955         text = QStyleSheet::convertFromPlainText(text); 
00956         QTextCodec::setCodecForCStrings(tc);
00957 
00958         
00959         
00960         
00961         
00962         
00963         
00964         
00965         QRegExp reSHA("..[0-9a-f]{21,40}|[^:][\\s(][0-9a-f]{6,20}", false);
00966         reSHA.setMinimal(false);
00967         int pos = 0;
00968         while ((pos = text.find(reSHA, pos)) != -1) {
00969 
00970                 SCRef ref = reSHA.cap(0).mid(2);
00971                 const Rev* r = (ref.length() == 40 ? revLookup(ref) : revLookup(getRefSha(ref)));
00972                 if (r) {
00973                         QString slog(r->shortLog());
00974                         if (slog.isEmpty()) 
00975                                 slog = r->sha();
00976                         if (slog.length() > 60)
00977                                 slog = slog.left(57).stripWhiteSpace().append("...");
00978 
00979                         slog = QStyleSheet::escape(slog);
00980                         const QString link("<a href=\"" + r->sha() + "\">" + slog + "</a>");
00981                         text.replace(pos + 2, ref.length(), link);
00982                         pos += link.length();
00983                 } else
00984                         pos += reSHA.cap(0).length();
00985         }
00986         text.replace("$_1", "<"); 
00987         text.replace("$_2", ">");
00988         return text;
00989 }
00990 
00991 const RevFile* Git::insertNewFiles(SCRef sha, SCRef data) {
00992 
00993         RevFile* rf = new RevFile();
00994         parseDiffFormat(*rf, data);
00995         revsFiles.insert(sha, rf);
00996         return rf;
00997 }
00998 
00999 const RevFile* Git::getAllMergeFiles(const Rev* r) {
01000 
01001         SCRef mySha(ALL_MERGE_FILES + r->sha());
01002         RevFile* rf = revsFiles[mySha];
01003         if (rf)
01004                 return rf;
01005 
01006         QString runCmd("git diff-tree --no-color  -r -m -C " + r->sha()), runOutput;
01007         if (!run(runCmd, &runOutput))
01008                 return NULL;
01009 
01010         return insertNewFiles(mySha, runOutput);
01011 }
01012 
01013 const RevFile* Git::getFiles(SCRef sha, SCRef diffToSha, bool allFiles, SCRef path) {
01014 
01015         const Rev* r = revLookup(sha);
01016         if (!r)
01017                 return NULL;
01018 
01019         if (r->parentsCount() == 0) 
01020                 return NULL;
01021 
01022         if (r->parentsCount() > 1 && diffToSha.isEmpty() && allFiles)
01023                 return getAllMergeFiles(r);
01024 
01025         if (!diffToSha.isEmpty() && (sha != ZERO_SHA)) {
01026 
01027                 QString runCmd("git diff-tree --no-color -r -m -C ");
01028                 runCmd.append(diffToSha + " " + sha);
01029                 if (!path.isEmpty())
01030                         runCmd.append(" " + path);
01031 
01032                 QString runOutput;
01033                 if (!run(runCmd, &runOutput))
01034                         return NULL;
01035 
01036                 
01037                 
01038                 return insertNewFiles(CUSTOM_SHA, runOutput);
01039         }
01040         RevFile* rf = revsFiles[sha];
01041         if (rf)
01042                 return rf; 
01043 
01044         if (sha == ZERO_SHA) {
01045                 dbs("ASSERT in Git::getFiles, ZERO_SHA not found");
01046                 return NULL;
01047         }
01048         QString runCmd("git diff-tree --no-color -r -c -C " + sha), runOutput;
01049         if (!run(runCmd, &runOutput))
01050                 return NULL;
01051 
01052         rf = revsFiles[sha];
01053         if (rf) 
01054                 return rf;
01055 
01056         cacheNeedsUpdate = true;
01057         return insertNewFiles(sha, runOutput);
01058 }
01059 
01060 bool Git::startFileHistory(FileHistory* fh) {
01061 
01062         return startRevList(fh->fileName, fh);
01063 }
01064 
01065 void Git::getFileFilter(SCRef path, QMap<QString, bool>& shaMap) {
01066 
01067         shaMap.clear();
01068         QRegExp rx(path, false, true); 
01069         FOREACH (StrVect, it, revData.revOrder) {
01070                 RevFile* rf = revsFiles[*it];
01071                 if (!rf)
01072                         continue;
01073 
01074                 
01075                 for (int i = 0; i < rf->count(); ++i)
01076                         if (rx.search(filePath(*rf, i)) != -1) {
01077                                 shaMap.insert(*it, true);
01078                                 break;
01079                         }
01080         }
01081 }
01082 
01083 bool Git::getPatchFilter(SCRef exp, bool isRegExp, QMap<QString, bool>& shaMap) {
01084 
01085         shaMap.clear();
01086         QString buf;
01087         FOREACH (StrVect, it, revData.revOrder)
01088                 if (*it != ZERO_SHA)
01089                         buf.append(*it).append('\n');
01090 
01091         if (buf.isEmpty())
01092                 return true;
01093 
01094         QString runCmd("git diff-tree --no-color -r -s --stdin "), runOutput;
01095         if (isRegExp)
01096                 runCmd.append("--pickaxe-regex ");
01097 
01098         runCmd.append(quote("-S" + exp));
01099         if (!run(runCmd, &runOutput, NULL, buf)) 
01100                 return false;
01101 
01102         const QStringList sl(QStringList::split('\n', runOutput));
01103         FOREACH_SL (it, sl)
01104                 shaMap.insert(*it, true);
01105 
01106         return true;
01107 }
01108 
01109 bool Git::resetCommits(int parentDepth) {
01110 
01111         QString runCmd("git reset --soft HEAD~");
01112         runCmd.append(QString::number(parentDepth));
01113         return run(runCmd);
01114 }
01115 
01116 bool Git::applyPatchFile(SCRef patchPath, bool fold, bool isDragDrop) {
01117 
01118         if (isStGIT) {
01119                 if (fold) {
01120                         bool ok = run("stg fold " + quote(patchPath)); 
01121                         if (ok)
01122                                 ok = run("stg refresh"); 
01123                         return ok;
01124                 } else
01125                         return run("stg import --mail " + quote(patchPath));
01126         }
01127         QString runCmd("git am --utf8 --3way ");
01128         if (isDragDrop)
01129                 runCmd.append("--keep ");
01130 
01131         else if (testFlag(SIGN_PATCH_F))
01132                 runCmd.append("--signoff ");
01133 
01134         return run(runCmd + quote(patchPath));
01135 }
01136 
01137 bool Git::formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir) {
01138 
01139         bool remote = !remoteDir.isEmpty();
01140         QSettings settings;
01141         const QString FPArgs = settings.readEntry(APP_KEY + FPATCH_ARGS_KEY, "");
01142 
01143         QString runCmd("git format-patch --no-color");
01144         if (testFlag(NUMBERS_F) && !remote)
01145                 runCmd.append(" -n");
01146 
01147         if (remote)
01148                 runCmd.append(" --keep-subject");
01149 
01150         runCmd.append(" -o " + dirPath);
01151         if (!FPArgs.isEmpty())
01152                 runCmd.append(" " + FPArgs);
01153 
01154         runCmd.append(" --start-number=");
01155 
01156         const QString tmp(workDir);
01157         if (remote)
01158                 workDir = remoteDir; 
01159 
01160         int n = shaList.count();
01161         bool ret = false;
01162         FOREACH_SL (it, shaList) { 
01163                 const QString cmd(runCmd + QString::number(n) + " " +
01164                                   *it + QString::fromLatin1("^..") + *it);
01165                 n--;
01166                 ret = run(cmd);
01167                 if (!ret)
01168                         break;
01169         }
01170         workDir = tmp;
01171         return ret;
01172 }
01173 
01174 bool Git::updateIndex(SCList selFiles) {
01175 
01176         if (selFiles.empty())
01177                 return true;
01178 
01179         QString runCmd("git update-index --add --remove --replace -- ");
01180         runCmd.append(quote(selFiles));
01181         return run(runCmd);
01182 }
01183 
01184 bool Git::commitFiles(SCList selFiles, SCRef msg) {
01185 
01186 
01187 
01188         const QString msgFile(gitDir + "/qgit_cmt_msg");
01189         if (!writeToFile(msgFile, msg)) 
01190                 return false;
01191 
01192         
01193         QSettings settings;
01194         const QString CMArgs = settings.readEntry(APP_KEY + CMT_ARGS_KEY, "");
01195 
01196         QString cmtOptions;
01197         if (!CMArgs.isEmpty())
01198                 cmtOptions.append(" " + CMArgs);
01199 
01200         if (testFlag(SIGN_CMT_F))
01201                 cmtOptions.append(" -s");
01202 
01203         if (testFlag(VERIFY_CMT_F))
01204                 cmtOptions.append(" -v");
01205 
01206         
01207         
01208         const QStringList notSelInIndexFiles(getOtherFiles(selFiles, optOnlyInIndex));
01209 
01210         
01211         
01212         
01213         QStringList selNotDelFiles;
01214         const RevFile* files = getFiles(ZERO_SHA); 
01215         FOREACH_SL (it, selFiles) {
01216                 int idx = findFileIndex(*files, *it);
01217                 if (!files->statusCmp(idx, RevFile::DELETED))
01218                         selNotDelFiles.append(*it);
01219         }
01220         
01221         
01222         if (!notSelInIndexFiles.empty())
01223                 if (!run("git read-tree --reset HEAD"))
01224                         return false;
01225 
01226         
01227         
01228         
01229         updateIndex(selFiles);
01230 
01231         
01232         
01233         QString runCmd("git commit" + cmtOptions + " -F " + msgFile);
01234         if (!selNotDelFiles.empty())
01235                 runCmd.append(" -i " + quote(selNotDelFiles));
01236 
01237         if (!run(runCmd))
01238                 return false;
01239 
01240         
01241         if (!notSelInIndexFiles.empty())
01242                 if (!updateIndex(notSelInIndexFiles))
01243                         return false;
01244 
01245         QDir dir(workDir);
01246         dir.remove(msgFile);
01247         return true;
01248 }
01249 
01250 bool Git::mkPatchFromIndex(SCRef msg, SCRef patchFile) {
01251 
01252         QString runOutput;
01253         if (!run("git diff-index --no-color --cached -p HEAD", &runOutput))
01254                 return false;
01255 
01256         const QString patch("Subject: " + msg + "\n---\n" + runOutput);
01257         return writeToFile(patchFile, patch);
01258 }
01259 
01260 const QStringList Git::getOtherFiles(SCList selFiles, bool onlyInIndex) {
01261 
01262         const RevFile* files = getFiles(ZERO_SHA); 
01263         QStringList notSelFiles;
01264         for (int i = 0; i < files->count(); ++i) {
01265                 SCRef fp = filePath(*files, i);
01266                 if (selFiles.find(fp) == selFiles.constEnd()) { 
01267                         if (!onlyInIndex)
01268                                 notSelFiles.append(fp);
01269                         else if (files->statusCmp(i, RevFile::IN_INDEX))
01270                                 notSelFiles.append(fp);
01271                 }
01272         }
01273         return notSelFiles;
01274 }
01275 
01276 void Git::removeFiles(SCList selFiles, SCRef workDir, SCRef ext) {
01277 
01278         QDir d(workDir);
01279         FOREACH_SL (it, selFiles)
01280                 d.rename(*it, *it + ext);
01281 }
01282 
01283 void Git::restoreFiles(SCList selFiles, SCRef workDir, SCRef ext) {
01284 
01285         QDir d(workDir);
01286         FOREACH_SL (it, selFiles)
01287                 d.rename(*it + ext, *it); 
01288 }
01289 
01290 void Git::removeDeleted(SCList selFiles) {
01291 
01292         QDir dir(workDir);
01293         const RevFile* files = getFiles(ZERO_SHA); 
01294         FOREACH_SL (it, selFiles) {
01295                 int idx = findFileIndex(*files, *it);
01296                 if (files->statusCmp(idx, RevFile::DELETED))
01297                         dir.remove(*it);
01298         }
01299 }
01300 
01301 bool Git::stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold) {
01302 
01303         
01304         
01305         
01306         bool retval = true;
01307         const QString patchFile(gitDir + "/qgit_tmp_patch");
01308         const QString extNS(".qgit_removed_not_selected");
01309         const QString extS(".qgit_removed_selected");
01310 
01311         
01312         
01313         
01314         const QStringList notSelFiles = getOtherFiles(selFiles, !optOnlyInIndex);
01315         const QStringList notSelInIndexFiles = getOtherFiles(selFiles, optOnlyInIndex);
01316 
01317         
01318         if (!run("git read-tree --reset HEAD"))
01319                 goto error;
01320         if (!updateIndex(selFiles))
01321                 goto error;
01322 
01323         
01324         if (!mkPatchFromIndex(msg, patchFile))
01325                 goto error;
01326 
01327         
01328         removeFiles(selFiles, workDir, extS); 
01329         removeFiles(notSelFiles, workDir, extNS); 
01330 
01331         
01332         if (!run("git read-tree --reset HEAD"))
01333                 goto error;
01334         if (!run("git checkout-index -q -f -u -a"))
01335                 goto rollback;
01336 
01337         
01338         if (fold) {
01339                 
01340                 if (!msg.isEmpty()) {
01341                         if (!run("stg refresh --message \"" + msg.stripWhiteSpace() + "\""))
01342                                 goto rollback;
01343                 }
01344                 if (!run("stg fold " + quote(patchFile)))
01345                         goto rollback;
01346                 if (!run("stg refresh")) 
01347                         goto rollback;
01348         } else {
01349                 if (!run("stg import --mail --name " + quote(patchName) + " " + quote(patchFile)))
01350                         goto rollback;
01351         }
01352         goto exit;
01353 
01354 rollback:
01355         restoreFiles(selFiles, workDir, extS);
01356         removeDeleted(selFiles); 
01357 
01358 error:
01359         retval = false;
01360 
01361 exit:
01362         
01363         
01364         restoreFiles(notSelFiles, workDir, extNS);
01365         updateIndex(notSelInIndexFiles);
01366         QDir dir(workDir);
01367         dir.remove(patchFile);
01368         FOREACH_SL (it, selFiles)
01369                 dir.remove(*it + extS); 
01370         return retval;
01371 }
01372 
01373 bool Git::makeTag(SCRef sha, SCRef tagName, SCRef msg) {
01374 
01375         if (msg.isEmpty())
01376                 return run("git tag " + tagName + " " + sha);
01377 
01378         return run("git tag -m \"" + msg + "\" " + tagName + " " + sha);
01379 }
01380 
01381 bool Git::deleteTag(SCRef sha) {
01382 
01383         const QStringList tags(getRefName(sha, TAG));
01384         if (!tags.empty())
01385                 return run("git tag -d " + tags.first()); 
01386 
01387         return false;
01388 }
01389 
01390 bool Git::stgPush(SCRef sha) {
01391 
01392         const QStringList patch(getRefName(sha, UN_APPLIED));
01393         if (patch.count() != 1) {
01394                 dbp("ASSERT in Git::stgPush, found %1 patches instead of 1", patch.count());
01395                 return false;
01396         }
01397         return run("stg push " + quote(patch.first()));
01398 }
01399 
01400 bool Git::stgPop(SCRef sha) {
01401 
01402         QString top;
01403         if (!run("stg top", &top))
01404                 return false;
01405 
01406         const QStringList patch(getRefName(sha, APPLIED));
01407         if (patch.count() != 1) {
01408                 dbp("ASSERT in Git::stgPop, found %1 patches instead of 1", patch.count());
01409                 return false;
01410         }
01411         if (patch.first() != top.stripWhiteSpace())
01412                 if (!run("stg pop " + quote(patch)))
01413                         return false;
01414 
01415         return run("stg pop"); 
01416 }