Add "GNU source-highlight not installed" to Source highlight button tooltip, if not...
[qgit4.git] / src / git.cpp
blob50e6dd3e6679a55f8ac3cfd5f79a42b524e1eb9a
1 /*
2 Description: interface to git programs
4 Author: Marco Costalba (C) 2005-2007
6 Copyright: See COPYING file that comes with this distribution
8 */
9 #include <QApplication>
10 #include <QDateTime>
11 #include <QDir>
12 #include <QFile>
13 #include <QFontMetrics>
14 #include <QImageReader>
15 #include <QRegExp>
16 #include <QSet>
17 #include <QSettings>
18 #include <QTextCodec>
19 #include <QTextDocument>
20 #include <QTextStream>
21 #include "annotate.h"
22 #include "cache.h"
23 #include "git.h"
24 #include "lanes.h"
25 #include "myprocess.h"
27 using namespace QGit;
29 FileHistory::FileHistory(QObject* p, Git* g) : QAbstractItemModel(p), git(g) {
31 headerInfo << "Graph" << "Id" << "Short Log" << "Author" << "Author Date";
32 lns = new Lanes();
33 revs.reserve(QGit::MAX_DICT_SIZE);
34 clear(); // after _headerInfo is set
36 connect(git, SIGNAL(newRevsAdded(const FileHistory*, const QVector<ShaString>&)),
37 this, SLOT(on_newRevsAdded(const FileHistory*, const QVector<ShaString>&)));
39 connect(git, SIGNAL(loadCompleted(const FileHistory*, const QString&)),
40 this, SLOT(on_loadCompleted(const FileHistory*, const QString&)));
42 connect(git, SIGNAL(changeFont(const QFont&)), this, SLOT(on_changeFont(const QFont&)));
45 FileHistory::~FileHistory() {
47 clear();
48 delete lns;
51 void FileHistory::resetFileNames(SCRef fn) {
53 fNames.clear();
54 fNames.append(fn);
55 curFNames = fNames;
58 int FileHistory::rowCount(const QModelIndex& parent) const {
60 return (!parent.isValid() ? rowCnt : 0);
63 bool FileHistory::hasChildren(const QModelIndex& parent) const {
65 return !parent.isValid();
68 int FileHistory::row(SCRef sha) const {
70 const Rev* r = git->revLookup(sha, this);
71 return (r ? r->orderIdx : -1);
74 const QString FileHistory::sha(int row) const {
76 return (row < 0 || row >= rowCnt ? "" : QString(revOrder.at(row)));
79 void FileHistory::flushTail() {
81 if (earlyOutputCnt < 0 || earlyOutputCnt >= revOrder.count()) {
82 dbp("ASSERT in FileHistory::flushTail(), earlyOutputCnt is %1", earlyOutputCnt);
83 return;
85 int cnt = revOrder.count() - earlyOutputCnt + 1;
86 while (cnt > 0) {
87 const ShaString& sha = revOrder.last();
88 const Rev* c = revs[sha];
89 delete c;
90 revs.remove(sha);
91 revOrder.pop_back();
92 cnt--;
94 // reset all lanes, will be redrawn
95 for (int i = earlyOutputCntBase; i < revOrder.count(); i++) {
96 Rev* c = const_cast<Rev*>(revs[revOrder[i]]);
97 c->lanes.clear();
99 firstFreeLane = earlyOutputCntBase;
100 lns->clear();
101 rowCnt = revOrder.count();
102 reset();
105 void FileHistory::clear(bool complete) {
107 if (!complete) {
108 if (revOrder.count() > 0)
109 flushTail();
110 return;
112 git->cancelDataLoading(this);
114 qDeleteAll(revs);
115 revs.clear();
116 revOrder.clear();
117 firstFreeLane = loadTime = earlyOutputCntBase = 0;
118 setEarlyOutputState(false);
119 lns->clear();
120 fNames.clear();
121 curFNames.clear();
122 qDeleteAll(rowData);
123 rowData.clear();
125 if (testFlag(REL_DATE_F)) {
126 secs = QDateTime::currentDateTime().toTime_t();
127 headerInfo[4] = "Last Change";
128 } else {
129 secs = 0;
130 headerInfo[4] = "Author Date";
132 rowCnt = revOrder.count();
133 annIdValid = false;
134 reset();
135 emit headerDataChanged(Qt::Horizontal, 0, 4);
138 void FileHistory::on_newRevsAdded(const FileHistory* fh, const QVector<ShaString>& shaVec) {
140 if (fh != this) // signal newRevsAdded() is broadcast
141 return;
143 // do not process revisions if there are possible renamed points
144 // or pending renamed patch to apply
145 if (!renamedRevs.isEmpty() || !renamedPatches.isEmpty())
146 return;
148 // do not attempt to insert 0 rows since the inclusive range would be invalid
149 if (rowCnt == shaVec.count())
150 return;
152 beginInsertRows(QModelIndex(), rowCnt, shaVec.count()-1);
153 rowCnt = shaVec.count();
154 endInsertRows();
157 void FileHistory::on_loadCompleted(const FileHistory* fh, const QString&) {
159 if (fh != this || rowCnt >= revOrder.count())
160 return;
162 // now we can process last revision
163 rowCnt = revOrder.count();
164 reset(); // force a reset to avoid artifacts in file history graph under Windows
166 // adjust Id column width according to the numbers of revisions we have
167 if (!git->isMainHistory(this))
168 on_changeFont(QGit::STD_FONT);
171 void FileHistory::on_changeFont(const QFont& f) {
173 QString maxStr(QString::number(rowCnt).length() + 1, '8');
174 QFontMetrics fmRows(f);
175 int neededWidth = fmRows.boundingRect(maxStr).width();
177 QString id("Id");
178 QFontMetrics fmId(qApp->font());
180 while (fmId.boundingRect(id).width() < neededWidth)
181 id += ' ';
183 headerInfo[1] = id;
184 emit headerDataChanged(Qt::Horizontal, 1, 1);
187 Qt::ItemFlags FileHistory::flags(const QModelIndex&) const {
189 return Qt::ItemIsEnabled | Qt::ItemIsSelectable; // read only
192 QVariant FileHistory::headerData(int section, Qt::Orientation orientation, int role) const {
194 if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
195 return headerInfo.at(section);
197 return QVariant();
200 QModelIndex FileHistory::index(int row, int column, const QModelIndex&) const {
202 index() is called much more then data(), also by a 100X factor on
203 big archives, so we use just the row number as QModelIndex payload
204 and defer the revision lookup later, inside data().
205 Because row and column info are stored anyway in QModelIndex we
206 don't need to add any additional data.
208 if (row < 0 || row >= rowCnt)
209 return QModelIndex();
211 return createIndex(row, column, 0);
214 QModelIndex FileHistory::parent(const QModelIndex&) const {
216 static const QModelIndex no_parent;
217 return no_parent;
220 const QString FileHistory::timeDiff(unsigned long secs) const {
222 uint days = secs / (3600 * 24);
223 uint hours = (secs - days * 3600 * 24) / 3600;
224 uint min = (secs - days * 3600 * 24 - hours * 3600) / 60;
225 uint sec = secs - days * 3600 * 24 - hours * 3600 - min * 60;
226 QString tmp;
227 if (days > 0)
228 tmp.append(QString::number(days) + "d ");
230 if (hours > 0 || !tmp.isEmpty())
231 tmp.append(QString::number(hours) + "h ");
233 if (min > 0 || !tmp.isEmpty())
234 tmp.append(QString::number(min) + "m ");
236 tmp.append(QString::number(sec) + "s");
237 return tmp;
240 QVariant FileHistory::data(const QModelIndex& index, int role) const {
242 static const QVariant no_value;
244 if (!index.isValid() || role != Qt::DisplayRole)
245 return no_value; // fast path, 90% of calls ends here!
247 const Rev* r = git->revLookup(revOrder.at(index.row()), this);
248 if (!r)
249 return no_value;
251 int col = index.column();
253 // calculate lanes
254 if (r->lanes.count() == 0)
255 git->setLane(r->sha(), const_cast<FileHistory*>(this));
257 if (col == QGit::ANN_ID_COL)
258 return (annIdValid ? rowCnt - index.row() : QVariant());
260 if (col == QGit::LOG_COL)
261 return r->shortLog();
263 if (col == QGit::AUTH_COL)
264 return r->author();
266 if (col == QGit::TIME_COL && r->sha() != QGit::ZERO_SHA_RAW) {
268 if (secs != 0) // secs is 0 for absolute date
269 return timeDiff(secs - r->authorDate().toULong());
270 else
271 return git->getLocalDate(r->authorDate());
273 return no_value;
276 // ****************************************************************************
278 bool Git::TreeEntry::operator<(const TreeEntry& te) const {
280 if (this->type == te.type)
281 return (this->name < te.name);
283 // directories are smaller then files
284 // to appear as first when sorted
285 if (this->type == "tree")
286 return true;
288 if (te.type == "tree")
289 return false;
291 return (this->name < te.name);
294 Git::Git(QObject* p) : QObject(p) {
296 EM_INIT(exGitStopped, "Stopping connection with git");
298 fileCacheAccessed = cacheNeedsUpdate = isMergeHead = false;
299 isStGIT = isGIT = loadingUnAppliedPatches = isTextHighlighterFound = false;
300 errorReportingEnabled = true; // report errors if run() fails
301 curDomain = NULL;
302 revData = NULL;
303 revsFiles.reserve(MAX_DICT_SIZE);
306 void Git::checkEnvironment() {
308 QString version;
309 if (run("git --version", &version)) {
311 version = version.section(' ', -1, -1).section('.', 0, 2);
312 if (version < GIT_VERSION) {
314 // simply send information, the 'not compatible version'
315 // policy should be implemented upstream
316 const QString cmd("Current git version is " + version +
317 " but is required " + GIT_VERSION + " or better");
319 const QString errorDesc("Your installed git is too old."
320 "\nPlease upgrade to avoid possible misbehaviours.");
322 MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc);
323 QApplication::postEvent(parent(), e);
325 } else {
326 dbs("Cannot find git files");
327 return;
329 errorReportingEnabled = false;
330 isTextHighlighterFound = run("source-highlight -V", &version);
331 errorReportingEnabled = true;
332 if (isTextHighlighterFound)
333 textHighlighterVersionFound = version.section('\n', 0, 0);
334 else
335 textHighlighterVersionFound = "GNU source-highlight not installed";
338 void Git::userInfo(SList info) {
340 git looks for commit user information in following order:
342 - GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL environment variables
343 - repository config file
344 - global config file
345 - your name, hostname and domain
347 const QString env(QProcess::systemEnvironment().join(","));
348 QString user(env.section("GIT_AUTHOR_NAME", 1).section(",", 0, 0).section("=", 1).trimmed());
349 QString email(env.section("GIT_AUTHOR_EMAIL", 1).section(",", 0, 0).section("=", 1).trimmed());
351 info.clear();
352 info << "Environment" << user << email;
354 errorReportingEnabled = false; // 'git config' could fail, see docs
356 run("git config user.name", &user);
357 run("git config user.email", &email);
358 info << "Local config" << user << email;
360 run("git config --global user.name", &user);
361 run("git config --global user.email", &email);
362 info << "Global config" << user << email;
364 errorReportingEnabled = true;
367 const QStringList Git::getGitConfigList(bool global) {
369 QString runOutput;
371 errorReportingEnabled = false; // 'git config' could fail, see docs
373 if (global)
374 run("git config --global --list", &runOutput);
375 else
376 run("git config --list", &runOutput);
378 errorReportingEnabled = true;
380 return runOutput.split('\n', QString::SkipEmptyParts);
383 bool Git::isImageFile(SCRef file) {
385 const QString ext(file.section('.', -1).toLower());
386 return QImageReader::supportedImageFormats().contains(ext.toAscii());
389 bool Git::isBinaryFile(SCRef file) {
391 static const char* binaryFileExtensions[] = {"bmp", "gif", "jpeg", "jpg",
392 "png", "svg", "tiff", "pcx", "xcf", "xpm",
393 "bz", "bz2", "rar", "tar", "z", "gz", "tgz", "zip", 0};
395 if (isImageFile(file))
396 return true;
398 const QString ext(file.section('.', -1).toLower());
399 int i = 0;
400 while (binaryFileExtensions[i] != 0)
401 if (ext == binaryFileExtensions[i++])
402 return true;
403 return false;
406 void Git::setThrowOnStop(bool b) {
408 if (b)
409 EM_REGISTER(exGitStopped);
410 else
411 EM_REMOVE(exGitStopped);
414 bool Git::isThrowOnStopRaised(int excpId, SCRef curContext) {
416 return EM_MATCH(excpId, exGitStopped, curContext);
419 void Git::setTextCodec(QTextCodec* tc) {
421 QTextCodec::setCodecForCStrings(tc); // works also with tc == 0 (Latin1)
422 QTextCodec::setCodecForLocale(tc);
423 QString name(tc ? tc->name() : "Latin1");
425 // workaround Qt issue of mime name different from
426 // standard http://www.iana.org/assignments/character-sets
427 if (name == "Big5-HKSCS")
428 name = "Big5";
430 run("git repo-config i18n.commitencoding " + name);
433 QTextCodec* Git::getTextCodec(bool* isGitArchive) {
435 *isGitArchive = isGIT;
436 if (!isGIT) // can be called also when not in an archive
437 return NULL;
439 QString runOutput;
440 if (!run("git repo-config --get i18n.commitencoding", &runOutput))
441 return NULL;
443 if (runOutput.isEmpty()) // git docs says default is utf-8
444 return QTextCodec::codecForName(QByteArray("utf8"));
446 return QTextCodec::codecForName(runOutput.trimmed().toLatin1());
449 const QString Git::quote(SCRef nm) {
451 return (QUOTE_CHAR + nm + QUOTE_CHAR);
454 const QString Git::quote(SCList sl) {
456 QString q(sl.join(QUOTE_CHAR + ' ' + QUOTE_CHAR));
457 q.prepend(QUOTE_CHAR).append(QUOTE_CHAR);
458 return q;
461 uint Git::checkRef(const ShaString& sha, uint mask) const {
463 RefMap::const_iterator it(refsShaMap.constFind(sha));
464 return (it != refsShaMap.constEnd() ? (*it).type & mask : 0);
467 uint Git::checkRef(SCRef sha, uint mask) const {
469 RefMap::const_iterator it(refsShaMap.constFind(toTempSha(sha)));
470 return (it != refsShaMap.constEnd() ? (*it).type & mask : 0);
473 const QStringList Git::getRefName(SCRef sha, RefType type, QString* curBranch) const {
475 if (!checkRef(sha, type))
476 return QStringList();
478 const Reference& rf = refsShaMap[toTempSha(sha)];
480 if (curBranch)
481 *curBranch = rf.currentBranch;
483 if (type == TAG)
484 return rf.tags;
486 else if (type == BRANCH)
487 return rf.branches;
489 else if (type == RMT_BRANCH)
490 return rf.remoteBranches;
492 else if (type == REF)
493 return rf.refs;
495 else if (type == APPLIED || type == UN_APPLIED)
496 return QStringList(rf.stgitPatch);
498 return QStringList();
501 const QStringList Git::getAllRefSha(uint mask) {
503 QStringList shas;
504 FOREACH (RefMap, it, refsShaMap)
505 if ((*it).type & mask)
506 shas.append(it.key());
507 return shas;
510 const QString Git::getRefSha(SCRef refName, RefType type, bool askGit) {
512 bool any = (type == ANY_REF);
514 FOREACH (RefMap, it, refsShaMap) {
516 const Reference& rf = *it;
518 if ((any || type == TAG) && rf.tags.contains(refName))
519 return it.key();
521 else if ((any || type == BRANCH) && rf.branches.contains(refName))
522 return it.key();
524 else if ((any || type == RMT_BRANCH) && rf.remoteBranches.contains(refName))
525 return it.key();
527 else if ((any || type == REF) && rf.refs.contains(refName))
528 return it.key();
530 else if ((any || type == APPLIED || type == UN_APPLIED) && rf.stgitPatch == refName)
531 return it.key();
533 if (!askGit)
534 return "";
536 // if a ref was not found perhaps is an abbreviated form
537 QString runOutput;
538 errorReportingEnabled = false;
539 bool ok = run("git rev-parse --revs-only " + refName, &runOutput);
540 errorReportingEnabled = true;
541 return (ok ? runOutput.trimmed() : "");
544 void Git::appendNamesWithId(QStringList& names, SCRef sha, SCList data, bool onlyLoaded) {
546 const Rev* r = revLookup(sha);
547 if (onlyLoaded && !r)
548 return;
550 if (onlyLoaded) { // prepare for later sorting
551 SCRef cap = QString("%1 ").arg(r->orderIdx, 6);
552 FOREACH_SL (it, data)
553 names.append(cap + *it);
554 } else
555 names += data;
558 const QStringList Git::getAllRefNames(uint mask, bool onlyLoaded) {
559 // returns reference names sorted by loading order if 'onlyLoaded' is set
561 QStringList names;
562 FOREACH (RefMap, it, refsShaMap) {
564 if (mask & TAG)
565 appendNamesWithId(names, it.key(), (*it).tags, onlyLoaded);
567 if (mask & BRANCH)
568 appendNamesWithId(names, it.key(), (*it).branches, onlyLoaded);
570 if (mask & RMT_BRANCH)
571 appendNamesWithId(names, it.key(), (*it).remoteBranches, onlyLoaded);
573 if (mask & REF)
574 appendNamesWithId(names, it.key(), (*it).refs, onlyLoaded);
576 if ((mask & (APPLIED | UN_APPLIED)) && !onlyLoaded)
577 names.append((*it).stgitPatch); // doesn't work with 'onlyLoaded'
579 if (onlyLoaded) {
580 names.sort();
581 QStringList::iterator itN(names.begin());
582 for ( ; itN != names.end(); ++itN) // strip 'idx'
583 (*itN) = (*itN).section(' ', -1, -1);
585 return names;
588 const QString Git::getRevInfo(SCRef sha) {
590 if (sha.isEmpty())
591 return "";
593 uint type = checkRef(sha);
594 if (type == 0)
595 return "";
597 QString refsInfo;
598 if (type & BRANCH) {
599 const QString cap(type & CUR_BRANCH ? "HEAD: " : "Branch: ");
600 refsInfo = cap + getRefName(sha, BRANCH).join(" ");
602 if (type & RMT_BRANCH)
603 refsInfo.append(" Remote branch: " + getRefName(sha, RMT_BRANCH).join(" "));
605 if (type & TAG)
606 refsInfo.append(" Tag: " + getRefName(sha, TAG).join(" "));
608 if (type & REF)
609 refsInfo.append(" Ref: " + getRefName(sha, REF).join(" "));
611 if (type & APPLIED)
612 refsInfo.append(" Patch: " + getRefName(sha, APPLIED).join(" "));
614 if (type & UN_APPLIED)
615 refsInfo.append(" Patch: " + getRefName(sha, UN_APPLIED).join(" "));
617 if (type & TAG) {
618 SCRef msg(getTagMsg(sha));
619 if (!msg.isEmpty())
620 refsInfo.append(" [" + msg + "]");
622 return refsInfo.trimmed();
625 const QString Git::getTagMsg(SCRef sha) {
627 if (!checkRef(sha, TAG)) {
628 dbs("ASSERT in Git::getTagMsg, tag not found");
629 return "";
631 Reference& rf = refsShaMap[toTempSha(sha)];
633 if (!rf.tagMsg.isEmpty())
634 return rf.tagMsg;
636 QRegExp pgp("-----BEGIN PGP SIGNATURE*END PGP SIGNATURE-----",
637 Qt::CaseSensitive, QRegExp::Wildcard);
639 if (!rf.tagObj.isEmpty()) {
640 QString ro;
641 if (run("git cat-file tag " + rf.tagObj, &ro))
642 rf.tagMsg = ro.section("\n\n", 1).remove(pgp).trimmed();
644 return rf.tagMsg;
647 bool Git::isPatchName(SCRef nm) {
649 if (!getRefSha(nm, UN_APPLIED, false).isEmpty())
650 return true;
652 return !getRefSha(nm, APPLIED, false).isEmpty();
655 void Git::addExtraFileInfo(QString* rowName, SCRef sha, SCRef diffToSha, bool allMergeFiles) {
657 const RevFile* files = getFiles(sha, diffToSha, allMergeFiles);
658 if (!files)
659 return;
661 int idx = findFileIndex(*files, *rowName);
662 if (idx == -1)
663 return;
665 QString extSt(files->extendedStatus(idx));
666 if (extSt.isEmpty())
667 return;
669 *rowName = extSt;
672 void Git::removeExtraFileInfo(QString* rowName) {
674 if (rowName->contains(" --> ")) // return destination file name
675 *rowName = rowName->section(" --> ", 1, 1).section(" (", 0, 0);
678 void Git::formatPatchFileHeader(QString* rowName, SCRef sha, SCRef diffToSha,
679 bool combined, bool allMergeFiles) {
680 if (combined) {
681 rowName->prepend("diff --combined ");
682 return; // TODO rename/copy still not supported in this case
684 // let's see if it's a rename/copy...
685 addExtraFileInfo(rowName, sha, diffToSha, allMergeFiles);
687 if (rowName->contains(" --> ")) { // ...it is!
689 SCRef destFile(rowName->section(" --> ", 1, 1).section(" (", 0, 0));
690 SCRef origFile(rowName->section(" --> ", 0, 0));
691 *rowName = "diff --git a/" + origFile + " b/" + destFile;
692 } else
693 *rowName = "diff --git a/" + *rowName + " b/" + *rowName;
696 Annotate* Git::startAnnotate(FileHistory* fh, QObject* guiObj) { // non blocking
698 Annotate* ann = new Annotate(this, guiObj);
699 if (!ann->start(fh)) // non blocking call
700 return NULL; // ann will delete itself when done
702 return ann; // caller will delete with Git::cancelAnnotate()
705 void Git::cancelAnnotate(Annotate* ann) {
707 if (ann)
708 ann->deleteWhenDone();
711 const FileAnnotation* Git::lookupAnnotation(Annotate* ann, SCRef sha) {
713 return (ann ? ann->lookupAnnotation(sha) : NULL);
716 void Git::cancelDataLoading(const FileHistory* fh) {
717 // normally called when closing file viewer
719 emit cancelLoading(fh); // non blocking
722 const Rev* Git::revLookup(SCRef sha, const FileHistory* fh) const {
724 return revLookup(toTempSha(sha), fh);
727 const Rev* Git::revLookup(const ShaString& sha, const FileHistory* fh) const {
729 const RevMap& r = (fh ? fh->revs : revData->revs);
730 return (sha.latin1() ? r.value(sha) : NULL);
733 bool Git::run(SCRef runCmd, QString* runOutput, QObject* receiver, SCRef buf) {
735 QByteArray ba;
736 bool ret = run(runOutput ? &ba : NULL, runCmd, receiver, buf);
737 if (runOutput)
738 *runOutput = ba;
740 return ret;
743 bool Git::run(QByteArray* runOutput, SCRef runCmd, QObject* receiver, SCRef buf) {
745 MyProcess p(parent(), this, workDir, errorReportingEnabled);
746 return p.runSync(runCmd, runOutput, receiver, buf);
749 MyProcess* Git::runAsync(SCRef runCmd, QObject* receiver, SCRef buf) {
751 MyProcess* p = new MyProcess(parent(), this, workDir, errorReportingEnabled);
752 if (!p->runAsync(runCmd, receiver, buf)) {
753 delete p;
754 p = NULL;
756 return p; // auto-deleted when done
759 MyProcess* Git::runAsScript(SCRef runCmd, QObject* receiver, SCRef buf) {
761 const QString scriptFile(workDir + "/qgit_script" + QGit::SCRIPT_EXT);
762 #ifndef Q_OS_WIN32
763 // without this process doesn't start under Linux
764 QString cmd(runCmd.startsWith("#!") ? runCmd : "#!/bin/sh\n" + runCmd);
765 #else
766 QString cmd(runCmd);
767 #endif
768 if (!writeToFile(scriptFile, cmd, true))
769 return NULL;
771 MyProcess* p = runAsync(scriptFile, receiver, buf);
772 if (p)
773 connect(p, SIGNAL(eof()), this, SLOT(on_runAsScript_eof()));
774 return p;
777 void Git::on_runAsScript_eof() {
779 QDir dir(workDir);
780 dir.remove("qgit_script" + QGit::SCRIPT_EXT);
783 void Git::cancelProcess(MyProcess* p) {
785 if (p)
786 p->on_cancel(); // non blocking call
789 int Git::findFileIndex(const RevFile& rf, SCRef name) {
791 if (name.isEmpty())
792 return -1;
794 int idx = name.lastIndexOf('/') + 1;
795 SCRef dr = name.left(idx);
796 SCRef nm = name.mid(idx);
798 for (uint i = 0, cnt = rf.count(); i < cnt; ++i) {
799 if (fileNamesVec[rf.nameAt(i)] == nm && dirNamesVec[rf.dirAt(i)] == dr)
800 return i;
802 return -1;
805 const QString Git::getLaneParent(SCRef fromSHA, int laneNum) {
807 const Rev* rs = revLookup(fromSHA);
808 if (!rs)
809 return "";
811 for (int idx = rs->orderIdx - 1; idx >= 0; idx--) {
813 const Rev* r = revLookup(revData->revOrder[idx]);
814 if (laneNum >= r->lanes.count())
815 return "";
817 if (!isFreeLane(r->lanes[laneNum])) {
819 int type = r->lanes[laneNum], parNum = 0;
820 while (!isMerge(type) && type != ACTIVE) {
822 if (isHead(type))
823 parNum++;
825 type = r->lanes[--laneNum];
827 return r->parent(parNum);
830 return "";
833 const QStringList Git::getChilds(SCRef parent) {
835 QStringList childs;
836 const Rev* r = revLookup(parent);
837 if (!r)
838 return childs;
840 for (int i = 0; i < r->childs.count(); i++)
841 childs.append(revData->revOrder[r->childs[i]]);
843 // reorder childs by loading order
844 QStringList::iterator itC(childs.begin());
845 for ( ; itC != childs.end(); ++itC) {
846 const Rev* r = revLookup(*itC);
847 (*itC).prepend(QString("%1 ").arg(r->orderIdx, 6));
849 childs.sort();
850 for (itC = childs.begin(); itC != childs.end(); ++itC)
851 (*itC) = (*itC).section(' ', -1, -1);
853 return childs;
856 const QString Git::getShortLog(SCRef sha) {
858 const Rev* r = revLookup(sha);
859 return (r ? r->shortLog() : "");
862 MyProcess* Git::getDiff(SCRef sha, QObject* receiver, SCRef diffToSha, bool combined) {
864 if (sha.isEmpty())
865 return NULL;
867 QString runCmd;
868 if (sha != ZERO_SHA) {
869 runCmd = "git diff-tree --no-color -r --patch-with-stat ";
870 runCmd.append(combined ? "-c " : "-C -m "); // TODO rename for combined
871 runCmd.append(diffToSha + " " + sha); // diffToSha could be empty
872 } else
873 runCmd = "git diff-index --no-color -r -m --patch-with-stat HEAD";
875 return runAsync(runCmd, receiver);
878 const QString Git::getWorkDirDiff(SCRef fileName) {
880 QString runCmd("git diff-index --no-color -r -z -m -p --full-index --no-commit-id HEAD"), runOutput;
881 if (!fileName.isEmpty())
882 runCmd.append(" -- " + quote(fileName));
884 if (!run(runCmd, &runOutput))
885 return "";
887 /* For unknown reasons file sha of index is not ZERO_SHA but
888 a value of unknown origin.
889 Replace that with ZERO_SHA so to not fool annotate
891 int idx = runOutput.indexOf("..");
892 if (idx != -1)
893 runOutput.replace(idx + 2, 40, ZERO_SHA);
895 return runOutput;
898 const QString Git::getFileSha(SCRef file, SCRef revSha) {
900 if (revSha == ZERO_SHA) {
901 QStringList files, dummy;
902 getWorkDirFiles(files, dummy, RevFile::ANY);
903 if (files.contains(file))
904 return ZERO_SHA; // it is unknown to git
906 const QString sha(revSha == ZERO_SHA ? "HEAD" : revSha);
907 QString runCmd("git ls-tree -r " + sha + " " + quote(file)), runOutput;
908 if (!run(runCmd, &runOutput))
909 return "";
911 return runOutput.mid(12, 40); // could be empty, deleted file case
914 MyProcess* Git::getFile(SCRef fileSha, QObject* receiver, QByteArray* result, SCRef fileName) {
916 QString runCmd;
918 symlinks in git are one line files with just the name of the target,
919 not the target content. Instead 'cat' command resolves symlinks and
920 returns target content. So we use 'cat' only if the file is modified
921 in working dir, to let annotation work for changed files, otherwise
922 we go with a safe 'git cat-file blob HEAD' instead.
923 NOTE: This fails if the modified file is a new symlink, converted
924 from an old plain file. In this case annotation will fail until
925 change is committed.
927 if (fileSha == ZERO_SHA)
929 #ifdef Q_OS_WIN32
931 QString winPath = quote(fileName);
932 winPath.replace("/", "\\");
933 runCmd = "type " + winPath;
935 #else
936 runCmd = "cat " + quote(fileName);
937 #endif
939 else {
940 if (fileSha.isEmpty()) // deleted
941 runCmd = "git diff-tree HEAD HEAD"; // fake an empty file reading
942 else
943 runCmd = "git cat-file blob " + fileSha;
945 if (!receiver) {
946 run(result, runCmd);
947 return NULL; // in case of sync call we ignore run() return value
949 return runAsync(runCmd, receiver);
952 MyProcess* Git::getHighlightedFile(SCRef fileSha, QObject* receiver, QString* result, SCRef fileName) {
954 if (!isTextHighlighter()) {
955 dbs("ASSERT in getHighlightedFile: highlighter not found");
956 return NULL;
958 QString ext(fileName.section('.', -1, -1, QString::SectionIncludeLeadingSep));
959 QString inputFile(workDir + "/qgit_hlght_input" + ext);
960 if (!saveFile(fileSha, fileName, inputFile))
961 return NULL;
963 QString runCmd("source-highlight --failsafe -f html -i " + quote(inputFile));
965 if (!receiver) {
966 run(runCmd, result);
967 on_getHighlightedFile_eof();
968 return NULL; // in case of sync call we ignore run() return value
970 MyProcess* p = runAsync(runCmd, receiver);
971 if (p)
972 connect(p, SIGNAL(eof()), this, SLOT(on_getHighlightedFile_eof()));
973 return p;
976 void Git::on_getHighlightedFile_eof() {
978 QDir dir(workDir);
979 const QStringList sl(dir.entryList(QStringList() << "qgit_hlght_input*"));
980 FOREACH_SL (it, sl)
981 dir.remove(*it);
984 bool Git::saveFile(SCRef fileSha, SCRef fileName, SCRef path) {
986 QByteArray fileData;
987 getFile(fileSha, NULL, &fileData, fileName); // sync call
988 if (isBinaryFile(fileName))
989 return writeToFile(path, fileData);
991 return writeToFile(path, QString(fileData));
994 bool Git::getTree(SCRef treeSha, TreeInfo& ti, bool isWorkingDir, SCRef path) {
996 QStringList deleted;
997 if (isWorkingDir) {
999 // retrieve unknown and deleted files under path
1000 QStringList unknowns, dummy;
1001 getWorkDirFiles(unknowns, dummy, RevFile::UNKNOWN);
1003 FOREACH_SL (it, unknowns) {
1005 // don't add files under other directories
1006 QFileInfo f(*it);
1007 SCRef d(f.dir().path());
1009 if (d == path || (path.isEmpty() && d == ".")) {
1010 TreeEntry te(f.fileName(), "", "?");
1011 ti.append(te);
1014 getWorkDirFiles(deleted, dummy, RevFile::DELETED);
1016 // if needed fake a working directory tree starting from HEAD tree
1017 QString runOutput, tree(treeSha);
1018 if (treeSha == ZERO_SHA) {
1019 // HEAD could be empty for just init'ed repositories
1020 if (!run("git rev-parse --revs-only HEAD", &tree))
1021 return false;
1023 tree = tree.trimmed();
1025 if (!tree.isEmpty() && !run("git ls-tree " + tree, &runOutput))
1026 return false;
1028 const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts));
1029 FOREACH_SL (it, sl) {
1031 // append any not deleted file
1032 SCRef fn((*it).section('\t', 1, 1));
1033 SCRef fp(path.isEmpty() ? fn : path + '/' + fn);
1035 if (deleted.empty() || (deleted.indexOf(fp) == -1)) {
1036 TreeEntry te(fn, (*it).mid(12, 40), (*it).mid(7, 4));
1037 ti.append(te);
1040 qSort(ti); // list directories before files
1041 return true;
1044 void Git::getWorkDirFiles(SList files, SList dirs, RevFile::StatusFlag status) {
1046 files.clear();
1047 dirs.clear();
1048 const RevFile* f = getFiles(ZERO_SHA);
1049 if (!f)
1050 return;
1052 for (int i = 0; i < f->count(); i++) {
1054 if (f->statusCmp(i, status)) {
1056 SCRef fp(filePath(*f, i));
1057 files.append(fp);
1058 for (int j = 0, cnt = fp.count('/'); j < cnt; j++) {
1060 SCRef dir(fp.section('/', 0, j));
1061 if (dirs.indexOf(dir) == -1)
1062 dirs.append(dir);
1068 bool Git::isNothingToCommit() {
1070 if (!revsFiles.contains(ZERO_SHA_RAW))
1071 return true;
1073 const RevFile* rf = revsFiles[ZERO_SHA_RAW];
1074 return (rf->count() == workingDirInfo.otherFiles.count());
1077 bool Git::isTreeModified(SCRef sha) {
1079 const RevFile* f = getFiles(sha);
1080 if (!f)
1081 return true; // no files info, stay on the safe side
1083 for (int i = 0; i < f->count(); ++i)
1084 if (!f->statusCmp(i, RevFile::MODIFIED))
1085 return true;
1087 return false;
1090 bool Git::isParentOf(SCRef par, SCRef child) {
1092 const Rev* c = revLookup(child);
1093 return (c && c->parentsCount() == 1 && QString(c->parent(0)) == par); // no merges
1096 bool Git::isSameFiles(SCRef tree1Sha, SCRef tree2Sha) {
1098 // early skip common case of browsing with up and down arrows, i.e.
1099 // going from parent(child) to child(parent). In this case we can
1100 // check RevFileMap and skip a costly 'git diff-tree' call.
1101 if (isParentOf(tree1Sha, tree2Sha))
1102 return !isTreeModified(tree2Sha);
1104 if (isParentOf(tree2Sha, tree1Sha))
1105 return !isTreeModified(tree1Sha);
1107 const QString runCmd("git diff-tree --no-color -r " + tree1Sha + " " + tree2Sha);
1108 QString runOutput;
1109 if (!run(runCmd, &runOutput))
1110 return false;
1112 bool isChanged = (runOutput.indexOf(" A\t") != -1 || runOutput.indexOf(" D\t") != -1);
1113 return !isChanged;
1116 const QStringList Git::getDescendantBranches(SCRef sha, bool shaOnly) {
1118 QStringList tl;
1119 const Rev* r = revLookup(sha);
1120 if (!r || (r->descBrnMaster == -1))
1121 return tl;
1123 const QVector<int>& nr = revLookup(revData->revOrder[r->descBrnMaster])->descBranches;
1125 for (int i = 0; i < nr.count(); i++) {
1127 const ShaString& sha = revData->revOrder[nr[i]];
1128 if (shaOnly) {
1129 tl.append(sha);
1130 continue;
1132 SCRef cap = " (" + sha + ") ";
1133 RefMap::const_iterator it(refsShaMap.find(sha));
1134 if (it == refsShaMap.constEnd())
1135 continue;
1137 if (!(*it).branches.empty())
1138 tl.append((*it).branches.join(" ").append(cap));
1140 if (!(*it).remoteBranches.empty())
1141 tl.append((*it).remoteBranches.join(" ").append(cap));
1143 return tl;
1146 const QStringList Git::getNearTags(bool goDown, SCRef sha) {
1148 QStringList tl;
1149 const Rev* r = revLookup(sha);
1150 if (!r)
1151 return tl;
1153 int nearRefsMaster = (goDown ? r->descRefsMaster : r->ancRefsMaster);
1154 if (nearRefsMaster == -1)
1155 return tl;
1157 const QVector<int>& nr = goDown ? revLookup(revData->revOrder[nearRefsMaster])->descRefs :
1158 revLookup(revData->revOrder[nearRefsMaster])->ancRefs;
1160 for (int i = 0; i < nr.count(); i++) {
1162 const ShaString& sha = revData->revOrder[nr[i]];
1163 SCRef cap = " (" + sha + ")";
1164 RefMap::const_iterator it(refsShaMap.find(sha));
1165 if (it != refsShaMap.constEnd())
1166 tl.append((*it).tags.join(cap).append(cap));
1168 return tl;
1171 const QString Git::getLastCommitMsg() {
1173 // FIXME: Make sure the amend action is not called when there is
1174 // nothing to amend. That is in empty repository or over StGit stack
1175 // with nothing applied.
1176 QString sha;
1177 QString top;
1178 if (run("git rev-parse --verify HEAD", &top))
1179 sha = top.trimmed();
1180 else {
1181 dbs("ASSERT: getLastCommitMsg head is not valid");
1182 return "";
1185 const Rev* c = revLookup(sha);
1186 if (!c) {
1187 dbp("ASSERT: getLastCommitMsg sha <%1> not found", sha);
1188 return "";
1191 return c->shortLog() + "\n\n" + c->longLog().trimmed();
1194 const QString Git::getNewCommitMsg() {
1196 const Rev* c = revLookup(ZERO_SHA);
1197 if (!c) {
1198 dbs("ASSERT: getNewCommitMsg zero_sha not found");
1199 return "";
1202 QString status = c->longLog();
1203 status.prepend('\n').replace(QRegExp("\\n([^#])"), "\n#\\1"); // comment all the lines
1204 return status;
1207 const QString Git::colorMatch(SCRef txt, QRegExp& regExp) {
1209 QString text;
1211 text = Qt::escape(txt);
1213 if (regExp.isEmpty())
1214 return text;
1216 SCRef startCol(QString::fromLatin1("<b><font color=\"red\">"));
1217 SCRef endCol(QString::fromLatin1("</font></b>"));
1218 int pos = 0;
1219 while ((pos = text.indexOf(regExp, pos)) != -1) {
1221 SCRef match(regExp.cap(0));
1222 const QString coloredText(startCol + match + endCol);
1223 text.replace(pos, match.length(), coloredText);
1224 pos += coloredText.length();
1226 return text;
1229 const QString Git::formatList(SCList sl, SCRef name, bool inOneLine) {
1231 if (sl.isEmpty())
1232 return QString();
1234 QString ls = "<tr><td class='h'>" + name + "</td><td>";
1235 const QString joinStr = inOneLine ? ", " : "</td></tr>\n" + ls;
1236 ls += sl.join(joinStr);
1237 ls += "</td></tr>\n";
1238 return ls;
1241 const QString Git::getDesc(SCRef sha, QRegExp& shortLogRE, QRegExp& longLogRE,
1242 bool showHeader, FileHistory* fh) {
1244 if (sha.isEmpty())
1245 return "";
1247 const Rev* c = revLookup(sha, fh);
1248 if (!c) // sha of a not loaded revision, as
1249 return ""; // example asked from file history
1251 QString text;
1252 if (c->isDiffCache)
1253 text = Qt::convertFromPlainText(c->longLog());
1254 else {
1255 QTextStream ts(&text);
1256 ts << "<html><head><style type=\"text/css\">"
1257 "tr.head { background-color: #a0a0e0 }\n"
1258 "td.h { font-weight: bold; }\n"
1259 "table { background-color: #e0e0f0; }\n"
1260 "span.h { font-weight: bold; font-size: medium; }\n"
1261 "div.l { white-space: pre; "
1262 "font-family: " << TYPE_WRITER_FONT.family() << ";"
1263 "font-size: " << TYPE_WRITER_FONT.pointSize() << "pt;}\n"
1264 "</style></head><body><div class='t'>\n"
1265 "<table border=0 cellspacing=0 cellpadding=2>";
1267 ts << "<tr class='head'> <th></th> <th><span class='h'>"
1268 << colorMatch(c->shortLog(), shortLogRE)
1269 << "</span></th></tr>";
1271 if (showHeader) {
1272 if (c->committer() != c->author())
1273 ts << formatList(QStringList(Qt::escape(c->committer())), "Committer");
1274 ts << formatList(QStringList(Qt::escape(c->author())), "Author");
1275 ts << formatList(QStringList(getLocalDate(c->authorDate())), " Author date");
1277 if (c->isUnApplied || c->isApplied) {
1279 QStringList patches(getRefName(sha, APPLIED));
1280 patches += getRefName(sha, UN_APPLIED);
1281 ts << formatList(patches, "Patch");
1282 } else {
1283 ts << formatList(c->parents(), "Parent", false);
1284 ts << formatList(getChilds(sha), "Child", false);
1285 ts << formatList(getDescendantBranches(sha), "Branch", false);
1286 ts << formatList(getNearTags(!optGoDown, sha), "Follows");
1287 ts << formatList(getNearTags(optGoDown, sha), "Precedes");
1290 QString longLog(c->longLog());
1291 if (showHeader) {
1292 longLog.prepend(QString("\n") + c->shortLog() + "\n");
1295 QString log(colorMatch(longLog, longLogRE));
1296 log.replace("\n", "\n ").prepend('\n');
1297 ts << "</table></div><div class='l'>" << log << "</div></body></html>";
1299 // highlight SHA's
1301 // added to commit logs, we avoid to call git rev-parse for a possible abbreviated
1302 // sha if there isn't a leading trailing space or an open parenthesis and,
1303 // in that case, before the space must not be a ':' character.
1304 // It's an ugly heuristic, but seems to work in most cases.
1305 QRegExp reSHA("..[0-9a-f]{21,40}|[^:][\\s(][0-9a-f]{6,20}", Qt::CaseInsensitive);
1306 reSHA.setMinimal(false);
1307 int pos = 0;
1308 while ((pos = text.indexOf(reSHA, pos)) != -1) {
1310 SCRef ref = reSHA.cap(0).mid(2);
1311 const Rev* r = (ref.length() == 40 ? revLookup(ref) : revLookup(getRefSha(ref)));
1312 if (r && r->sha() != ZERO_SHA_RAW) {
1313 QString slog(r->shortLog());
1314 if (slog.isEmpty()) // very rare but possible
1315 slog = r->sha();
1316 if (slog.length() > 60)
1317 slog = slog.left(57).trimmed().append("...");
1319 slog = Qt::escape(slog);
1320 const QString link("<a href=\"" + r->sha() + "\">" + slog + "</a>");
1321 text.replace(pos + 2, ref.length(), link);
1322 pos += link.length();
1323 } else
1324 pos += reSHA.cap(0).length();
1326 return text;
1329 const RevFile* Git::insertNewFiles(SCRef sha, SCRef data) {
1331 /* we use an independent FileNamesLoader to avoid data
1332 * corruption if we are loading file names in background
1334 FileNamesLoader fl;
1336 RevFile* rf = new RevFile();
1337 parseDiffFormat(*rf, data, fl);
1338 flushFileNames(fl);
1340 revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf);
1341 return rf;
1344 bool Git::runDiffTreeWithRenameDetection(SCRef runCmd, QString* runOutput) {
1345 /* Under some cases git could warn out:
1347 "too many files, skipping inexact rename detection"
1349 So if this occurs fallback on NO rename detection.
1351 QString cmd(runCmd); // runCmd must be without -C option
1352 cmd.replace("git diff-tree", "git diff-tree -C");
1354 errorReportingEnabled = false;
1355 bool renameDetectionOk = run(cmd, runOutput);
1356 errorReportingEnabled = true;
1358 if (!renameDetectionOk) // retry without rename detection
1359 return run(runCmd, runOutput);
1361 return true;
1364 const RevFile* Git::getAllMergeFiles(const Rev* r) {
1366 SCRef mySha(ALL_MERGE_FILES + r->sha());
1367 if (revsFiles.contains(toTempSha(mySha)))
1368 return revsFiles[toTempSha(mySha)];
1370 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1372 QString runCmd("git diff-tree --no-color -r -m " + r->sha()), runOutput;
1373 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1374 return NULL;
1376 return insertNewFiles(mySha, runOutput);
1379 const RevFile* Git::getFiles(SCRef sha, SCRef diffToSha, bool allFiles, SCRef path) {
1381 const Rev* r = revLookup(sha);
1382 if (!r)
1383 return NULL;
1385 if (r->parentsCount() == 0) // skip initial rev
1386 return NULL;
1388 if (r->parentsCount() > 1 && diffToSha.isEmpty() && allFiles)
1389 return getAllMergeFiles(r);
1391 if (!diffToSha.isEmpty() && (sha != ZERO_SHA)) {
1393 QString runCmd("git diff-tree --no-color -r -m ");
1394 runCmd.append(diffToSha + " " + sha);
1395 if (!path.isEmpty())
1396 runCmd.append(" " + path);
1398 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1400 QString runOutput;
1401 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1402 return NULL;
1404 // we insert a dummy revision file object. It will be
1405 // overwritten at each request but we don't care.
1406 return insertNewFiles(CUSTOM_SHA, runOutput);
1408 if (revsFiles.contains(r->sha()))
1409 return revsFiles[r->sha()]; // ZERO_SHA search arrives here
1411 if (sha == ZERO_SHA) {
1412 dbs("ASSERT in Git::getFiles, ZERO_SHA not found");
1413 return NULL;
1416 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1418 QString runCmd("git diff-tree --no-color -r -c " + sha), runOutput;
1419 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1420 return NULL;
1422 if (revsFiles.contains(r->sha())) // has been created in the mean time?
1423 return revsFiles[r->sha()];
1425 cacheNeedsUpdate = true;
1426 return insertNewFiles(sha, runOutput);
1429 bool Git::startFileHistory(SCRef sha, SCRef startingFileName, FileHistory* fh) {
1431 QStringList args(getDescendantBranches(sha, true));
1432 if (args.isEmpty())
1433 args << "HEAD";
1435 QString newestFileName = getNewestFileName(args, startingFileName);
1436 fh->resetFileNames(newestFileName);
1438 args.clear(); // load history from all the branches
1439 args << getAllRefSha(BRANCH | RMT_BRANCH);
1441 args << "--" << newestFileName;
1442 return startRevList(args, fh);
1445 const QString Git::getNewestFileName(SCList branches, SCRef fileName) {
1447 QString curFileName(fileName), runOutput, args;
1448 while (true) {
1449 args = branches.join(" ") + " -- " + curFileName;
1450 if (!run("git ls-tree " + args, &runOutput))
1451 break;
1453 if (!runOutput.isEmpty())
1454 break;
1456 QString msg("Retrieving file renames, now at '" + curFileName + "'...");
1457 QApplication::postEvent(parent(), new MessageEvent(msg));
1458 EM_PROCESS_EVENTS_NO_INPUT;
1460 if (!run("git rev-list -n1 " + args, &runOutput))
1461 break;
1463 if (runOutput.isEmpty()) // try harder
1464 if (!run("git rev-list --full-history -n1 " + args, &runOutput))
1465 break;
1467 if (runOutput.isEmpty())
1468 break;
1470 SCRef sha = runOutput.trimmed();
1471 QStringList newCur;
1472 if (!populateRenamedPatches(sha, QStringList(curFileName), NULL, &newCur, true))
1473 break;
1475 curFileName = newCur.first();
1477 return curFileName;
1480 void Git::getFileFilter(SCRef path, ShaSet& shaSet) const {
1482 shaSet.clear();
1483 QRegExp rx(path, Qt::CaseInsensitive, QRegExp::Wildcard);
1484 FOREACH (ShaVect, it, revData->revOrder) {
1486 if (!revsFiles.contains(*it))
1487 continue;
1489 // case insensitive, wildcard search
1490 const RevFile* rf = revsFiles[*it];
1491 for (int i = 0; i < rf->count(); ++i)
1492 if (filePath(*rf, i).contains(rx)) {
1493 shaSet.insert(*it);
1494 break;
1499 bool Git::getPatchFilter(SCRef exp, bool isRegExp, ShaSet& shaSet) {
1501 shaSet.clear();
1502 QString buf;
1503 FOREACH (ShaVect, it, revData->revOrder)
1504 if (*it != ZERO_SHA_RAW)
1505 buf.append(*it).append('\n');
1507 if (buf.isEmpty())
1508 return true;
1510 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1512 QString runCmd("git diff-tree --no-color -r -s --stdin "), runOutput;
1513 if (isRegExp)
1514 runCmd.append("--pickaxe-regex ");
1516 runCmd.append(quote("-S" + exp));
1517 if (!run(runCmd, &runOutput, NULL, buf))
1518 return false;
1520 const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts));
1521 FOREACH_SL (it, sl)
1522 shaSet.insert(*it);
1524 return true;
1527 bool Git::resetCommits(int parentDepth) {
1529 QString runCmd("git reset --soft HEAD~");
1530 runCmd.append(QString::number(parentDepth));
1531 return run(runCmd);
1534 bool Git::applyPatchFile(SCRef patchPath, bool fold, bool isDragDrop) {
1536 if (isStGIT) {
1537 if (fold) {
1538 bool ok = run("stg fold " + quote(patchPath)); // merge in working dir
1539 if (ok)
1540 ok = run("stg refresh"); // update top patch
1541 return ok;
1542 } else
1543 return run("stg import --mail " + quote(patchPath));
1545 QString runCmd("git am --utf8 --3way ");
1547 QSettings settings;
1548 const QString APOpt(settings.value(AM_P_OPT_KEY).toString());
1549 if (!APOpt.isEmpty())
1550 runCmd.append(APOpt.trimmed() + " ");
1552 if (isDragDrop)
1553 runCmd.append("--keep ");
1555 if (testFlag(SIGN_PATCH_F))
1556 runCmd.append("--signoff ");
1558 return run(runCmd + quote(patchPath));
1561 const QStringList Git::sortShaListByIndex(SCList shaList) {
1563 QStringList orderedShaList;
1564 FOREACH_SL (it, shaList)
1565 appendNamesWithId(orderedShaList, *it, QStringList(*it), true);
1567 orderedShaList.sort();
1568 QStringList::iterator itN(orderedShaList.begin());
1569 for ( ; itN != orderedShaList.end(); ++itN) // strip 'idx'
1570 (*itN) = (*itN).section(' ', -1, -1);
1572 return orderedShaList;
1575 bool Git::formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir) {
1577 bool remote = !remoteDir.isEmpty();
1578 QSettings settings;
1579 const QString FPOpt(settings.value(FMT_P_OPT_KEY).toString());
1581 QString runCmd("git format-patch --no-color");
1582 if (testFlag(NUMBERS_F) && !remote)
1583 runCmd.append(" -n");
1585 if (remote)
1586 runCmd.append(" --keep-subject");
1588 runCmd.append(" -o " + quote(dirPath));
1589 if (!FPOpt.isEmpty())
1590 runCmd.append(" " + FPOpt.trimmed());
1592 const QString tmp(workDir);
1593 if (remote)
1594 workDir = remoteDir; // run() uses workDir value
1596 // shaList is ordered by newest to oldest
1597 runCmd.append(" " + shaList.last());
1598 runCmd.append(QString::fromLatin1("^..") + shaList.first());
1599 bool ret = run(runCmd);
1600 workDir = tmp;
1601 return ret;
1604 const QStringList Git::getOtherFiles(SCList selFiles, bool onlyInIndex) {
1606 const RevFile* files = getFiles(ZERO_SHA); // files != NULL
1607 QStringList notSelFiles;
1608 for (int i = 0; i < files->count(); ++i) {
1609 SCRef fp = filePath(*files, i);
1610 if (selFiles.indexOf(fp) == -1) { // not selected...
1611 if (!onlyInIndex || files->statusCmp(i, RevFile::IN_INDEX))
1612 notSelFiles.append(fp);
1615 return notSelFiles;
1618 bool Git::updateIndex(SCList selFiles) {
1620 const RevFile* files = getFiles(ZERO_SHA); // files != NULL
1622 QStringList toAdd, toRemove;
1623 FOREACH_SL (it, selFiles) {
1624 int idx = findFileIndex(*files, *it);
1625 if (files->statusCmp(idx, RevFile::DELETED))
1626 toRemove << *it;
1627 else
1628 toAdd << *it;
1630 if (!toRemove.isEmpty() && !run("git rm --cached --ignore-unmatch -- " + quote(toRemove)))
1631 return false;
1633 if (!toAdd.isEmpty() && !run("git add -- " + quote(toAdd)))
1634 return false;
1636 return true;
1639 bool Git::commitFiles(SCList selFiles, SCRef msg, bool amend) {
1641 const QString msgFile(gitDir + "/qgit_cmt_msg.txt");
1642 if (!writeToFile(msgFile, msg)) // early skip
1643 return false;
1645 // add user selectable commit options
1646 QSettings settings;
1647 const QString CMArgs(settings.value(CMT_ARGS_KEY).toString());
1649 QString cmtOptions;
1650 if (!CMArgs.isEmpty())
1651 cmtOptions.append(" " + CMArgs);
1653 if (testFlag(SIGN_CMT_F))
1654 cmtOptions.append(" -s");
1656 if (testFlag(VERIFY_CMT_F))
1657 cmtOptions.append(" -v");
1659 if (amend)
1660 cmtOptions.append(" --amend");
1662 bool ret = false;
1664 // get not selected files but updated in index to restore at the end
1665 const QStringList notSel(getOtherFiles(selFiles, optOnlyInIndex));
1667 // call git reset to remove not selected files from index
1668 if (!notSel.empty() && !run("git reset -- " + quote(notSel)))
1669 goto fail;
1671 // update index with selected files
1672 if (!updateIndex(selFiles))
1673 goto fail;
1675 // now we can finally commit..
1676 if (!run("git commit" + cmtOptions + " -F " + quote(msgFile)))
1677 goto fail;
1679 // restore not selected files that were already in index
1680 if (!notSel.empty() && !updateIndex(notSel))
1681 goto fail;
1683 ret = true;
1684 fail:
1685 QDir dir(workDir);
1686 dir.remove(msgFile);
1687 return ret;
1690 bool Git::mkPatchFromWorkDir(SCRef msg, SCRef patchFile, SCList files) {
1692 /* unfortunately 'git diff' sees only files already
1693 * known to git or already in index, so update index first
1694 * to be sure also unknown files are correctly found
1696 if (!updateIndex(files))
1697 return false;
1699 QString runOutput;
1700 if (!run("git diff -C HEAD -- " + quote(files), &runOutput))
1701 return false;
1703 const QString patch("Subject: " + msg + "\n---\n" + runOutput);
1704 return writeToFile(patchFile, patch);
1707 bool Git::stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold) {
1709 /* Here the deal is to use 'stg import' and 'stg fold' to add a new
1710 * patch or refresh the current one respectively. Unfortunately refresh
1711 * does not work with partial selection of files and also does not take
1712 * patch message from a file that is needed to avoid artifacts with '\n'
1713 * and friends.
1715 * So steps are:
1717 * - Create a patch file with the changes you want to import/fold in StGit
1718 * - Stash working dir files because import/fold wants a clean directory
1719 * - Import/fold the patch
1720 * - Unstash and merge working dir modified files
1721 * - Restore index with not selected files
1724 /* Step 1: Create a patch file with the changes you want to import/fold */
1725 bool ret = false;
1726 const QString patchFile(gitDir + "/qgit_tmp_patch.txt");
1728 // in case we don't have files to restore we can shortcut various commands
1729 bool partialSelection = !getOtherFiles(selFiles, !optOnlyInIndex).isEmpty();
1731 // get not selected files but updated in index to restore at the end
1732 QStringList notSel;
1733 if (partialSelection) // otherwise notSel is for sure empty
1734 notSel = getOtherFiles(selFiles, optOnlyInIndex);
1736 // create a patch with diffs between working dir and HEAD
1737 if (!mkPatchFromWorkDir(msg, patchFile, selFiles))
1738 goto fail;
1740 /* Step 2: Stash working dir modified files */
1741 if (partialSelection) {
1742 errorReportingEnabled = false;
1743 run("git stash"); // unfortunately 'git stash' is noisy on stderr
1744 errorReportingEnabled = true;
1747 /* Step 3: Call stg import/fold */
1749 // setup a clean state
1750 if (!run("stg status --reset"))
1751 goto fail_and_unstash;
1753 if (fold) {
1754 // update patch message before to fold, note that
1755 // command 'stg edit' requires stg version 0.14 or later
1756 if (!msg.isEmpty() && !run("stg edit --message " + quote(msg.trimmed())))
1757 goto fail_and_unstash;
1759 if (!run("stg fold " + quote(patchFile)))
1760 goto fail_and_unstash;
1762 if (!run("stg refresh")) // refresh needed after fold
1763 goto fail_and_unstash;
1765 } else if (!run("stg import --mail --name " + quote(patchName) + " " + quote(patchFile)))
1766 goto fail_and_unstash;
1768 if (partialSelection) {
1770 /* Step 4: Unstash and merge working dir modified files */
1771 errorReportingEnabled = false;
1772 run("git stash pop"); // unfortunately 'git stash' is noisy on stderr
1773 errorReportingEnabled = true;
1775 /* Step 5: restore not selected files that were already in index */
1776 if (!notSel.empty() && !updateIndex(notSel))
1777 goto fail;
1780 ret = true;
1781 goto exit;
1783 fail_and_unstash:
1785 if (partialSelection) {
1786 run("git reset");
1787 errorReportingEnabled = false;
1788 run("git stash pop");
1789 errorReportingEnabled = true;
1791 fail:
1792 exit:
1793 QDir dir(workDir);
1794 dir.remove(patchFile);
1795 return ret;
1798 bool Git::makeBranch(SCRef sha, SCRef branchName) {
1800 return run("git branch " + branchName + " " + sha);
1803 bool Git::makeTag(SCRef sha, SCRef tagName, SCRef msg) {
1805 if (msg.isEmpty())
1806 return run("git tag " + tagName + " " + sha);
1808 return run("git tag -m \"" + msg + "\" " + tagName + " " + sha);
1811 bool Git::deleteTag(SCRef sha) {
1813 const QStringList tags(getRefName(sha, TAG));
1814 if (!tags.empty())
1815 return run("git tag -d " + tags.first()); // only one
1817 return false;
1820 bool Git::stgPush(SCRef sha) {
1822 const QStringList patch(getRefName(sha, UN_APPLIED));
1823 if (patch.count() != 1) {
1824 dbp("ASSERT in Git::stgPush, found %1 patches instead of 1", patch.count());
1825 return false;
1827 return run("stg push " + quote(patch.first()));
1830 bool Git::stgPop(SCRef sha) {
1832 const QStringList patch(getRefName(sha, APPLIED));
1833 if (patch.count() != 1) {
1834 dbp("ASSERT in Git::stgPop, found %1 patches instead of 1", patch.count());
1835 return false;
1837 return run("stg pop " + quote(patch));