Version 2.6
[qgit4/redivivus.git] / src / git.cpp
blobe663b7cfb7739d8e53fe1307ea527a9ad77466d5
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 <QDir>
11 #include <QFile>
12 #include <QImageReader>
13 #include <QPalette>
14 #include <QRegExp>
15 //#include <QSet> //CT TODO remove
16 #include <QSettings>
17 #include <QTextCodec>
18 #include <QTextDocument>
19 #include <QTextStream>
20 #include "FileHistory.h"
21 #include "annotate.h"
22 #include "cache.h"
23 #include "dataloader.h"
24 #include "git.h"
25 #include "lanes.h"
26 #include "myprocess.h"
27 #include "rangeselectimpl.h"
29 #define SHOW_MSG(x) QApplication::postEvent(parent(), new MessageEvent(x)); EM_PROCESS_EVENTS_NO_INPUT;
31 // Used on init() for reading parameters once;
32 // It's OK to be unique among qgit windows.
33 static bool startup = true;
35 using namespace QGit;
38 // ****************************************************************************
40 bool Git::TreeEntry::operator<(const TreeEntry& te) const {
42 if (this->type == te.type)
43 return( this->name.localeAwareCompare( te.name ) < 0 );
45 // directories are smaller then files
46 // to appear as first when sorted
47 if (this->type == "tree")
48 return true;
50 if (te.type == "tree")
51 return false;
53 return( this->name.localeAwareCompare( te.name ) < 0 );
56 Git::Git(QObject* p) : QObject(p) {
58 EM_INIT(exGitStopped, "Stopping connection with git");
60 fileCacheAccessed = cacheNeedsUpdate = isMergeHead = false;
61 isStGIT = isGIT = loadingUnAppliedPatches = isTextHighlighterFound = false;
62 errorReportingEnabled = true; // report errors if run() fails
63 curDomain = NULL;
64 revData = NULL;
65 revsFiles.reserve(MAX_DICT_SIZE);
68 void Git::checkEnvironment() {
70 QString version;
71 if (run("git --version", &version)) {
73 version = version.section(' ', -1, -1).section('.', 0, 2);
74 if (version < GIT_VERSION) {
76 // simply send information, the 'not compatible version'
77 // policy should be implemented upstream
78 const QString cmd("Current git version is " + version +
79 " but is required " + GIT_VERSION + " or better");
81 const QString errorDesc("Your installed git is too old."
82 "\nPlease upgrade to avoid possible misbehaviours.");
84 MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc);
85 QApplication::postEvent(parent(), e);
87 } else {
88 dbs("Cannot find git files");
89 return;
91 errorReportingEnabled = false;
92 isTextHighlighterFound = run("source-highlight -V", &version);
93 errorReportingEnabled = true;
94 if (isTextHighlighterFound)
95 textHighlighterVersionFound = version.section('\n', 0, 0);
96 else
97 textHighlighterVersionFound = "GNU source-highlight not installed";
100 void Git::userInfo(SList info) {
102 git looks for commit user information in following order:
104 - GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL environment variables
105 - repository config file
106 - global config file
107 - your name, hostname and domain
109 const QString env(QProcess::systemEnvironment().join(","));
110 QString user(env.section("GIT_AUTHOR_NAME", 1).section(",", 0, 0).section("=", 1).trimmed());
111 QString email(env.section("GIT_AUTHOR_EMAIL", 1).section(",", 0, 0).section("=", 1).trimmed());
113 info.clear();
114 info << "Environment" << user << email;
116 errorReportingEnabled = false; // 'git config' could fail, see docs
118 run("git config user.name", &user);
119 run("git config user.email", &email);
120 info << "Local config" << user << email;
122 run("git config --global user.name", &user);
123 run("git config --global user.email", &email);
124 info << "Global config" << user << email;
126 errorReportingEnabled = true;
129 const QStringList Git::getGitConfigList(bool global) {
131 QString runOutput;
133 errorReportingEnabled = false; // 'git config' could fail, see docs
135 if (global)
136 run("git config --global --list", &runOutput);
137 else
138 run("git config --list", &runOutput);
140 errorReportingEnabled = true;
142 return runOutput.split('\n', QString::SkipEmptyParts);
145 bool Git::isImageFile(SCRef file) {
147 const QString ext(file.section('.', -1).toLower());
148 return QImageReader::supportedImageFormats().contains(ext.toLatin1());
151 //CT TODO investigate if there is a better way of getting this (from git e.g.)
152 bool Git::isBinaryFile(SCRef file) {
154 static const char* binaryFileExtensions[] = {"bmp", "gif", "jpeg", "jpg",
155 "png", "svg", "tiff", "pcx", "xcf", "xpm",
156 "bz", "bz2", "rar", "tar", "z", "gz", "tgz", "zip", 0};
158 if (isImageFile(file))
159 return true;
161 const QString ext(file.section('.', -1).toLower());
162 int i = 0;
163 while (binaryFileExtensions[i] != 0)
164 if (ext == binaryFileExtensions[i++])
165 return true;
166 return false;
169 void Git::setThrowOnStop(bool b) {
171 if (b)
172 EM_REGISTER(exGitStopped);
173 else
174 EM_REMOVE(exGitStopped);
177 bool Git::isThrowOnStopRaised(int excpId, SCRef curContext) {
179 return EM_MATCH(excpId, exGitStopped, curContext);
182 void Git::setTextCodec(QTextCodec* tc) {
184 // QTextCodec::setCodecForCStrings(tc); // works also with tc == 0 (Latin1)
185 QTextCodec::setCodecForLocale(tc);
186 QString name(tc ? tc->name() : "Latin1");
188 // workaround Qt issue of mime name different from
189 // standard http://www.iana.org/assignments/character-sets
190 if (name == "Big5-HKSCS")
191 name = "Big5";
193 run("git config i18n.commitencoding " + name);
196 QTextCodec* Git::getTextCodec(bool* isGitArchive) {
198 *isGitArchive = isGIT;
199 if (!isGIT) // can be called also when not in an archive
200 return NULL;
202 QString runOutput;
203 if (!run("git config i18n.commitencoding", &runOutput))
204 return NULL;
206 if (runOutput.isEmpty()) // git docs says default is utf-8
207 return QTextCodec::codecForName(QByteArray("utf8"));
209 return QTextCodec::codecForName(runOutput.trimmed().toLatin1());
212 //CT TODO utility function; can go elsewhere
213 const QString Git::quote(SCRef nm) {
215 return (QUOTE_CHAR + nm + QUOTE_CHAR);
218 //CT TODO utility function; can go elsewhere
219 const QString Git::quote(SCList sl) {
221 QString q(sl.join(QUOTE_CHAR + ' ' + QUOTE_CHAR));
222 q.prepend(QUOTE_CHAR).append(QUOTE_CHAR);
223 return q;
226 uint Git::checkRef(const ShaString& sha, uint mask) const {
228 RefMap::const_iterator it(refsShaMap.constFind(sha));
229 return (it != refsShaMap.constEnd() ? (*it).type & mask : 0);
232 uint Git::checkRef(SCRef sha, uint mask) const {
234 RefMap::const_iterator it(refsShaMap.constFind(toTempSha(sha)));
235 return (it != refsShaMap.constEnd() ? (*it).type & mask : 0);
238 const QStringList Git::getRefName(SCRef sha, RefType type, QString* curBranch) const {
240 if (!checkRef(sha, type))
241 return QStringList();
243 const Reference& rf = refsShaMap[toTempSha(sha)];
245 if (curBranch)
246 *curBranch = rf.currentBranch;
248 if (type == TAG)
249 return rf.tags;
251 else if (type == BRANCH)
252 return rf.branches;
254 else if (type == RMT_BRANCH)
255 return rf.remoteBranches;
257 else if (type == REF)
258 return rf.refs;
260 else if (type == APPLIED || type == UN_APPLIED)
261 return QStringList(rf.stgitPatch);
263 return QStringList();
266 const QStringList Git::getAllRefSha(uint mask) {
268 QStringList shas;
269 FOREACH (RefMap, it, refsShaMap)
270 if ((*it).type & mask)
271 shas.append(it.key());
272 return shas;
275 const QString Git::getRefSha(SCRef refName, RefType type, bool askGit) {
277 bool any = (type == ANY_REF);
279 FOREACH (RefMap, it, refsShaMap) {
281 const Reference& rf = *it;
283 if ((any || type == TAG) && rf.tags.contains(refName))
284 return it.key();
286 else if ((any || type == BRANCH) && rf.branches.contains(refName))
287 return it.key();
289 else if ((any || type == RMT_BRANCH) && rf.remoteBranches.contains(refName))
290 return it.key();
292 else if ((any || type == REF) && rf.refs.contains(refName))
293 return it.key();
295 else if ((any || type == APPLIED || type == UN_APPLIED) && rf.stgitPatch == refName)
296 return it.key();
298 if (!askGit)
299 return "";
301 // if a ref was not found perhaps is an abbreviated form
302 QString runOutput;
303 errorReportingEnabled = false;
304 bool ok = run("git rev-parse --revs-only " + refName, &runOutput);
305 errorReportingEnabled = true;
306 return (ok ? runOutput.trimmed() : "");
309 void Git::appendNamesWithId(QStringList& names, SCRef sha, SCList data, bool onlyLoaded) {
311 const Rev* r = revLookup(sha);
312 if (onlyLoaded && !r)
313 return;
315 if (onlyLoaded) { // prepare for later sorting
316 SCRef cap = QString("%1 ").arg(r->orderIdx, 6);
317 FOREACH_SL (it, data)
318 names.append(cap + *it);
319 } else
320 names += data;
323 const QStringList Git::getAllRefNames(uint mask, bool onlyLoaded) {
324 // returns reference names sorted by loading order if 'onlyLoaded' is set
326 QStringList names;
327 FOREACH (RefMap, it, refsShaMap) {
329 if (mask & TAG)
330 appendNamesWithId(names, it.key(), (*it).tags, onlyLoaded);
332 if (mask & BRANCH)
333 appendNamesWithId(names, it.key(), (*it).branches, onlyLoaded);
335 if (mask & RMT_BRANCH)
336 appendNamesWithId(names, it.key(), (*it).remoteBranches, onlyLoaded);
338 if (mask & REF)
339 appendNamesWithId(names, it.key(), (*it).refs, onlyLoaded);
341 if ((mask & (APPLIED | UN_APPLIED)) && !onlyLoaded)
342 names.append((*it).stgitPatch); // doesn't work with 'onlyLoaded'
344 if (onlyLoaded) {
345 names.sort();
346 QStringList::iterator itN(names.begin());
347 for ( ; itN != names.end(); ++itN) // strip 'idx'
348 (*itN) = (*itN).section(' ', -1, -1);
350 return names;
353 const QString Git::getRevInfo(SCRef sha) {
355 if (sha.isEmpty())
356 return "";
358 uint type = checkRef(sha);
359 if (type == 0)
360 return "";
362 QString refsInfo;
363 if (type & BRANCH) {
364 const QString cap(type & CUR_BRANCH ? "HEAD: " : "Branch: ");
365 refsInfo = cap + getRefName(sha, BRANCH).join(" ");
367 if (type & RMT_BRANCH)
368 refsInfo.append(" Remote branch: " + getRefName(sha, RMT_BRANCH).join(" "));
370 if (type & TAG)
371 refsInfo.append(" Tag: " + getRefName(sha, TAG).join(" "));
373 if (type & REF)
374 refsInfo.append(" Ref: " + getRefName(sha, REF).join(" "));
376 if (type & APPLIED)
377 refsInfo.append(" Patch: " + getRefName(sha, APPLIED).join(" "));
379 if (type & UN_APPLIED)
380 refsInfo.append(" Patch: " + getRefName(sha, UN_APPLIED).join(" "));
382 if (type & TAG) {
383 SCRef msg(getTagMsg(sha));
384 if (!msg.isEmpty())
385 refsInfo.append(" [" + msg + "]");
387 return refsInfo.trimmed();
390 const QString Git::getTagMsg(SCRef sha) {
392 if (!checkRef(sha, TAG)) {
393 dbs("ASSERT in Git::getTagMsg, tag not found");
394 return "";
396 Reference& rf = refsShaMap[toTempSha(sha)];
398 if (!rf.tagMsg.isEmpty())
399 return rf.tagMsg;
401 QRegExp pgp("-----BEGIN PGP SIGNATURE*END PGP SIGNATURE-----",
402 Qt::CaseSensitive, QRegExp::Wildcard);
404 if (!rf.tagObj.isEmpty()) {
405 QString ro;
406 if (run("git cat-file tag " + rf.tagObj, &ro))
407 rf.tagMsg = ro.section("\n\n", 1).remove(pgp).trimmed();
409 return rf.tagMsg;
412 bool Git::isPatchName(SCRef nm) {
414 if (!getRefSha(nm, UN_APPLIED, false).isEmpty())
415 return true;
417 return !getRefSha(nm, APPLIED, false).isEmpty();
420 void Git::addExtraFileInfo(QString* rowName, SCRef sha, SCRef diffToSha, bool allMergeFiles) {
422 const RevFile* files = getFiles(sha, diffToSha, allMergeFiles);
423 if (!files)
424 return;
426 int idx = findFileIndex(*files, *rowName);
427 if (idx == -1)
428 return;
430 QString extSt(files->extendedStatus(idx));
431 if (extSt.isEmpty())
432 return;
434 *rowName = extSt;
437 void Git::removeExtraFileInfo(QString* rowName) {
439 if (rowName->contains(" --> ")) // return destination file name
440 *rowName = rowName->section(" --> ", 1, 1).section(" (", 0, 0);
443 void Git::formatPatchFileHeader(QString* rowName, SCRef sha, SCRef diffToSha,
444 bool combined, bool allMergeFiles) {
445 if (combined) {
446 rowName->prepend("diff --combined ");
447 return; // TODO rename/copy still not supported in this case
449 // let's see if it's a rename/copy...
450 addExtraFileInfo(rowName, sha, diffToSha, allMergeFiles);
452 if (rowName->contains(" --> ")) { // ...it is!
454 SCRef destFile(rowName->section(" --> ", 1, 1).section(" (", 0, 0));
455 SCRef origFile(rowName->section(" --> ", 0, 0));
456 *rowName = "diff --git a/" + origFile + " b/" + destFile;
457 } else
458 *rowName = "diff --git a/" + *rowName + " b/" + *rowName;
461 Annotate* Git::startAnnotate(FileHistory* fh, QObject* guiObj) { // non blocking
463 Annotate* ann = new Annotate(this, guiObj);
464 if (!ann->start(fh)) // non blocking call
465 return NULL; // ann will delete itself when done
467 return ann; // caller will delete with Git::cancelAnnotate()
470 void Git::cancelAnnotate(Annotate* ann) {
472 if (ann)
473 ann->deleteWhenDone();
476 const FileAnnotation* Git::lookupAnnotation(Annotate* ann, SCRef sha) {
478 return (ann ? ann->lookupAnnotation(sha) : NULL);
481 void Git::cancelDataLoading(const FileHistory* fh) {
482 // normally called when closing file viewer
484 emit cancelLoading(fh); // non blocking
487 const Rev* Git::revLookup(SCRef sha, const FileHistory* fh) const {
489 return revLookup(toTempSha(sha), fh);
492 const Rev* Git::revLookup(const ShaString& sha, const FileHistory* fh) const {
494 const RevMap& r = (fh ? fh->revs : revData->revs);
495 return (sha.latin1() ? r.value(sha) : NULL);
498 bool Git::run(SCRef runCmd, QString* runOutput, QObject* receiver, SCRef buf) {
500 QByteArray ba;
501 bool ret = run(runOutput ? &ba : NULL, runCmd, receiver, buf);
502 if (runOutput)
503 *runOutput = ba;
505 return ret;
508 bool Git::run(QByteArray* runOutput, SCRef runCmd, QObject* receiver, SCRef buf) {
510 MyProcess p(parent(), this, workDir, errorReportingEnabled);
511 return p.runSync(runCmd, runOutput, receiver, buf);
514 MyProcess* Git::runAsync(SCRef runCmd, QObject* receiver, SCRef buf) {
516 MyProcess* p = new MyProcess(parent(), this, workDir, errorReportingEnabled);
517 if (!p->runAsync(runCmd, receiver, buf)) {
518 delete p;
519 p = NULL;
521 return p; // auto-deleted when done
524 MyProcess* Git::runAsScript(SCRef runCmd, QObject* receiver, SCRef buf) {
526 const QString scriptFile(workDir + "/qgit_script" + QGit::SCRIPT_EXT);
527 #ifndef Q_OS_WIN32
528 // without this process doesn't start under Linux
529 QString cmd(runCmd.startsWith("#!") ? runCmd : "#!/bin/sh\n" + runCmd);
530 #else
531 QString cmd(runCmd);
532 #endif
533 if (!writeToFile(scriptFile, cmd, true))
534 return NULL;
536 MyProcess* p = runAsync(scriptFile, receiver, buf);
537 if (p)
538 connect(p, SIGNAL(eof()), this, SLOT(on_runAsScript_eof()));
539 return p;
542 void Git::on_runAsScript_eof() {
544 QDir dir(workDir);
545 dir.remove("qgit_script" + QGit::SCRIPT_EXT);
548 void Git::cancelProcess(MyProcess* p) {
550 if (p)
551 p->on_cancel(); // non blocking call
554 int Git::findFileIndex(const RevFile& rf, SCRef name) {
556 if (name.isEmpty())
557 return -1;
559 int idx = name.lastIndexOf('/') + 1;
560 SCRef dr = name.left(idx);
561 SCRef nm = name.mid(idx);
563 for (uint i = 0, cnt = rf.count(); i < cnt; ++i) {
564 if (fileNamesVec[rf.nameAt(i)] == nm && dirNamesVec[rf.dirAt(i)] == dr)
565 return i;
567 return -1;
570 const QString Git::getLaneParent(SCRef fromSHA, int laneNum) {
572 const Rev* rs = revLookup(fromSHA);
573 if (!rs)
574 return "";
576 for (int idx = rs->orderIdx - 1; idx >= 0; idx--) {
578 const Rev* r = revLookup(revData->revOrder[idx]);
579 if (laneNum >= r->lanes.count())
580 return "";
582 if (!isFreeLane(r->lanes[laneNum])) {
584 int type = r->lanes[laneNum], parNum = 0;
585 while (!isMerge(type) && type != ACTIVE) {
587 if (isHead(type))
588 parNum++;
590 type = r->lanes[--laneNum];
592 return r->parent(parNum);
595 return "";
598 const QStringList Git::getChildren(SCRef parent) {
600 QStringList children;
601 const Rev* r = revLookup(parent);
602 if (!r)
603 return children;
605 for (int i = 0; i < r->children.count(); i++)
606 children.append(revData->revOrder[r->children[i]]);
608 // reorder children by loading order
609 QStringList::iterator itC(children.begin());
610 for ( ; itC != children.end(); ++itC) {
611 const Rev* r = revLookup(*itC);
612 (*itC).prepend(QString("%1 ").arg(r->orderIdx, 6));
614 children.sort();
615 for (itC = children.begin(); itC != children.end(); ++itC)
616 (*itC) = (*itC).section(' ', -1, -1);
618 return children;
621 const QString Git::getShortLog(SCRef sha) {
623 const Rev* r = revLookup(sha);
624 return (r ? r->shortLog() : "");
627 MyProcess* Git::getDiff(SCRef sha, QObject* receiver, SCRef diffToSha, bool combined) {
629 if (sha.isEmpty())
630 return NULL;
632 QString runCmd;
633 if (sha != ZERO_SHA) {
634 runCmd = "git diff-tree --no-color -r --patch-with-stat ";
635 runCmd.append(combined ? "-c " : "-C -m "); // TODO rename for combined
636 runCmd.append(diffToSha + " " + sha); // diffToSha could be empty
637 } else
638 runCmd = "git diff-index --no-color -r -m --patch-with-stat HEAD";
640 return runAsync(runCmd, receiver);
643 const QString Git::getWorkDirDiff(SCRef fileName) {
645 QString runCmd("git diff-index --no-color -r -z -m -p --full-index --no-commit-id HEAD"), runOutput;
646 if (!fileName.isEmpty())
647 runCmd.append(" -- " + quote(fileName));
649 if (!run(runCmd, &runOutput))
650 return "";
652 /* For unknown reasons file sha of index is not ZERO_SHA but
653 a value of unknown origin.
654 Replace that with ZERO_SHA so to not fool annotate
656 int idx = runOutput.indexOf("..");
657 if (idx != -1)
658 runOutput.replace(idx + 2, 40, ZERO_SHA);
660 return runOutput;
663 const QString Git::getFileSha(SCRef file, SCRef revSha) {
665 if (revSha == ZERO_SHA) {
666 QStringList files, dummy;
667 getWorkDirFiles(files, dummy, RevFile::ANY);
668 if (files.contains(file))
669 return ZERO_SHA; // it is unknown to git
671 const QString sha(revSha == ZERO_SHA ? "HEAD" : revSha);
672 QString runCmd("git ls-tree -r " + sha + " " + quote(file)), runOutput;
673 if (!run(runCmd, &runOutput))
674 return "";
676 return runOutput.mid(12, 40); // could be empty, deleted file case
679 MyProcess* Git::getFile(SCRef fileSha, QObject* receiver, QByteArray* result, SCRef fileName) {
681 QString runCmd;
683 symlinks in git are one line files with just the name of the target,
684 not the target content. Instead 'cat' command resolves symlinks and
685 returns target content. So we use 'cat' only if the file is modified
686 in working directory, to let annotation work for changed files, otherwise
687 we go with a safe 'git cat-file blob HEAD' instead.
688 NOTE: This fails if the modified file is a new symlink, converted
689 from an old plain file. In this case annotation will fail until
690 change is committed.
692 if (fileSha == ZERO_SHA)
694 #ifdef Q_OS_WIN32
696 QString winPath = quote(fileName);
697 winPath.replace("/", "\\");
698 runCmd = "type " + winPath;
700 #else
701 runCmd = "cat " + quote(fileName);
702 #endif
704 else {
705 if (fileSha.isEmpty()) // deleted
706 runCmd = "git diff-tree HEAD HEAD"; // fake an empty file reading
707 else
708 runCmd = "git cat-file blob " + fileSha;
710 if (!receiver) {
711 run(result, runCmd);
712 return NULL; // in case of sync call we ignore run() return value
714 return runAsync(runCmd, receiver);
717 MyProcess* Git::getHighlightedFile(SCRef fileSha, QObject* receiver, QString* result, SCRef fileName) {
719 if (!isTextHighlighter()) {
720 dbs("ASSERT in getHighlightedFile: highlighter not found");
721 return NULL;
723 QString ext(fileName.section('.', -1, -1, QString::SectionIncludeLeadingSep));
724 QString inputFile(workDir + "/qgit_hlght_input" + ext);
725 if (!saveFile(fileSha, fileName, inputFile))
726 return NULL;
728 QString runCmd("source-highlight --failsafe -f html -i " + quote(inputFile));
730 if (!receiver) {
731 run(runCmd, result);
732 on_getHighlightedFile_eof();
733 return NULL; // in case of sync call we ignore run() return value
735 MyProcess* p = runAsync(runCmd, receiver);
736 if (p)
737 connect(p, SIGNAL(eof()), this, SLOT(on_getHighlightedFile_eof()));
738 return p;
741 void Git::on_getHighlightedFile_eof() {
743 QDir dir(workDir);
744 const QStringList sl(dir.entryList(QStringList() << "qgit_hlght_input*"));
745 FOREACH_SL (it, sl)
746 dir.remove(*it);
749 bool Git::saveFile(SCRef fileSha, SCRef fileName, SCRef path) {
751 QByteArray fileData;
752 getFile(fileSha, NULL, &fileData, fileName); // sync call
753 if (isBinaryFile(fileName))
754 return writeToFile(path, fileData);
756 return writeToFile(path, QString(fileData));
759 bool Git::getTree(SCRef treeSha, TreeInfo& ti, bool isWorkingDir, SCRef path) {
761 QStringList deleted;
762 if (isWorkingDir) {
764 // retrieve unknown and deleted files under path
765 QStringList unknowns, dummy;
766 getWorkDirFiles(unknowns, dummy, RevFile::UNKNOWN);
768 FOREACH_SL (it, unknowns) {
770 // don't add files under other directories
771 QFileInfo f(*it);
772 SCRef d(f.dir().path());
774 if (d == path || (path.isEmpty() && d == ".")) {
775 TreeEntry te(f.fileName(), "", "?");
776 ti.append(te);
779 getWorkDirFiles(deleted, dummy, RevFile::DELETED);
781 // if needed fake a working directory tree starting from HEAD tree
782 QString runOutput, tree(treeSha);
783 if (treeSha == ZERO_SHA) {
784 // HEAD could be empty for just init'ed repositories
785 if (!run("git rev-parse --revs-only HEAD", &tree))
786 return false;
788 tree = tree.trimmed();
790 if (!tree.isEmpty() && !run("git ls-tree " + tree, &runOutput))
791 return false;
793 const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts));
794 FOREACH_SL (it, sl) {
796 // append any not deleted file
797 SCRef fn((*it).section('\t', 1, 1));
798 SCRef fp(path.isEmpty() ? fn : path + '/' + fn);
800 if (deleted.empty() || (deleted.indexOf(fp) == -1)) {
801 TreeEntry te(fn, (*it).mid(12, 40), (*it).mid(7, 4));
802 ti.append(te);
805 qSort(ti); // list directories before files
806 return true;
809 void Git::getWorkDirFiles(SList files, SList dirs, RevFile::StatusFlag status) {
811 files.clear();
812 dirs.clear();
813 const RevFile* f = getFiles(ZERO_SHA);
814 if (!f)
815 return;
817 for (int i = 0; i < f->count(); i++) {
819 if (f->statusCmp(i, status)) {
821 SCRef fp(filePath(*f, i));
822 files.append(fp);
823 for (int j = 0, cnt = fp.count('/'); j < cnt; j++) {
825 SCRef dir(fp.section('/', 0, j));
826 if (dirs.indexOf(dir) == -1)
827 dirs.append(dir);
833 bool Git::isNothingToCommit() {
835 if (!revsFiles.contains(ZERO_SHA_RAW))
836 return true;
838 const RevFile* rf = revsFiles[ZERO_SHA_RAW];
839 return (rf->count() == workingDirInfo.otherFiles.count());
842 bool Git::isTreeModified(SCRef sha) {
844 const RevFile* f = getFiles(sha);
845 if (!f)
846 return true; // no files info, stay on the safe side
848 for (int i = 0; i < f->count(); ++i)
849 if (!f->statusCmp(i, RevFile::MODIFIED))
850 return true;
852 return false;
855 bool Git::isParentOf(SCRef par, SCRef child) {
857 const Rev* c = revLookup(child);
858 return (c && c->parentsCount() == 1 && QString(c->parent(0)) == par); // no merges
861 bool Git::isSameFiles(SCRef tree1Sha, SCRef tree2Sha) {
863 // early skip common case of browsing with up and down arrows, i.e.
864 // going from parent(child) to child(parent). In this case we can
865 // check RevFileMap and skip a costly 'git diff-tree' call.
866 if (isParentOf(tree1Sha, tree2Sha))
867 return !isTreeModified(tree2Sha);
869 if (isParentOf(tree2Sha, tree1Sha))
870 return !isTreeModified(tree1Sha);
872 const QString runCmd("git diff-tree --no-color -r " + tree1Sha + " " + tree2Sha);
873 QString runOutput;
874 if (!run(runCmd, &runOutput))
875 return false;
877 bool isChanged = (runOutput.indexOf(" A\t") != -1 || runOutput.indexOf(" D\t") != -1);
878 return !isChanged;
881 const QStringList Git::getDescendantBranches(SCRef sha, bool shaOnly) {
883 QStringList tl;
884 const Rev* r = revLookup(sha);
885 if (!r || (r->descBrnMaster == -1))
886 return tl;
888 const QVector<int>& nr = revLookup(revData->revOrder[r->descBrnMaster])->descBranches;
890 for (int i = 0; i < nr.count(); i++) {
892 const ShaString& sha = revData->revOrder[nr[i]];
893 if (shaOnly) {
894 tl.append(sha);
895 continue;
897 SCRef cap = " (" + sha + ") ";
898 RefMap::const_iterator it(refsShaMap.find(sha));
899 if (it == refsShaMap.constEnd())
900 continue;
902 if (!(*it).branches.empty())
903 tl.append((*it).branches.join(" ").append(cap));
905 if (!(*it).remoteBranches.empty())
906 tl.append((*it).remoteBranches.join(" ").append(cap));
908 return tl;
911 const QStringList Git::getNearTags(bool goDown, SCRef sha) {
913 QStringList tl;
914 const Rev* r = revLookup(sha);
915 if (!r)
916 return tl;
918 int nearRefsMaster = (goDown ? r->descRefsMaster : r->ancRefsMaster);
919 if (nearRefsMaster == -1)
920 return tl;
922 const QVector<int>& nr = goDown ? revLookup(revData->revOrder[nearRefsMaster])->descRefs :
923 revLookup(revData->revOrder[nearRefsMaster])->ancRefs;
925 for (int i = 0; i < nr.count(); i++) {
927 const ShaString& sha = revData->revOrder[nr[i]];
928 SCRef cap = " (" + sha + ")";
929 RefMap::const_iterator it(refsShaMap.find(sha));
930 if (it != refsShaMap.constEnd())
931 tl.append((*it).tags.join(cap).append(cap));
933 return tl;
936 const QString Git::getLastCommitMsg() {
938 // FIXME: Make sure the amend action is not called when there is
939 // nothing to amend. That is in empty repository or over StGit stack
940 // with nothing applied.
941 QString sha;
942 QString top;
943 if (run("git rev-parse --verify HEAD", &top))
944 sha = top.trimmed();
945 else {
946 dbs("ASSERT: getLastCommitMsg head is not valid");
947 return "";
950 const Rev* c = revLookup(sha);
951 if (!c) {
952 dbp("ASSERT: getLastCommitMsg sha <%1> not found", sha);
953 return "";
956 return c->shortLog() + "\n\n" + c->longLog().trimmed();
959 const QString Git::getNewCommitMsg() {
961 const Rev* c = revLookup(ZERO_SHA);
962 if (!c) {
963 dbs("ASSERT: getNewCommitMsg zero_sha not found");
964 return "";
967 QString status = c->longLog();
968 status.prepend('\n').replace(QRegExp("\\n([^#])"), "\n#\\1"); // comment all the lines
969 return status;
972 //CT TODO utility function; can go elsewhere
973 const QString Git::colorMatch(SCRef txt, QRegExp& regExp) {
975 QString text = qt4and5escaping(txt);
977 if (regExp.isEmpty())
978 return text;
980 SCRef startCol(QString::fromLatin1("<b><font color=\"red\">"));
981 SCRef endCol(QString::fromLatin1("</font></b>"));
982 int pos = 0;
983 while ((pos = text.indexOf(regExp, pos)) != -1) {
985 SCRef match(regExp.cap(0));
986 const QString coloredText(startCol + match + endCol);
987 text.replace(pos, match.length(), coloredText);
988 pos += coloredText.length();
990 return text;
993 //CT TODO utility function; can go elsewhere
994 const QString Git::formatList(SCList sl, SCRef name, bool inOneLine) {
996 if (sl.isEmpty())
997 return QString();
999 QString ls = "<tr><td class='h'>" + name + "</td><td>";
1000 const QString joinStr = inOneLine ? ", " : "</td></tr>\n" + ls;
1001 ls += sl.join(joinStr);
1002 ls += "</td></tr>\n";
1003 return ls;
1006 const QString Git::getDesc(SCRef sha, QRegExp& shortLogRE, QRegExp& longLogRE,
1007 bool showHeader, FileHistory* fh) {
1009 if (sha.isEmpty())
1010 return "";
1012 const Rev* c = revLookup(sha, fh);
1013 if (!c) // sha of a not loaded revision, as
1014 return ""; // example asked from file history
1016 QString text;
1017 if (c->isDiffCache)
1018 text = Qt::convertFromPlainText(c->longLog());
1019 else {
1020 QTextStream ts(&text);
1021 ts << "<html><head><style type=\"text/css\">"
1022 "tr.head { background-color: " << QPalette().color(QPalette::Mid).name() << " }\n"
1023 "td.h { font-weight: bold; }\n"
1024 "table { background-color: " << QPalette().color(QPalette::Button).name() << "; }\n"
1025 "span.h { font-weight: bold; font-size: medium; }\n"
1026 "div.l { white-space: pre; "
1027 "font-family: " << TYPE_WRITER_FONT.family() << ";"
1028 "font-size: " << TYPE_WRITER_FONT.pointSize() << "pt;}\n"
1029 "</style></head><body><div class='t'>\n"
1030 "<table border=0 cellspacing=0 cellpadding=2>";
1032 ts << "<tr class='head'> <th></th> <th><span class='h'>"
1033 << colorMatch(c->shortLog(), shortLogRE)
1034 << "</span></th></tr>";
1036 if (showHeader) {
1037 if (c->committer() != c->author())
1038 ts << formatList(QStringList(qt4and5escaping(c->committer())), "Committer");
1039 ts << formatList(QStringList(qt4and5escaping(c->author())), "Author");
1040 ts << formatList(QStringList(getLocalDate(c->authorDate())), " Author date");
1042 if (c->isUnApplied || c->isApplied) {
1044 QStringList patches(getRefName(sha, APPLIED));
1045 patches += getRefName(sha, UN_APPLIED);
1046 ts << formatList(patches, "Patch");
1047 } else {
1048 ts << formatList(c->parents(), "Parent", false);
1049 ts << formatList(getChildren(sha), "Child", false);
1050 ts << formatList(getDescendantBranches(sha), "Branch", false);
1051 ts << formatList(getNearTags(!optGoDown, sha), "Follows");
1052 ts << formatList(getNearTags(optGoDown, sha), "Precedes");
1055 QString longLog(c->longLog());
1056 if (showHeader) {
1057 longLog.prepend(QString("\n") + c->shortLog() + "\n");
1060 QString log(colorMatch(longLog, longLogRE));
1061 log.replace("\n", "\n ").prepend('\n');
1062 ts << "</table></div><div class='l'>" << log << "</div></body></html>";
1064 // highlight SHA's
1066 // added to commit logs, we avoid to call git rev-parse for a possible abbreviated
1067 // sha if there isn't a leading trailing space or an open parenthesis and,
1068 // in that case, before the space must not be a ':' character.
1069 // It's an ugly heuristic, but seems to work in most cases.
1070 QRegExp reSHA("..[0-9a-f]{21,40}|[^:][\\s(][0-9a-f]{6,20}", Qt::CaseInsensitive);
1071 reSHA.setMinimal(false);
1072 int pos = 0;
1073 while ((pos = text.indexOf(reSHA, pos)) != -1) {
1075 SCRef ref = reSHA.cap(0).mid(2);
1076 const Rev* r = (ref.length() == 40 ? revLookup(ref) : revLookup(getRefSha(ref)));
1077 if (r && r->sha() != ZERO_SHA_RAW) {
1078 QString slog(r->shortLog());
1079 if (slog.isEmpty()) // very rare but possible
1080 slog = r->sha();
1081 if (slog.length() > 60)
1082 slog = slog.left(57).trimmed().append("...");
1084 const QString link("<a href=\"" + r->sha() + "\">" + slog.toHtmlEscaped() + "</a>");
1085 text.replace(pos + 2, ref.length(), link);
1086 pos += link.length();
1087 } else
1088 pos += reSHA.cap(0).length();
1090 return text;
1093 const RevFile* Git::insertNewFiles(SCRef sha, SCRef data) {
1095 /* we use an independent FileNamesLoader to avoid data
1096 * corruption if we are loading file names in background
1098 FileNamesLoader fl;
1100 RevFile* rf = new RevFile();
1101 parseDiffFormat(*rf, data, fl);
1102 flushFileNames(fl);
1104 revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf);
1105 return rf;
1108 bool Git::runDiffTreeWithRenameDetection(SCRef runCmd, QString* runOutput) {
1109 /* Under some cases git could warn out:
1111 "too many files, skipping inexact rename detection"
1113 So if this occurs fallback on NO rename detection.
1115 QString cmd(runCmd); // runCmd must be without -C option
1116 cmd.replace("git diff-tree", "git diff-tree -C");
1118 errorReportingEnabled = false;
1119 bool renameDetectionOk = run(cmd, runOutput);
1120 errorReportingEnabled = true;
1122 if (!renameDetectionOk) // retry without rename detection
1123 return run(runCmd, runOutput);
1125 return true;
1128 const RevFile* Git::getAllMergeFiles(const Rev* r) {
1130 SCRef mySha(ALL_MERGE_FILES + r->sha());
1131 if (revsFiles.contains(toTempSha(mySha)))
1132 return revsFiles[toTempSha(mySha)];
1134 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1136 QString runCmd("git diff-tree --no-color -r -m " + r->sha()), runOutput;
1137 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1138 return NULL;
1140 return insertNewFiles(mySha, runOutput);
1143 const RevFile* Git::getFiles(SCRef sha, SCRef diffToSha, bool allFiles, SCRef path) {
1145 const Rev* r = revLookup(sha);
1146 if (!r)
1147 return NULL;
1149 if (r->parentsCount() == 0) // skip initial rev
1150 return NULL;
1152 if (r->parentsCount() > 1 && diffToSha.isEmpty() && allFiles)
1153 return getAllMergeFiles(r);
1155 if (!diffToSha.isEmpty() && (sha != ZERO_SHA)) {
1157 QString runCmd("git diff-tree --no-color -r -m ");
1158 runCmd.append(diffToSha + " " + sha);
1159 if (!path.isEmpty())
1160 runCmd.append(" " + path);
1162 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1164 QString runOutput;
1165 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1166 return NULL;
1168 // we insert a dummy revision file object. It will be
1169 // overwritten at each request but we don't care.
1170 return insertNewFiles(CUSTOM_SHA, runOutput);
1172 if (revsFiles.contains(r->sha()))
1173 return revsFiles[r->sha()]; // ZERO_SHA search arrives here
1175 if (sha == ZERO_SHA) {
1176 dbs("ASSERT in Git::getFiles, ZERO_SHA not found");
1177 return NULL;
1180 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1182 QString runCmd("git diff-tree --no-color -r -c " + sha), runOutput;
1183 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1184 return NULL;
1186 if (revsFiles.contains(r->sha())) // has been created in the mean time?
1187 return revsFiles[r->sha()];
1189 cacheNeedsUpdate = true;
1190 return insertNewFiles(sha, runOutput);
1193 bool Git::startFileHistory(SCRef sha, SCRef startingFileName, FileHistory* fh) {
1195 QStringList args(getDescendantBranches(sha, true));
1196 if (args.isEmpty())
1197 args << "HEAD";
1199 QString newestFileName = getNewestFileName(args, startingFileName);
1200 fh->resetFileNames(newestFileName);
1202 args.clear(); // load history from all the branches
1203 args << getAllRefSha(BRANCH | RMT_BRANCH);
1205 args << "--" << newestFileName;
1206 return startRevList(args, fh);
1209 const QString Git::getNewestFileName(SCList branches, SCRef fileName) {
1211 QString curFileName(fileName), runOutput, args;
1212 while (true) {
1213 args = branches.join(" ") + " -- " + curFileName;
1214 if (!run("git ls-tree " + args, &runOutput))
1215 break;
1217 if (!runOutput.isEmpty())
1218 break;
1220 QString msg("Retrieving file renames, now at '" + curFileName + "'...");
1221 QApplication::postEvent(parent(), new MessageEvent(msg));
1222 EM_PROCESS_EVENTS_NO_INPUT;
1224 if (!run("git rev-list -n1 " + args, &runOutput))
1225 break;
1227 if (runOutput.isEmpty()) // try harder
1228 if (!run("git rev-list --full-history -n1 " + args, &runOutput))
1229 break;
1231 if (runOutput.isEmpty())
1232 break;
1234 SCRef sha = runOutput.trimmed();
1235 QStringList newCur;
1236 if (!populateRenamedPatches(sha, QStringList(curFileName), NULL, &newCur, true))
1237 break;
1239 curFileName = newCur.first();
1241 return curFileName;
1244 void Git::getFileFilter(SCRef path, ShaSet& shaSet) const {
1246 shaSet.clear();
1247 QRegExp rx(path, Qt::CaseInsensitive, QRegExp::Wildcard);
1248 FOREACH (ShaVect, it, revData->revOrder) {
1250 if (!revsFiles.contains(*it))
1251 continue;
1253 // case insensitive, wildcard search
1254 const RevFile* rf = revsFiles[*it];
1255 for (int i = 0; i < rf->count(); ++i)
1256 if (filePath(*rf, i).contains(rx)) {
1257 shaSet.insert(*it);
1258 break;
1263 bool Git::getPatchFilter(SCRef exp, bool isRegExp, ShaSet& shaSet) {
1265 shaSet.clear();
1266 QString buf;
1267 FOREACH (ShaVect, it, revData->revOrder)
1268 if (*it != ZERO_SHA_RAW)
1269 buf.append(*it).append('\n');
1271 if (buf.isEmpty())
1272 return true;
1274 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1276 QString runCmd("git diff-tree --no-color -r -s --stdin "), runOutput;
1277 if (isRegExp)
1278 runCmd.append("--pickaxe-regex ");
1280 runCmd.append(quote("-S" + exp));
1281 if (!run(runCmd, &runOutput, NULL, buf))
1282 return false;
1284 const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts));
1285 FOREACH_SL (it, sl)
1286 shaSet.insert(*it);
1288 return true;
1291 bool Git::resetCommits(int parentDepth) {
1293 QString runCmd("git reset --soft HEAD~");
1294 runCmd.append(QString::number(parentDepth));
1295 return run(runCmd);
1298 bool Git::applyPatchFile(SCRef patchPath, bool fold, bool isDragDrop) {
1300 if (isStGIT) {
1301 if (fold) {
1302 bool ok = run("stg fold " + quote(patchPath)); // merge in working directory
1303 if (ok)
1304 ok = run("stg refresh"); // update top patch
1305 return ok;
1306 } else
1307 return run("stg import --mail " + quote(patchPath));
1309 QString runCmd("git am --utf8 --3way ");
1311 QSettings settings;
1312 const QString APOpt(settings.value(AM_P_OPT_KEY).toString());
1313 if (!APOpt.isEmpty())
1314 runCmd.append(APOpt.trimmed() + " ");
1316 if (isDragDrop)
1317 runCmd.append("--keep ");
1319 if (testFlag(SIGN_PATCH_F))
1320 runCmd.append("--signoff ");
1322 return run(runCmd + quote(patchPath));
1325 const QStringList Git::sortShaListByIndex(SCList shaList) {
1327 QStringList orderedShaList;
1328 FOREACH_SL (it, shaList)
1329 appendNamesWithId(orderedShaList, *it, QStringList(*it), true);
1331 orderedShaList.sort();
1332 QStringList::iterator itN(orderedShaList.begin());
1333 for ( ; itN != orderedShaList.end(); ++itN) // strip 'idx'
1334 (*itN) = (*itN).section(' ', -1, -1);
1336 return orderedShaList;
1339 bool Git::formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir) {
1341 bool remote = !remoteDir.isEmpty();
1342 QSettings settings;
1343 const QString FPOpt(settings.value(FMT_P_OPT_KEY).toString());
1345 QString runCmd("git format-patch --no-color");
1346 if (testFlag(NUMBERS_F) && !remote)
1347 runCmd.append(" -n");
1349 if (remote)
1350 runCmd.append(" --keep-subject");
1352 runCmd.append(" -o " + quote(dirPath));
1353 if (!FPOpt.isEmpty())
1354 runCmd.append(" " + FPOpt.trimmed());
1356 const QString tmp(workDir);
1357 if (remote)
1358 workDir = remoteDir; // run() uses workDir value
1360 // shaList is ordered by newest to oldest
1361 runCmd.append(" " + shaList.last());
1362 runCmd.append(QString::fromLatin1("^..") + shaList.first());
1363 bool ret = run(runCmd);
1364 workDir = tmp;
1365 return ret;
1368 const QStringList Git::getOtherFiles(SCList selFiles, bool onlyInIndex) {
1370 const RevFile* files = getFiles(ZERO_SHA); // files != NULL
1371 QStringList notSelFiles;
1372 for (int i = 0; i < files->count(); ++i) {
1373 SCRef fp = filePath(*files, i);
1374 if (selFiles.indexOf(fp) == -1) { // not selected...
1375 if (!onlyInIndex || files->statusCmp(i, RevFile::IN_INDEX))
1376 notSelFiles.append(fp);
1379 return notSelFiles;
1382 bool Git::updateIndex(SCList selFiles) {
1384 const RevFile* files = getFiles(ZERO_SHA); // files != NULL
1386 QStringList toAdd, toRemove;
1387 FOREACH_SL (it, selFiles) {
1388 int idx = findFileIndex(*files, *it);
1389 if (files->statusCmp(idx, RevFile::DELETED))
1390 toRemove << *it;
1391 else
1392 toAdd << *it;
1394 if (!toRemove.isEmpty() && !run("git rm --cached --ignore-unmatch -- " + quote(toRemove)))
1395 return false;
1397 if (!toAdd.isEmpty() && !run("git add -- " + quote(toAdd)))
1398 return false;
1400 return true;
1403 bool Git::commitFiles(SCList selFiles, SCRef msg, bool amend) {
1405 const QString msgFile(gitDir + "/qgit_cmt_msg.txt");
1406 if (!writeToFile(msgFile, msg)) // early skip
1407 return false;
1409 // add user selectable commit options
1410 QSettings settings;
1411 const QString CMArgs(settings.value(CMT_ARGS_KEY).toString());
1413 QString cmtOptions;
1414 if (!CMArgs.isEmpty())
1415 cmtOptions.append(" " + CMArgs);
1417 if (testFlag(SIGN_CMT_F))
1418 cmtOptions.append(" -s");
1420 if (testFlag(VERIFY_CMT_F))
1421 cmtOptions.append(" -v");
1423 if (amend)
1424 cmtOptions.append(" --amend");
1426 bool ret = false;
1428 // get not selected files but updated in index to restore at the end
1429 const QStringList notSel(getOtherFiles(selFiles, optOnlyInIndex));
1431 // call git reset to remove not selected files from index
1432 if (!notSel.empty() && !run("git reset -- " + quote(notSel)))
1433 goto fail;
1435 // update index with selected files
1436 if (!updateIndex(selFiles))
1437 goto fail;
1439 // now we can finally commit..
1440 if (!run("git commit" + cmtOptions + " -F " + quote(msgFile)))
1441 goto fail;
1443 // restore not selected files that were already in index
1444 if (!notSel.empty() && !updateIndex(notSel))
1445 goto fail;
1447 ret = true;
1448 fail:
1449 QDir dir(workDir);
1450 dir.remove(msgFile);
1451 return ret;
1454 bool Git::mkPatchFromWorkDir(SCRef msg, SCRef patchFile, SCList files) {
1456 /* unfortunately 'git diff' sees only files already
1457 * known to git or already in index, so update index first
1458 * to be sure also unknown files are correctly found
1460 if (!updateIndex(files))
1461 return false;
1463 QString runOutput;
1464 if (!run("git diff --no-ext-diff -C HEAD -- " + quote(files), &runOutput))
1465 return false;
1467 const QString patch("Subject: " + msg + "\n---\n" + runOutput);
1468 return writeToFile(patchFile, patch);
1471 bool Git::stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold) {
1473 /* Here the deal is to use 'stg import' and 'stg fold' to add a new
1474 * patch or refresh the current one respectively. Unfortunately refresh
1475 * does not work with partial selection of files and also does not take
1476 * patch message from a file that is needed to avoid artifacts with '\n'
1477 * and friends.
1479 * So steps are:
1481 * - Create a patch file with the changes you want to import/fold in StGit
1482 * - Stash working directory files because import/fold wants a clean directory
1483 * - Import/fold the patch
1484 * - Unstash and merge working directory modified files
1485 * - Restore index with not selected files
1488 /* Step 1: Create a patch file with the changes you want to import/fold */
1489 bool ret = false;
1490 const QString patchFile(gitDir + "/qgit_tmp_patch.txt");
1492 // in case we don't have files to restore we can shortcut various commands
1493 bool partialSelection = !getOtherFiles(selFiles, !optOnlyInIndex).isEmpty();
1495 // get not selected files but updated in index to restore at the end
1496 QStringList notSel;
1497 if (partialSelection) // otherwise notSel is for sure empty
1498 notSel = getOtherFiles(selFiles, optOnlyInIndex);
1500 // create a patch with diffs between working directory and HEAD
1501 if (!mkPatchFromWorkDir(msg, patchFile, selFiles))
1502 goto fail;
1504 /* Step 2: Stash working directory modified files */
1505 if (partialSelection) {
1506 errorReportingEnabled = false;
1507 run("git stash"); // unfortunately 'git stash' is noisy on stderr
1508 errorReportingEnabled = true;
1511 /* Step 3: Call stg import/fold */
1513 // setup a clean state
1514 if (!run("stg status --reset"))
1515 goto fail_and_unstash;
1517 if (fold) {
1518 // update patch message before to fold, note that
1519 // command 'stg edit' requires stg version 0.14 or later
1520 if (!msg.isEmpty() && !run("stg edit --message " + quote(msg.trimmed())))
1521 goto fail_and_unstash;
1523 if (!run("stg fold " + quote(patchFile)))
1524 goto fail_and_unstash;
1526 if (!run("stg refresh")) // refresh needed after fold
1527 goto fail_and_unstash;
1529 } else if (!run("stg import --mail --name " + quote(patchName) + " " + quote(patchFile)))
1530 goto fail_and_unstash;
1532 if (partialSelection) {
1534 /* Step 4: Unstash and merge working directory modified files */
1535 errorReportingEnabled = false;
1536 run("git stash pop"); // unfortunately 'git stash' is noisy on stderr
1537 errorReportingEnabled = true;
1539 /* Step 5: restore not selected files that were already in index */
1540 if (!notSel.empty() && !updateIndex(notSel))
1541 goto fail;
1544 ret = true;
1545 goto exit;
1547 fail_and_unstash:
1549 if (partialSelection) {
1550 run("git reset");
1551 errorReportingEnabled = false;
1552 run("git stash pop");
1553 errorReportingEnabled = true;
1555 fail:
1556 exit:
1557 QDir dir(workDir);
1558 dir.remove(patchFile);
1559 return ret;
1562 bool Git::makeBranch(SCRef sha, SCRef branchName) {
1564 return run("git branch " + branchName + " " + sha);
1567 bool Git::makeTag(SCRef sha, SCRef tagName, SCRef msg) {
1569 if (msg.isEmpty())
1570 return run("git tag " + tagName + " " + sha);
1572 return run("git tag -m \"" + msg + "\" " + tagName + " " + sha);
1575 bool Git::deleteTag(SCRef sha) {
1577 const QStringList tags(getRefName(sha, TAG));
1578 if (!tags.empty())
1579 return run("git tag -d " + tags.first()); // only one
1581 return false;
1584 bool Git::stgPush(SCRef sha) {
1586 const QStringList patch(getRefName(sha, UN_APPLIED));
1587 if (patch.count() != 1) {
1588 dbp("ASSERT in Git::stgPush, found %1 patches instead of 1", patch.count());
1589 return false;
1591 return run("stg push " + quote(patch.first()));
1594 bool Git::stgPop(SCRef sha) {
1596 const QStringList patch(getRefName(sha, APPLIED));
1597 if (patch.count() != 1) {
1598 dbp("ASSERT in Git::stgPop, found %1 patches instead of 1", patch.count());
1599 return false;
1601 return run("stg pop " + quote(patch));
1605 //! cache for dates conversion. Common among qgit windows
1606 static QHash<QString, QString> localDates;
1608 * Accesses a cache that avoids slow date calculation
1610 * @param gitDate
1611 * the reference from which we want to get the date
1613 * @return
1614 * human-readable date
1616 const QString Git::getLocalDate(SCRef gitDate) {
1617 QString localDate(localDates.value(gitDate));
1619 // cache miss
1620 if (localDate.isEmpty()) {
1621 static QDateTime d;
1622 d.setTime_t(gitDate.toUInt());
1623 localDate = d.toString(Qt::SystemLocaleShortDate);
1625 // save to cache
1626 localDates[gitDate] = localDate;
1629 return localDate;
1632 const QStringList Git::getArgs(bool* quit, bool repoChanged) {
1634 QString args;
1635 QStringList arglist = qApp->arguments();
1637 // Remove first argument which is the path of the current executable
1638 arglist.removeFirst();
1640 if (startup) {
1641 foreach (QString arg, arglist) {
1642 // in arguments with spaces double quotes
1643 // are stripped by Qt, so re-add them
1644 if (arg.contains(' '))
1645 arg.prepend('\"').append('\"');
1647 args.append(arg + ' ');
1650 if (testFlag(RANGE_SELECT_F) && (!startup || args.isEmpty())) {
1652 RangeSelectImpl rs((QWidget*)parent(), &args, repoChanged, this);
1653 *quit = (rs.exec() == QDialog::Rejected); // modal execution
1654 if (*quit)
1655 return QStringList();
1657 startup = false;
1658 return MyProcess::splitArgList(args);
1661 bool Git::getGitDBDir(SCRef wd, QString& gd, bool& changed) {
1662 // we could run from a subdirectory, so we need to get correct directories
1664 QString runOutput, tmp(workDir);
1665 workDir = wd;
1666 errorReportingEnabled = false;
1667 bool success = run("git rev-parse --git-dir", &runOutput); // run under newWorkDir
1668 errorReportingEnabled = true;
1669 workDir = tmp;
1670 runOutput = runOutput.trimmed();
1671 if (success) {
1672 // 'git rev-parse --git-dir' output could be a relative
1673 // to working directory (as ex .git) or an absolute path
1674 QDir d(runOutput.startsWith("/") ? runOutput : wd + "/" + runOutput);
1675 changed = (d.absolutePath() != gitDir);
1676 gd = d.absolutePath();
1678 return success;
1681 bool Git::getBaseDir(SCRef wd, QString& bd, bool& changed) {
1682 // we could run from a subdirectory, so we need to get correct directories
1684 // We use --show-cdup and not --git-dir for this, in order to take into account configurations
1685 // in which .git is indeed a "symlink", a text file containing the path of the actual .git database dir.
1686 // In that particular case, the parent directory of the one given by --git-dir is *not* necessarily
1687 // the base directory of the repository.
1689 QString runOutput, tmp(workDir);
1690 workDir = wd;
1691 errorReportingEnabled = false;
1692 bool success = run("git rev-parse --show-cdup", &runOutput); // run under newWorkDir
1693 errorReportingEnabled = true;
1694 workDir = tmp;
1695 runOutput = runOutput.trimmed();
1696 if (success) {
1697 // 'git rev-parse --show-cdup' is relative to working directory.
1698 QDir d(wd + "/" + runOutput);
1699 bd = d.absolutePath();
1700 changed = (bd != workDir);
1702 else {
1703 changed = true;
1704 bd = wd;
1706 return success;
1709 Git::Reference* Git::lookupOrAddReference(const ShaString& sha) {
1710 RefMap::iterator it(refsShaMap.find(sha));
1711 if (it == refsShaMap.end()) it = refsShaMap.insert(sha, Reference());
1712 return &(*it);
1715 Git::Reference* Git::lookupReference(const ShaString& sha) {
1716 RefMap::iterator it(refsShaMap.find(sha));
1717 if (it == refsShaMap.end()) return 0;
1718 return &(*it);
1721 bool Git::getRefs() {
1723 // check for a StGIT stack
1724 QDir d(gitDir);
1725 QString stgCurBranch;
1726 if (d.exists("patches")) { // early skip
1727 errorReportingEnabled = false;
1728 isStGIT = run("stg branch", &stgCurBranch); // slow command
1729 errorReportingEnabled = true;
1730 stgCurBranch = stgCurBranch.trimmed();
1731 } else
1732 isStGIT = false;
1734 // check for a merge and read current branch sha
1735 isMergeHead = d.exists("MERGE_HEAD");
1736 QString curBranchSHA, curBranchName;
1737 if (!run("git rev-parse --revs-only HEAD", &curBranchSHA))
1738 return false;
1740 if (!run("git branch", &curBranchName))
1741 return false;
1743 curBranchSHA = curBranchSHA.trimmed();
1744 curBranchName = curBranchName.prepend('\n').section("\n*", 1);
1745 curBranchName = curBranchName.section('\n', 0, 0).trimmed();
1747 // read refs, normally unsorted
1748 QString runOutput;
1749 if (!run("git show-ref -d", &runOutput))
1750 return false;
1752 refsShaMap.clear();
1753 shaBackupBuf.clear(); // revs are already empty now
1755 QString prevRefSha;
1756 QStringList patchNames, patchShas;
1757 const QStringList rLst(runOutput.split('\n', QString::SkipEmptyParts));
1758 FOREACH_SL (it, rLst) {
1760 SCRef revSha = (*it).left(40);
1761 SCRef refName = (*it).mid(41);
1763 if (refName.startsWith("refs/patches/")) {
1765 // save StGIT patch sha, to be used later
1766 SCRef patchesDir("refs/patches/" + stgCurBranch + "/");
1767 if (refName.startsWith(patchesDir)) {
1768 patchNames.append(refName.mid(patchesDir.length()));
1769 patchShas.append(revSha);
1771 // StGIT patches should not be added to refs,
1772 // but an applied StGIT patch could be also an head or
1773 // a tag in this case will be added in another loop cycle
1774 continue;
1776 // one rev could have many tags
1777 Reference* cur = lookupOrAddReference(toPersistentSha(revSha, shaBackupBuf));
1779 if (refName.startsWith("refs/tags/")) {
1781 if (refName.endsWith("^{}")) { // tag dereference
1783 // we assume that a tag dereference follows strictly
1784 // the corresponding tag object in rLst. So the
1785 // last added tag is a tag object, not a commit object
1786 cur->tags.append(refName.mid(10, refName.length() - 13));
1788 // store tag object. Will be used to fetching
1789 // tag message (if any) when necessary.
1790 cur->tagObj = prevRefSha;
1792 // tagObj must be removed from ref map
1793 if (!prevRefSha.isEmpty())
1794 refsShaMap.remove(toTempSha(prevRefSha));
1796 } else
1797 cur->tags.append(refName.mid(10));
1799 cur->type |= TAG;
1801 } else if (refName.startsWith("refs/heads/")) {
1803 cur->branches.append(refName.mid(11));
1804 cur->type |= BRANCH;
1805 if (curBranchSHA == revSha) {
1806 cur->type |= CUR_BRANCH;
1807 cur->currentBranch = curBranchName;
1809 } else if (refName.startsWith("refs/remotes/") && !refName.endsWith("HEAD")) {
1811 cur->remoteBranches.append(refName.mid(13));
1812 cur->type |= RMT_BRANCH;
1814 } else if (!refName.startsWith("refs/bases/") && !refName.endsWith("HEAD")) {
1816 cur->refs.append(refName);
1817 cur->type |= REF;
1819 prevRefSha = revSha;
1821 if (isStGIT && !patchNames.isEmpty())
1822 parseStGitPatches(patchNames, patchShas);
1824 return !refsShaMap.empty();
1827 void Git::parseStGitPatches(SCList patchNames, SCList patchShas) {
1829 patchesStillToFind = 0;
1831 // get patch names and status of current branch
1832 QString runOutput;
1833 if (!run("stg series", &runOutput))
1834 return;
1836 const QStringList pl(runOutput.split('\n', QString::SkipEmptyParts));
1837 FOREACH_SL (it, pl) {
1839 SCRef status = (*it).left(1);
1840 SCRef patchName = (*it).mid(2);
1842 bool applied = (status == "+" || status == ">");
1843 int pos = patchNames.indexOf(patchName);
1844 if (pos == -1) {
1845 dbp("ASSERT in Git::parseStGitPatches(), patch %1 "
1846 "not found in references list.", patchName);
1847 continue;
1849 const ShaString& ss = toPersistentSha(patchShas.at(pos), shaBackupBuf);
1850 Reference* cur = lookupOrAddReference(ss);
1851 cur->stgitPatch = patchName;
1852 cur->type |= (applied ? APPLIED : UN_APPLIED);
1854 if (applied)
1855 patchesStillToFind++;
1859 const QStringList Git::getOthersFiles() {
1860 // add files present in working directory but not in git archive
1862 QString runCmd("git ls-files --others");
1863 QSettings settings;
1864 QString exFile(settings.value(EX_KEY, EX_DEF).toString());
1865 if (!exFile.isEmpty()) {
1866 QString path = (exFile.startsWith("/")) ? exFile : workDir + "/" + exFile;
1867 if (QFile::exists(path))
1868 runCmd.append(" --exclude-from=" + quote(exFile));
1870 QString exPerDir(settings.value(EX_PER_DIR_KEY, EX_PER_DIR_DEF).toString());
1871 if (!exPerDir.isEmpty())
1872 runCmd.append(" --exclude-per-directory=" + quote(exPerDir));
1874 QString runOutput;
1875 run(runCmd, &runOutput);
1876 return runOutput.split('\n', QString::SkipEmptyParts);
1879 Rev* Git::fakeRevData(SCRef sha, SCList parents, SCRef author, SCRef date, SCRef log, SCRef longLog,
1880 SCRef patch, int idx, FileHistory* fh) {
1882 QString data('>' + sha + 'X' + parents.join(" ") + " \n");
1883 data.append(author + '\n' + author + '\n' + date + '\n');
1884 data.append(log + '\n' + longLog);
1886 QString header("log size " + QString::number(QByteArray(data.toLatin1()).length() - 1) + '\n');
1887 data.prepend(header);
1888 if (!patch.isEmpty())
1889 data.append('\n' + patch);
1891 QByteArray* ba = new QByteArray(data.toLatin1());
1892 ba->append('\0');
1894 fh->rowData.append(ba);
1895 int dummy;
1896 Rev* c = new Rev(*ba, 0, idx, &dummy, !isMainHistory(fh));
1897 return c;
1900 const Rev* Git::fakeWorkDirRev(SCRef parent, SCRef log, SCRef longLog, int idx, FileHistory* fh) {
1902 QString patch;
1903 if (!isMainHistory(fh))
1904 patch = getWorkDirDiff(fh->fileNames().first());
1906 QString date(QString::number(QDateTime::currentDateTime().toTime_t()));
1907 QString author("-");
1908 QStringList parents(parent);
1909 Rev* c = fakeRevData(ZERO_SHA, parents, author, date, log, longLog, patch, idx, fh);
1910 c->isDiffCache = true;
1911 c->lanes.append(EMPTY);
1912 return c;
1915 const RevFile* Git::fakeWorkDirRevFile(const WorkingDirInfo& wd) {
1917 FileNamesLoader fl;
1918 RevFile* rf = new RevFile();
1919 parseDiffFormat(*rf, wd.diffIndex, fl);
1920 rf->onlyModified = false;
1922 FOREACH_SL (it, wd.otherFiles) {
1924 appendFileName(*rf, *it, fl);
1925 rf->status.append(RevFile::UNKNOWN);
1926 rf->mergeParent.append(1);
1928 RevFile cachedFiles;
1929 parseDiffFormat(cachedFiles, wd.diffIndexCached, fl);
1930 flushFileNames(fl);
1932 for (int i = 0; i < rf->count(); i++)
1933 if (findFileIndex(cachedFiles, filePath(*rf, i)) != -1)
1934 rf->status[i] |= RevFile::IN_INDEX;
1935 return rf;
1938 void Git::getDiffIndex() {
1940 QString status;
1941 if (!run("git status", &status)) // git status refreshes the index, run as first
1942 return;
1944 QString head;
1945 if (!run("git rev-parse --revs-only HEAD", &head))
1946 return;
1948 head = head.trimmed();
1949 if (!head.isEmpty()) { // repository initialized but still no history
1951 if (!run("git diff-index " + head, &workingDirInfo.diffIndex))
1952 return;
1954 // check for files already updated in cache, we will
1955 // save this information in status third field
1956 if (!run("git diff-index --cached " + head, &workingDirInfo.diffIndexCached))
1957 return;
1959 // get any file not in tree
1960 workingDirInfo.otherFiles = getOthersFiles();
1962 // now mockup a RevFile
1963 revsFiles.insert(ZERO_SHA_RAW, fakeWorkDirRevFile(workingDirInfo));
1965 // then mockup the corresponding Rev
1966 SCRef log = (isNothingToCommit() ? "Nothing to commit" : "Working directory changes");
1967 const Rev* r = fakeWorkDirRev(head, log, status, revData->revOrder.count(), revData);
1968 revData->revs.insert(ZERO_SHA_RAW, r);
1969 revData->revOrder.append(ZERO_SHA_RAW);
1970 revData->earlyOutputCntBase = revData->revOrder.count();
1972 // finally send it to GUI
1973 emit newRevsAdded(revData, revData->revOrder);
1976 void Git::parseDiffFormatLine(RevFile& rf, SCRef line, int parNum, FileNamesLoader& fl) {
1978 if (line[1] == ':') { // it's a combined merge
1980 /* For combined merges rename/copy information is useless
1981 * because nor the original file name, nor similarity info
1982 * is given, just the status tracks that in the left/right
1983 * branch a renamed/copy occurred (as example status could
1984 * be RM or MR). For visualization purposes we could consider
1985 * the file as modified
1987 appendFileName(rf, line.section('\t', -1), fl);
1988 setStatus(rf, "M");
1989 rf.mergeParent.append(parNum);
1990 } else { // faster parsing in normal case
1992 if (line.at(98) == '\t') {
1993 appendFileName(rf, line.mid(99), fl);
1994 setStatus(rf, line.at(97));
1995 rf.mergeParent.append(parNum);
1996 } else
1997 // it's a rename or a copy, we are not in fast path now!
1998 setExtStatus(rf, line.mid(97), parNum, fl);
2002 //CT TODO can go in RevFile
2003 void Git::setStatus(RevFile& rf, SCRef rowSt) {
2005 char status = rowSt.at(0).toLatin1();
2006 switch (status) {
2007 case 'M':
2008 case 'T':
2009 case 'U':
2010 rf.status.append(RevFile::MODIFIED);
2011 break;
2012 case 'D':
2013 rf.status.append(RevFile::DELETED);
2014 rf.onlyModified = false;
2015 break;
2016 case 'A':
2017 rf.status.append(RevFile::NEW);
2018 rf.onlyModified = false;
2019 break;
2020 case '?':
2021 rf.status.append(RevFile::UNKNOWN);
2022 rf.onlyModified = false;
2023 break;
2024 default:
2025 dbp("ASSERT in Git::setStatus, unknown status <%1>. "
2026 "'MODIFIED' will be used instead.", rowSt);
2027 rf.status.append(RevFile::MODIFIED);
2028 break;
2032 void Git::setExtStatus(RevFile& rf, SCRef rowSt, int parNum, FileNamesLoader& fl) {
2034 const QStringList sl(rowSt.split('\t', QString::SkipEmptyParts));
2035 if (sl.count() != 3) {
2036 dbp("ASSERT in setExtStatus, unexpected status string %1", rowSt);
2037 return;
2039 // we want store extra info with format "orig --> dest (Rxx%)"
2040 // but git give us something like "Rxx\t<orig>\t<dest>"
2041 SCRef type = sl[0];
2042 SCRef orig = sl[1];
2043 SCRef dest = sl[2];
2044 const QString extStatusInfo(orig + " --> " + dest + " (" + type + "%)");
2047 NOTE: we set rf.extStatus size equal to position of latest
2048 copied/renamed file. So it can have size lower then
2049 rf.count() if after copied/renamed file there are
2050 others. Here we have no possibility to know final
2051 dimension of this RefFile. We are still in parsing.
2054 // simulate new file
2055 appendFileName(rf, dest, fl);
2056 rf.mergeParent.append(parNum);
2057 rf.status.append(RevFile::NEW);
2058 rf.extStatus.resize(rf.status.size());
2059 rf.extStatus[rf.status.size() - 1] = extStatusInfo;
2061 // simulate deleted orig file only in case of rename
2062 if (type.at(0) == 'R') { // renamed file
2063 appendFileName(rf, orig, fl);
2064 rf.mergeParent.append(parNum);
2065 rf.status.append(RevFile::DELETED);
2066 rf.extStatus.resize(rf.status.size());
2067 rf.extStatus[rf.status.size() - 1] = extStatusInfo;
2069 rf.onlyModified = false;
2072 //CT TODO utility function; can go elsewhere
2073 void Git::parseDiffFormat(RevFile& rf, SCRef buf, FileNamesLoader& fl) {
2075 int parNum = 1, startPos = 0, endPos = buf.indexOf('\n');
2076 while (endPos != -1) {
2078 SCRef line = buf.mid(startPos, endPos - startPos);
2079 if (line[0] == ':') // avoid sha's in merges output
2080 parseDiffFormatLine(rf, line, parNum, fl);
2081 else
2082 parNum++;
2084 startPos = endPos + 1;
2085 endPos = buf.indexOf('\n', endPos + 99);
2089 bool Git::startParseProc(SCList initCmd, FileHistory* fh, SCRef buf) {
2091 DataLoader* dl = new DataLoader(this, fh); // auto-deleted when done
2093 connect(this, SIGNAL(cancelLoading(const FileHistory*)),
2094 dl, SLOT(on_cancel(const FileHistory*)));
2096 connect(dl, SIGNAL(newDataReady(const FileHistory*)),
2097 this, SLOT(on_newDataReady(const FileHistory*)));
2099 connect(dl, SIGNAL(loaded(FileHistory*, ulong, int,
2100 bool, const QString&, const QString&)), this,
2101 SLOT(on_loaded(FileHistory*, ulong, int,
2102 bool, const QString&, const QString&)));
2104 return dl->start(initCmd, workDir, buf);
2107 bool Git::startRevList(SCList args, FileHistory* fh) {
2109 QString baseCmd("git log --topo-order --no-color "
2111 #ifndef Q_OS_WIN32
2112 "--log-size " // FIXME broken on Windows
2113 #endif
2114 "--parents --boundary -z "
2115 "--pretty=format:%m%HX%PX%n%cn<%ce>%n%an<%ae>%n%at%n%s%n");
2117 // we don't need log message body for file history
2118 if (isMainHistory(fh))
2119 baseCmd.append("%b");
2121 QStringList initCmd(baseCmd.split(' '));
2122 if (!isMainHistory(fh)) {
2124 NOTE: we don't use '--remove-empty' option because
2125 in case a file is deleted and then a new file with
2126 the same name is created again in the same directory
2127 then, with this option, file history is truncated to
2128 the file deletion revision.
2130 initCmd << QString("-r -m -p --full-index").split(' ');
2131 } else
2132 {} // initCmd << QString("--early-output"); currently disabled
2134 return startParseProc(initCmd + args, fh, QString());
2137 bool Git::startUnappliedList() {
2139 QStringList unAppliedShaList(getAllRefSha(UN_APPLIED));
2140 if (unAppliedShaList.isEmpty())
2141 return false;
2143 // WARNING: with this command 'git log' could send spurious
2144 // revs so we need some filter out logic during loading
2145 QString cmd("git log --no-color --parents -z "
2147 #ifndef Q_OS_WIN32
2148 "--log-size " // FIXME broken on Windows
2149 #endif
2150 "--pretty=format:%m%HX%PX%n%an<%ae>%n%at%n%s%n%b ^HEAD");
2152 QStringList sl(cmd.split(' '));
2153 sl << unAppliedShaList;
2154 return startParseProc(sl, revData, QString());
2157 void Git::stop(bool saveCache) {
2158 // normally called when changing directory or closing
2160 EM_RAISE(exGitStopped);
2162 // stop all data sending from process and asks them
2163 // to terminate. Note that process could still keep
2164 // running for a while although silently
2165 emit cancelAllProcesses(); // non blocking
2167 // after cancelAllProcesses() procFinished() is not called anymore
2168 // TODO perhaps is better to call procFinished() also if process terminated
2169 // incorrectly as QProcess does. BUt first we need to fix FileView::on_loadCompleted()
2170 emit fileNamesLoad(1, revsFiles.count() - filesLoadingStartOfs);
2172 if (cacheNeedsUpdate && saveCache) {
2174 cacheNeedsUpdate = false;
2175 if (!filesLoadingCurSha.isEmpty()) // we are in the middle of a loading
2176 revsFiles.remove(toTempSha(filesLoadingCurSha)); // remove partial data
2178 if (!revsFiles.isEmpty()) {
2179 SHOW_MSG("Saving cache. Please wait...");
2180 if (!Cache::save(gitDir, revsFiles, dirNamesVec, fileNamesVec))
2181 dbs("ERROR unable to save file names cache");
2186 void Git::clearRevs() {
2188 revData->clear();
2189 patchesStillToFind = 0; // TODO TEST WITH FILTERING
2190 firstNonStGitPatch = "";
2191 workingDirInfo.clear();
2192 revsFiles.remove(ZERO_SHA_RAW);
2195 void Git::clearFileNames() {
2197 qDeleteAll(revsFiles);
2198 revsFiles.clear();
2199 fileNamesMap.clear();
2200 dirNamesMap.clear();
2201 dirNamesVec.clear();
2202 fileNamesVec.clear();
2203 revsFilesShaBackupBuf.clear();
2204 cacheNeedsUpdate = false;
2207 bool Git::init(SCRef wd, bool askForRange, const QStringList* passedArgs, bool overwriteArgs, bool* quit) {
2208 // normally called when changing git directory. Must be called after stop()
2210 *quit = false;
2211 clearRevs();
2213 /* we only update filtering info here, original arguments
2214 * are not overwritten. Only getArgs() can update arguments,
2215 * an exception is if flag overwriteArgs is set
2217 loadArguments.filteredLoading = (!overwriteArgs && passedArgs != NULL);
2218 if (loadArguments.filteredLoading)
2219 loadArguments.filterList = *passedArgs;
2221 if (overwriteArgs) // in this case must be passedArgs != NULL
2222 loadArguments.args = *passedArgs;
2224 try {
2225 setThrowOnStop(true);
2227 const QString msg1("Path is '" + workDir + "' Loading ");
2229 // check if repository is valid
2230 bool repoChanged;
2231 isGIT = getGitDBDir(wd, gitDir, repoChanged);
2233 if (repoChanged) {
2234 bool dummy;
2235 getBaseDir(wd, workDir, dummy);
2236 localDates.clear();
2237 clearFileNames();
2238 fileCacheAccessed = false;
2240 SHOW_MSG(msg1 + "file names cache...");
2241 loadFileCache();
2242 SHOW_MSG("");
2244 if (!isGIT) {
2245 setThrowOnStop(false);
2246 return false;
2248 if (!passedArgs) {
2250 // update text codec according to repo settings
2251 // bool dummy;
2252 // QTextCodec::setCodecForCStrings(getTextCodec(&dummy));
2254 // load references
2255 SHOW_MSG(msg1 + "refs...");
2256 if (!getRefs())
2257 dbs("WARNING: no tags or heads found");
2259 // startup input range dialog
2260 SHOW_MSG("");
2261 if (startup || askForRange) {
2262 loadArguments.args = getArgs(quit, repoChanged); // must be called with refs loaded
2263 if (*quit) {
2264 setThrowOnStop(false);
2265 return false;
2268 // load StGit unapplied patches, must be after getRefs()
2269 if (isStGIT) {
2270 loadingUnAppliedPatches = startUnappliedList();
2271 if (loadingUnAppliedPatches) {
2273 SHOW_MSG(msg1 + "StGIT unapplied patches...");
2274 setThrowOnStop(false);
2276 // we will continue with init2() at
2277 // the end of loading...
2278 return true;
2282 init2();
2283 setThrowOnStop(false);
2284 return true;
2286 } catch (int i) {
2288 setThrowOnStop(false);
2290 if (isThrowOnStopRaised(i, "initializing 1")) {
2291 EM_THROW_PENDING;
2292 return false;
2294 const QString info("Exception \'" + EM_DESC(i) + "\' "
2295 "not handled in init...re-throw");
2296 dbs(info);
2297 throw;
2301 void Git::init2() {
2303 const QString msg1("Path is '" + workDir + "' Loading ");
2305 // after loading unapplied patch update base early output offset to
2306 // avoid losing unapplied patches at first early output event
2307 if (isStGIT)
2308 revData->earlyOutputCntBase = revData->revOrder.count();
2310 try {
2311 setThrowOnStop(true);
2313 // load working directory files
2314 if (!loadArguments.filteredLoading && testFlag(DIFF_INDEX_F)) {
2315 SHOW_MSG(msg1 + "working directory changed files...");
2316 getDiffIndex(); // blocking, we could be in setRepository() now
2318 SHOW_MSG(msg1 + "revisions...");
2320 // build up command line arguments
2321 QStringList args(loadArguments.args);
2322 if (loadArguments.filteredLoading) {
2323 if (!args.contains("--"))
2324 args << "--";
2326 args << loadArguments.filterList;
2328 if (!startRevList(args, revData))
2329 SHOW_MSG("ERROR: unable to start 'git log'");
2331 setThrowOnStop(false);
2333 } catch (int i) {
2335 setThrowOnStop(false);
2337 if (isThrowOnStopRaised(i, "initializing 2")) {
2338 EM_THROW_PENDING;
2339 return;
2341 const QString info("Exception \'" + EM_DESC(i) + "\' "
2342 "not handled in init2...re-throw");
2343 dbs(info);
2344 throw;
2348 void Git::on_newDataReady(const FileHistory* fh) {
2350 emit newRevsAdded(fh , fh->revOrder);
2353 void Git::on_loaded(FileHistory* fh, ulong byteSize, int loadTime,
2354 bool normalExit, SCRef cmd, SCRef errorDesc) {
2356 if (!errorDesc.isEmpty()) {
2357 MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc);
2358 QApplication::postEvent(parent(), e);
2360 if (normalExit) { // do not send anything if killed
2362 on_newDataReady(fh);
2364 if (!loadingUnAppliedPatches) {
2366 fh->loadTime += loadTime;
2368 ulong kb = byteSize / 1024;
2369 double mbs = (double)byteSize / fh->loadTime / 1000;
2370 QString tmp;
2371 tmp.sprintf("Loaded %i revisions (%li KB), "
2372 "time elapsed: %i ms (%.2f MB/s)",
2373 fh->revs.count(), kb, fh->loadTime, mbs);
2375 if (!tryFollowRenames(fh))
2376 emit loadCompleted(fh, tmp);
2378 if (isMainHistory(fh))
2379 // wait the dust to settle down before to start
2380 // background file names loading for new revisions
2381 QTimer::singleShot(500, this, SLOT(loadFileNames()));
2384 if (loadingUnAppliedPatches) {
2385 loadingUnAppliedPatches = false;
2386 revData->lns->clear(); // again to reset lanes
2387 init2(); // continue with loading of remaining revisions
2391 bool Git::tryFollowRenames(FileHistory* fh) {
2393 if (isMainHistory(fh))
2394 return false;
2396 QStringList oldNames;
2397 QMutableStringListIterator it(fh->renamedRevs);
2398 while (it.hasNext())
2399 if (!populateRenamedPatches(it.next(), fh->curFNames, fh, &oldNames, false))
2400 it.remove();
2402 if (fh->renamedRevs.isEmpty())
2403 return false;
2405 QStringList args;
2406 args << fh->renamedRevs << "--" << oldNames;
2407 fh->fNames << oldNames;
2408 fh->curFNames = oldNames;
2409 fh->renamedRevs.clear();
2410 return startRevList(args, fh);
2413 bool Git::populateRenamedPatches(SCRef renamedSha, SCList newNames, FileHistory* fh,
2414 QStringList* oldNames, bool backTrack) {
2416 QString runOutput;
2417 if (!run("git diff-tree -r -M " + renamedSha, &runOutput))
2418 return false;
2420 // find the first renamed file with the new file name in renamedFiles list
2421 QString line;
2422 FOREACH_SL (it, newNames) {
2423 if (backTrack) {
2424 line = runOutput.section('\t' + *it + '\t', 0, 0,
2425 QString::SectionIncludeTrailingSep);
2426 line.chop(1);
2427 } else
2428 line = runOutput.section('\t' + *it + '\n', 0, 0);
2430 if (!line.isEmpty())
2431 break;
2433 if (line.contains('\n'))
2434 line = line.section('\n', -1, -1);
2436 SCRef status = line.section('\t', -2, -2).section(' ', -1, -1);
2437 if (!status.startsWith('R'))
2438 return false;
2440 if (backTrack) {
2441 SCRef nextFile = runOutput.section(line, 1, 1).section('\t', 1, 1);
2442 oldNames->append(nextFile.section('\n', 0, 0));
2443 return true;
2445 // get the diff betwen two files
2446 SCRef prevFileSha = line.section(' ', 2, 2);
2447 SCRef lastFileSha = line.section(' ', 3, 3);
2448 if (prevFileSha == lastFileSha) // just renamed
2449 runOutput.clear();
2450 else if (!run("git diff --no-ext-diff -r --full-index " + prevFileSha + " " + lastFileSha, &runOutput))
2451 return false;
2453 SCRef prevFile = line.section('\t', -1, -1);
2454 if (!oldNames->contains(prevFile))
2455 oldNames->append(prevFile);
2457 // save the patch, will be used later to create a
2458 // proper graft sha with correct parent info
2459 if (fh) {
2460 QString tmp(!runOutput.isEmpty() ? runOutput : "diff --no-ext-diff --\nsimilarity index 100%\n");
2461 fh->renamedPatches.insert(renamedSha, tmp);
2463 return true;
2466 void Git::populateFileNamesMap() {
2468 for (int i = 0; i < dirNamesVec.count(); ++i)
2469 dirNamesMap.insert(dirNamesVec[i], i);
2471 for (int i = 0; i < fileNamesVec.count(); ++i)
2472 fileNamesMap.insert(fileNamesVec[i], i);
2475 void Git::loadFileCache() {
2477 if (!fileCacheAccessed) {
2479 fileCacheAccessed = true;
2480 QByteArray shaBuf;
2481 if (Cache::load(gitDir, revsFiles, dirNamesVec, fileNamesVec, shaBuf)) {
2482 revsFilesShaBackupBuf.append(shaBuf);
2483 populateFileNamesMap();
2484 } else
2485 dbs("ERROR: unable to load file names cache");
2489 void Git::loadFileNames() {
2491 indexTree(); // we are sure data loading is finished at this point
2493 int revCnt = 0;
2494 QString diffTreeBuf;
2495 FOREACH (ShaVect, it, revData->revOrder) {
2497 if (!revsFiles.contains(*it)) {
2498 const Rev* c = revLookup(*it);
2499 if (c->parentsCount() == 1) { // skip initials and merges
2500 diffTreeBuf.append(*it).append('\n');
2501 revCnt++;
2505 if (!diffTreeBuf.isEmpty()) {
2506 filesLoadingPending = filesLoadingCurSha = "";
2507 filesLoadingStartOfs = revsFiles.count();
2508 emit fileNamesLoad(3, revCnt);
2510 const QString runCmd("git diff-tree --no-color -r -C --stdin");
2511 runAsync(runCmd, this, diffTreeBuf);
2515 bool Git::filterEarlyOutputRev(FileHistory* fh, Rev* rev) {
2517 if (fh->earlyOutputCnt < fh->revOrder.count()) {
2519 const ShaString& sha = fh->revOrder[fh->earlyOutputCnt++];
2520 const Rev* c = revLookup(sha, fh);
2521 if (c) {
2522 if (rev->sha() != sha || rev->parents() != c->parents()) {
2523 // mismatch found! set correct value, 'rev' will
2524 // overwrite 'c' upon returning
2525 rev->orderIdx = c->orderIdx;
2526 revData->clear(false); // flush the tail
2527 } else
2528 return true; // filter out 'rev'
2531 // we have new revisions, exit from early output state
2532 fh->setEarlyOutputState(false);
2533 return false;
2536 int Git::addChunk(FileHistory* fh, const QByteArray& ba, int start) {
2538 RevMap& r = fh->revs;
2539 int nextStart;
2540 Rev* rev;
2542 do {
2543 // only here we create a new rev
2544 rev = new Rev(ba, start, fh->revOrder.count(), &nextStart, !isMainHistory(fh));
2546 if (nextStart == -2) {
2547 delete rev;
2548 fh->setEarlyOutputState(true);
2549 start = ba.indexOf('\n', start) + 1;
2552 } while (nextStart == -2);
2554 if (nextStart == -1) { // half chunk detected
2555 delete rev;
2556 return -1;
2559 const ShaString& sha = rev->sha();
2561 if (fh->earlyOutputCnt != -1 && filterEarlyOutputRev(fh, rev)) {
2562 delete rev;
2563 return nextStart;
2566 if (isStGIT) {
2567 if (loadingUnAppliedPatches) { // filter out possible spurious revs
2569 Reference* rf = lookupReference(sha);
2570 if (!(rf && (rf->type & UN_APPLIED))) {
2571 delete rev;
2572 return nextStart;
2575 // remove StGIT spurious revs filter
2576 if (!firstNonStGitPatch.isEmpty() && firstNonStGitPatch == sha)
2577 firstNonStGitPatch = "";
2579 // StGIT called with --all option creates spurious revs so filter
2580 // out unknown revs until no more StGIT patches are waited and
2581 // firstNonStGitPatch is reached
2582 if (!(firstNonStGitPatch.isEmpty() && patchesStillToFind == 0) &&
2583 !loadingUnAppliedPatches && isMainHistory(fh)) {
2585 Reference* rf = lookupReference(sha);
2586 if (!(rf && (rf->type & APPLIED))) {
2587 delete rev;
2588 return nextStart;
2591 if (r.contains(sha)) {
2592 // StGIT unapplied patches could be sent again by
2593 // 'git log' as example if called with --all option.
2594 if (r[sha]->isUnApplied) {
2595 delete rev;
2596 return nextStart;
2598 // could be a side effect of 'git log -m', see below
2599 if (isMainHistory(fh) || rev->parentsCount() < 2)
2600 dbp("ASSERT: addChunk sha <%1> already received", sha);
2603 if (r.isEmpty() && !isMainHistory(fh)) {
2604 bool added = copyDiffIndex(fh, sha);
2605 rev->orderIdx = added ? 1 : 0;
2607 if ( !isMainHistory(fh)
2608 && !fh->renamedPatches.isEmpty()
2609 && fh->renamedPatches.contains(sha)) {
2611 // this is the new rev with renamed file, the rev is correct but
2612 // the patch, create a new rev with proper patch and use that instead
2613 const Rev* prevSha = revLookup(sha, fh);
2614 Rev* c = fakeRevData(sha, rev->parents(), rev->author(),
2615 rev->authorDate(), rev->shortLog(), rev->longLog(),
2616 fh->renamedPatches[sha], prevSha->orderIdx, fh);
2618 r.insert(sha, c); // overwrite old content
2619 fh->renamedPatches.remove(sha);
2620 return nextStart;
2622 if (!isMainHistory(fh) && rev->parentsCount() > 1 && r.contains(sha)) {
2623 /* In this case git log is called with -m option and merges are splitted
2624 in one commit per parent but all them have the same sha.
2625 So we add only the first to fh->revOrder to display history correctly,
2626 but we nevertheless add all the commits to 'r' so that annotation code
2627 can get the patches.
2629 QString mergeSha;
2630 int i = 0;
2632 mergeSha = QString::number(++i) + " m " + sha;
2633 while (r.contains(toTempSha(mergeSha)));
2635 const ShaString& ss = toPersistentSha(mergeSha, shaBackupBuf);
2636 r.insert(ss, rev);
2637 } else {
2638 r.insert(sha, rev);
2639 fh->revOrder.append(sha);
2641 if (rev->parentsCount() == 0 && !isMainHistory(fh))
2642 fh->renamedRevs.append(sha);
2644 if (isStGIT) {
2645 // updateLanes() is called too late, after loadingUnAppliedPatches
2646 // has been reset so update the lanes now.
2647 if (loadingUnAppliedPatches) {
2649 Rev* c = const_cast<Rev*>(revLookup(sha, fh));
2650 c->isUnApplied = true;
2651 c->lanes.append(UNAPPLIED);
2653 } else if (patchesStillToFind > 0 || !isMainHistory(fh)) { // try to avoid costly lookup
2655 Reference* rf = lookupReference(sha);
2656 if (rf && (rf->type & APPLIED)) {
2658 Rev* c = const_cast<Rev*>(revLookup(sha, fh));
2659 c->isApplied = true;
2660 if (isMainHistory(fh)) {
2661 patchesStillToFind--;
2662 if (patchesStillToFind == 0)
2663 // any rev will be discarded until
2664 // firstNonStGitPatch arrives
2665 firstNonStGitPatch = c->parent(0);
2670 return nextStart;
2673 bool Git::copyDiffIndex(FileHistory* fh, SCRef parent) {
2674 // must be called with empty revs and empty revOrder
2676 if (!fh->revOrder.isEmpty() || !fh->revs.isEmpty()) {
2677 dbs("ASSERT in copyDiffIndex: called with wrong context");
2678 return false;
2680 const Rev* r = revLookup(ZERO_SHA);
2681 if (!r)
2682 return false;
2684 const RevFile* files = getFiles(ZERO_SHA);
2685 if (!files || findFileIndex(*files, fh->fileNames().first()) == -1)
2686 return false;
2688 // insert a custom ZERO_SHA rev with proper parent
2689 const Rev* rf = fakeWorkDirRev(parent, "Working directory changes", "long log\n", 0, fh);
2690 fh->revs.insert(ZERO_SHA_RAW, rf);
2691 fh->revOrder.append(ZERO_SHA_RAW);
2692 return true;
2695 void Git::setLane(SCRef sha, FileHistory* fh) {
2697 Lanes* l = fh->lns;
2698 uint i = fh->firstFreeLane;
2699 QVector<QByteArray> ba;
2700 const ShaString& ss = toPersistentSha(sha, ba);
2701 const ShaVect& shaVec(fh->revOrder);
2703 for (uint cnt = shaVec.count(); i < cnt; ++i) {
2705 const ShaString& curSha = shaVec[i];
2706 Rev* r = const_cast<Rev*>(revLookup(curSha, fh));
2707 if (r->lanes.count() == 0)
2708 updateLanes(*r, *l, curSha);
2710 if (curSha == ss)
2711 break;
2713 fh->firstFreeLane = ++i;
2716 void Git::updateLanes(Rev& c, Lanes& lns, SCRef sha) {
2717 // we could get third argument from c.sha(), but we are in fast path here
2718 // and c.sha() involves a deep copy, so we accept a little redundancy
2720 if (lns.isEmpty())
2721 lns.init(sha);
2723 bool isDiscontinuity;
2724 bool isFork = lns.isFork(sha, isDiscontinuity);
2725 bool isMerge = (c.parentsCount() > 1);
2726 bool isInitial = (c.parentsCount() == 0);
2728 if (isDiscontinuity)
2729 lns.changeActiveLane(sha); // uses previous isBoundary state
2731 lns.setBoundary(c.isBoundary()); // update must be here
2733 if (isFork)
2734 lns.setFork(sha);
2735 if (isMerge)
2736 lns.setMerge(c.parents());
2737 if (c.isApplied)
2738 lns.setApplied();
2739 if (isInitial)
2740 lns.setInitial();
2742 lns.getLanes(c.lanes); // here lanes are snapshotted
2744 SCRef nextSha = (isInitial) ? "" : QString(c.parent(0));
2746 lns.nextParent(nextSha);
2748 if (c.isApplied)
2749 lns.afterApplied();
2750 if (isMerge)
2751 lns.afterMerge();
2752 if (isFork)
2753 lns.afterFork();
2754 if (lns.isBranch())
2755 lns.afterBranch();
2757 // QString tmp = "", tmp2;
2758 // for (uint i = 0; i < c.lanes.count(); i++) {
2759 // tmp2.setNum(c.lanes[i]);
2760 // tmp.append(tmp2 + "-");
2761 // }
2762 // qDebug("%s %s", tmp.toUtf8().data(), sha.toUtf8().data());
2765 void Git::procFinished() {
2767 flushFileNames(fileLoader);
2768 filesLoadingPending = filesLoadingCurSha = "";
2769 emit fileNamesLoad(1, revsFiles.count() - filesLoadingStartOfs);
2772 void Git::procReadyRead(const QByteArray& fileChunk) {
2774 if (filesLoadingPending.isEmpty())
2775 filesLoadingPending = fileChunk;
2776 else
2777 filesLoadingPending.append(fileChunk); // add to previous half lines
2779 RevFile* rf = NULL;
2780 if (!filesLoadingCurSha.isEmpty() && revsFiles.contains(toTempSha(filesLoadingCurSha)))
2781 rf = const_cast<RevFile*>(revsFiles[toTempSha(filesLoadingCurSha)]);
2783 int nextEOL = filesLoadingPending.indexOf('\n');
2784 int lastEOL = -1;
2785 while (nextEOL != -1) {
2787 SCRef line(filesLoadingPending.mid(lastEOL + 1, nextEOL - lastEOL - 1));
2788 if (line.at(0) != ':') {
2789 SCRef sha = line.left(40);
2790 if (!rf || sha != filesLoadingCurSha) { // new commit
2791 rf = new RevFile();
2792 revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf);
2793 filesLoadingCurSha = sha;
2794 cacheNeedsUpdate = true;
2795 } else
2796 dbp("ASSERT: repeated sha %1 in file names loading", sha);
2797 } else // line.constref(0) == ':'
2798 parseDiffFormatLine(*rf, line, 1, fileLoader);
2800 lastEOL = nextEOL;
2801 nextEOL = filesLoadingPending.indexOf('\n', lastEOL + 1);
2803 if (lastEOL != -1)
2804 filesLoadingPending.remove(0, lastEOL + 1);
2806 emit fileNamesLoad(2, revsFiles.count() - filesLoadingStartOfs);
2809 void Git::flushFileNames(FileNamesLoader& fl) {
2811 if (!fl.rf)
2812 return;
2814 QByteArray& b = fl.rf->pathsIdx;
2815 QVector<int>& dirs = fl.rfDirs;
2817 b.clear();
2818 b.resize(2 * dirs.size() * sizeof(int));
2820 int* d = (int*)(b.data());
2822 for (int i = 0; i < dirs.size(); i++) {
2824 d[i] = dirs.at(i);
2825 d[dirs.size() + i] = fl.rfNames.at(i);
2827 dirs.clear();
2828 fl.rfNames.clear();
2829 fl.rf = NULL;
2832 void Git::appendFileName(RevFile& rf, SCRef name, FileNamesLoader& fl) {
2834 if (fl.rf != &rf) {
2835 flushFileNames(fl);
2836 fl.rf = &rf;
2838 int idx = name.lastIndexOf('/') + 1;
2839 SCRef dr = name.left(idx);
2840 SCRef nm = name.mid(idx);
2842 QHash<QString, int>::const_iterator it(dirNamesMap.constFind(dr));
2843 if (it == dirNamesMap.constEnd()) {
2844 int idx = dirNamesVec.count();
2845 dirNamesMap.insert(dr, idx);
2846 dirNamesVec.append(dr);
2847 fl.rfDirs.append(idx);
2848 } else
2849 fl.rfDirs.append(*it);
2851 it = fileNamesMap.constFind(nm);
2852 if (it == fileNamesMap.constEnd()) {
2853 int idx = fileNamesVec.count();
2854 fileNamesMap.insert(nm, idx);
2855 fileNamesVec.append(nm);
2856 fl.rfNames.append(idx);
2857 } else
2858 fl.rfNames.append(*it);
2861 void Git::updateDescMap(const Rev* r,uint idx, QHash<QPair<uint, uint>, bool>& dm,
2862 QHash<uint, QVector<int> >& dv) {
2864 QVector<int> descVec;
2865 if (r->descRefsMaster != -1) {
2867 const Rev* tmp = revLookup(revData->revOrder[r->descRefsMaster]);
2868 const QVector<int>& nr = tmp->descRefs;
2870 for (int i = 0; i < nr.count(); i++) {
2872 if (!dv.contains(nr[i])) {
2873 dbp("ASSERT descendant for %1 not found", r->sha());
2874 return;
2876 const QVector<int>& dvv = dv[nr[i]];
2878 // copy the whole vector instead of each element
2879 // in the first iteration of the loop below
2880 descVec = dvv; // quick (shared) copy
2882 for (int y = 0; y < dvv.count(); y++) {
2884 uint v = (uint)dvv[y];
2885 QPair<uint, uint> key = qMakePair(idx, v);
2886 QPair<uint, uint> keyN = qMakePair(v, idx);
2887 dm.insert(key, true);
2888 dm.insert(keyN, false);
2890 // we don't want duplicated entry, otherwise 'dvv' grows
2891 // greatly in repos with many tagged development branches
2892 if (i > 0 && !descVec.contains(v)) // i > 0 is rare, no
2893 descVec.append(v); // need to optimize
2897 descVec.append(idx);
2898 dv.insert(idx, descVec);
2901 void Git::mergeBranches(Rev* p, const Rev* r) {
2903 int r_descBrnMaster = (checkRef(r->sha(), BRANCH | RMT_BRANCH) ? r->orderIdx : r->descBrnMaster);
2905 if (p->descBrnMaster == r_descBrnMaster || r_descBrnMaster == -1)
2906 return;
2908 // we want all the descendant branches, so just avoid duplicates
2909 const QVector<int>& src1 = revLookup(revData->revOrder[p->descBrnMaster])->descBranches;
2910 const QVector<int>& src2 = revLookup(revData->revOrder[r_descBrnMaster])->descBranches;
2911 QVector<int> dst(src1);
2912 for (int i = 0; i < src2.count(); i++)
2913 if (qFind(src1.constBegin(), src1.constEnd(), src2[i]) == src1.constEnd())
2914 dst.append(src2[i]);
2916 p->descBranches = dst;
2917 p->descBrnMaster = p->orderIdx;
2920 void Git::mergeNearTags(bool down, Rev* p, const Rev* r, const QHash<QPair<uint, uint>, bool>& dm) {
2922 bool isTag = checkRef(r->sha(), TAG);
2923 int r_descRefsMaster = isTag ? r->orderIdx : r->descRefsMaster;
2924 int r_ancRefsMaster = isTag ? r->orderIdx : r->ancRefsMaster;
2926 if (down && (p->descRefsMaster == r_descRefsMaster || r_descRefsMaster == -1))
2927 return;
2929 if (!down && (p->ancRefsMaster == r_ancRefsMaster || r_ancRefsMaster == -1))
2930 return;
2932 // we want the nearest tag only, so remove any tag
2933 // that is ancestor of any other tag in p U r
2934 const ShaVect& ro = revData->revOrder;
2935 const ShaString& sha1 = down ? ro[p->descRefsMaster] : ro[p->ancRefsMaster];
2936 const ShaString& sha2 = down ? ro[r_descRefsMaster] : ro[r_ancRefsMaster];
2937 const QVector<int>& src1 = down ? revLookup(sha1)->descRefs : revLookup(sha1)->ancRefs;
2938 const QVector<int>& src2 = down ? revLookup(sha2)->descRefs : revLookup(sha2)->ancRefs;
2939 QVector<int> dst(src1);
2941 for (int s2 = 0; s2 < src2.count(); s2++) {
2943 bool add = false;
2944 for (int s1 = 0; s1 < src1.count(); s1++) {
2946 if (src2[s2] == src1[s1]) {
2947 add = false;
2948 break;
2950 QPair<uint, uint> key = qMakePair((uint)src2[s2], (uint)src1[s1]);
2952 if (!dm.contains(key)) { // could be empty if all tags are independent
2953 add = true; // could be an independent path
2954 continue;
2956 add = (down && dm[key]) || (!down && !dm[key]);
2957 if (add)
2958 dst[s1] = -1; // mark for removing
2959 else
2960 break;
2962 if (add)
2963 dst.append(src2[s2]);
2965 QVector<int>& nearRefs = (down ? p->descRefs : p->ancRefs);
2966 int& nearRefsMaster = (down ? p->descRefsMaster : p->ancRefsMaster);
2968 nearRefs.clear();
2969 for (int s2 = 0; s2 < dst.count(); s2++)
2970 if (dst[s2] != -1)
2971 nearRefs.append(dst[s2]);
2973 nearRefsMaster = p->orderIdx;
2976 void Git::indexTree() {
2978 const ShaVect& ro = revData->revOrder;
2979 if (ro.count() == 0)
2980 return;
2982 // we keep the pairs(x, y). Value is true if x is
2983 // ancestor of y or false if y is ancestor of x
2984 QHash<QPair<uint, uint>, bool> descMap;
2985 QHash<uint, QVector<int> > descVect;
2987 // walk down the tree from latest to oldest,
2988 // compute children and nearest descendants
2989 for (uint i = 0, cnt = ro.count(); i < cnt; i++) {
2991 uint type = checkRef(ro[i]);
2992 bool isB = (type & (BRANCH | RMT_BRANCH));
2993 bool isT = (type & TAG);
2995 const Rev* r = revLookup(ro[i]);
2997 if (isB) {
2998 Rev* rr = const_cast<Rev*>(r);
2999 if (r->descBrnMaster != -1) {
3000 const ShaString& sha = ro[r->descBrnMaster];
3001 rr->descBranches = revLookup(sha)->descBranches;
3003 rr->descBranches.append(i);
3005 if (isT) {
3006 updateDescMap(r, i, descMap, descVect);
3007 Rev* rr = const_cast<Rev*>(r);
3008 rr->descRefs.clear();
3009 rr->descRefs.append(i);
3011 for (uint y = 0; y < r->parentsCount(); y++) {
3013 Rev* p = const_cast<Rev*>(revLookup(r->parent(y)));
3014 if (p) {
3015 p->children.append(i);
3017 if (p->descBrnMaster == -1)
3018 p->descBrnMaster = isB ? r->orderIdx : r->descBrnMaster;
3019 else
3020 mergeBranches(p, r);
3022 if (p->descRefsMaster == -1)
3023 p->descRefsMaster = isT ? r->orderIdx : r->descRefsMaster;
3024 else
3025 mergeNearTags(optGoDown, p, r, descMap);
3029 // walk backward through the tree and compute nearest tagged ancestors
3030 for (int i = ro.count() - 1; i >= 0; i--) {
3032 const Rev* r = revLookup(ro[i]);
3033 bool isTag = checkRef(ro[i], TAG);
3035 if (isTag) {
3036 Rev* rr = const_cast<Rev*>(r);
3037 rr->ancRefs.clear();
3038 rr->ancRefs.append(i);
3040 for (int y = 0; y < r->children.count(); y++) {
3042 Rev* c = const_cast<Rev*>(revLookup(ro[r->children[y]]));
3043 if (c) {
3044 if (c->ancRefsMaster == -1)
3045 c->ancRefsMaster = isTag ? r->orderIdx:r->ancRefsMaster;
3046 else
3047 mergeNearTags(!optGoDown, c, r, descMap);