fix error calculate length log for revision="working directory" when 'git status...
[qgit4/redivivus.git] / src / git.cpp
blobf6739d84cca90dbb81e478245e15c05c69447768
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.toAscii());
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;
977 text = Qt::escape(txt);
979 if (regExp.isEmpty())
980 return text;
982 SCRef startCol(QString::fromLatin1("<b><font color=\"red\">"));
983 SCRef endCol(QString::fromLatin1("</font></b>"));
984 int pos = 0;
985 while ((pos = text.indexOf(regExp, pos)) != -1) {
987 SCRef match(regExp.cap(0));
988 const QString coloredText(startCol + match + endCol);
989 text.replace(pos, match.length(), coloredText);
990 pos += coloredText.length();
992 return text;
995 //CT TODO utility function; can go elsewhere
996 const QString Git::formatList(SCList sl, SCRef name, bool inOneLine) {
998 if (sl.isEmpty())
999 return QString();
1001 QString ls = "<tr><td class='h'>" + name + "</td><td>";
1002 const QString joinStr = inOneLine ? ", " : "</td></tr>\n" + ls;
1003 ls += sl.join(joinStr);
1004 ls += "</td></tr>\n";
1005 return ls;
1008 const QString Git::getDesc(SCRef sha, QRegExp& shortLogRE, QRegExp& longLogRE,
1009 bool showHeader, FileHistory* fh) {
1011 if (sha.isEmpty())
1012 return "";
1014 const Rev* c = revLookup(sha, fh);
1015 if (!c) // sha of a not loaded revision, as
1016 return ""; // example asked from file history
1018 QString text;
1019 if (c->isDiffCache)
1020 text = Qt::convertFromPlainText(c->longLog());
1021 else {
1022 QTextStream ts(&text);
1023 ts << "<html><head><style type=\"text/css\">"
1024 "tr.head { background-color: " << QPalette().color(QPalette::Mid).name() << " }\n"
1025 "td.h { font-weight: bold; }\n"
1026 "table { background-color: " << QPalette().color(QPalette::Button).name() << "; }\n"
1027 "span.h { font-weight: bold; font-size: medium; }\n"
1028 "div.l { white-space: pre; "
1029 "font-family: " << TYPE_WRITER_FONT.family() << ";"
1030 "font-size: " << TYPE_WRITER_FONT.pointSize() << "pt;}\n"
1031 "</style></head><body><div class='t'>\n"
1032 "<table border=0 cellspacing=0 cellpadding=2>";
1034 ts << "<tr class='head'> <th></th> <th><span class='h'>"
1035 << colorMatch(c->shortLog(), shortLogRE)
1036 << "</span></th></tr>";
1038 if (showHeader) {
1039 if (c->committer() != c->author())
1040 ts << formatList(QStringList(Qt::escape(c->committer())), "Committer");
1041 ts << formatList(QStringList(Qt::escape(c->author())), "Author");
1042 ts << formatList(QStringList(getLocalDate(c->authorDate())), " Author date");
1044 if (c->isUnApplied || c->isApplied) {
1046 QStringList patches(getRefName(sha, APPLIED));
1047 patches += getRefName(sha, UN_APPLIED);
1048 ts << formatList(patches, "Patch");
1049 } else {
1050 ts << formatList(c->parents(), "Parent", false);
1051 ts << formatList(getChildren(sha), "Child", false);
1052 ts << formatList(getDescendantBranches(sha), "Branch", false);
1053 ts << formatList(getNearTags(!optGoDown, sha), "Follows");
1054 ts << formatList(getNearTags(optGoDown, sha), "Precedes");
1057 QString longLog(c->longLog());
1058 if (showHeader) {
1059 longLog.prepend(QString("\n") + c->shortLog() + "\n");
1062 QString log(colorMatch(longLog, longLogRE));
1063 log.replace("\n", "\n ").prepend('\n');
1064 ts << "</table></div><div class='l'>" << log << "</div></body></html>";
1066 // highlight SHA's
1068 // added to commit logs, we avoid to call git rev-parse for a possible abbreviated
1069 // sha if there isn't a leading trailing space or an open parenthesis and,
1070 // in that case, before the space must not be a ':' character.
1071 // It's an ugly heuristic, but seems to work in most cases.
1072 QRegExp reSHA("..[0-9a-f]{21,40}|[^:][\\s(][0-9a-f]{6,20}", Qt::CaseInsensitive);
1073 reSHA.setMinimal(false);
1074 int pos = 0;
1075 while ((pos = text.indexOf(reSHA, pos)) != -1) {
1077 SCRef ref = reSHA.cap(0).mid(2);
1078 const Rev* r = (ref.length() == 40 ? revLookup(ref) : revLookup(getRefSha(ref)));
1079 if (r && r->sha() != ZERO_SHA_RAW) {
1080 QString slog(r->shortLog());
1081 if (slog.isEmpty()) // very rare but possible
1082 slog = r->sha();
1083 if (slog.length() > 60)
1084 slog = slog.left(57).trimmed().append("...");
1086 slog = Qt::escape(slog);
1087 const QString link("<a href=\"" + r->sha() + "\">" + slog + "</a>");
1088 text.replace(pos + 2, ref.length(), link);
1089 pos += link.length();
1090 } else
1091 pos += reSHA.cap(0).length();
1093 return text;
1096 const RevFile* Git::insertNewFiles(SCRef sha, SCRef data) {
1098 /* we use an independent FileNamesLoader to avoid data
1099 * corruption if we are loading file names in background
1101 FileNamesLoader fl;
1103 RevFile* rf = new RevFile();
1104 parseDiffFormat(*rf, data, fl);
1105 flushFileNames(fl);
1107 revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf);
1108 return rf;
1111 bool Git::runDiffTreeWithRenameDetection(SCRef runCmd, QString* runOutput) {
1112 /* Under some cases git could warn out:
1114 "too many files, skipping inexact rename detection"
1116 So if this occurs fallback on NO rename detection.
1118 QString cmd(runCmd); // runCmd must be without -C option
1119 cmd.replace("git diff-tree", "git diff-tree -C");
1121 errorReportingEnabled = false;
1122 bool renameDetectionOk = run(cmd, runOutput);
1123 errorReportingEnabled = true;
1125 if (!renameDetectionOk) // retry without rename detection
1126 return run(runCmd, runOutput);
1128 return true;
1131 const RevFile* Git::getAllMergeFiles(const Rev* r) {
1133 SCRef mySha(ALL_MERGE_FILES + r->sha());
1134 if (revsFiles.contains(toTempSha(mySha)))
1135 return revsFiles[toTempSha(mySha)];
1137 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1139 QString runCmd("git diff-tree --no-color -r -m " + r->sha()), runOutput;
1140 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1141 return NULL;
1143 return insertNewFiles(mySha, runOutput);
1146 const RevFile* Git::getFiles(SCRef sha, SCRef diffToSha, bool allFiles, SCRef path) {
1148 const Rev* r = revLookup(sha);
1149 if (!r)
1150 return NULL;
1152 if (r->parentsCount() == 0) // skip initial rev
1153 return NULL;
1155 if (r->parentsCount() > 1 && diffToSha.isEmpty() && allFiles)
1156 return getAllMergeFiles(r);
1158 if (!diffToSha.isEmpty() && (sha != ZERO_SHA)) {
1160 QString runCmd("git diff-tree --no-color -r -m ");
1161 runCmd.append(diffToSha + " " + sha);
1162 if (!path.isEmpty())
1163 runCmd.append(" " + path);
1165 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1167 QString runOutput;
1168 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1169 return NULL;
1171 // we insert a dummy revision file object. It will be
1172 // overwritten at each request but we don't care.
1173 return insertNewFiles(CUSTOM_SHA, runOutput);
1175 if (revsFiles.contains(r->sha()))
1176 return revsFiles[r->sha()]; // ZERO_SHA search arrives here
1178 if (sha == ZERO_SHA) {
1179 dbs("ASSERT in Git::getFiles, ZERO_SHA not found");
1180 return NULL;
1183 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1185 QString runCmd("git diff-tree --no-color -r -c " + sha), runOutput;
1186 if (!runDiffTreeWithRenameDetection(runCmd, &runOutput))
1187 return NULL;
1189 if (revsFiles.contains(r->sha())) // has been created in the mean time?
1190 return revsFiles[r->sha()];
1192 cacheNeedsUpdate = true;
1193 return insertNewFiles(sha, runOutput);
1196 bool Git::startFileHistory(SCRef sha, SCRef startingFileName, FileHistory* fh) {
1198 QStringList args(getDescendantBranches(sha, true));
1199 if (args.isEmpty())
1200 args << "HEAD";
1202 QString newestFileName = getNewestFileName(args, startingFileName);
1203 fh->resetFileNames(newestFileName);
1205 args.clear(); // load history from all the branches
1206 args << getAllRefSha(BRANCH | RMT_BRANCH);
1208 args << "--" << newestFileName;
1209 return startRevList(args, fh);
1212 const QString Git::getNewestFileName(SCList branches, SCRef fileName) {
1214 QString curFileName(fileName), runOutput, args;
1215 while (true) {
1216 args = branches.join(" ") + " -- " + curFileName;
1217 if (!run("git ls-tree " + args, &runOutput))
1218 break;
1220 if (!runOutput.isEmpty())
1221 break;
1223 QString msg("Retrieving file renames, now at '" + curFileName + "'...");
1224 QApplication::postEvent(parent(), new MessageEvent(msg));
1225 EM_PROCESS_EVENTS_NO_INPUT;
1227 if (!run("git rev-list -n1 " + args, &runOutput))
1228 break;
1230 if (runOutput.isEmpty()) // try harder
1231 if (!run("git rev-list --full-history -n1 " + args, &runOutput))
1232 break;
1234 if (runOutput.isEmpty())
1235 break;
1237 SCRef sha = runOutput.trimmed();
1238 QStringList newCur;
1239 if (!populateRenamedPatches(sha, QStringList(curFileName), NULL, &newCur, true))
1240 break;
1242 curFileName = newCur.first();
1244 return curFileName;
1247 void Git::getFileFilter(SCRef path, ShaSet& shaSet) const {
1249 shaSet.clear();
1250 QRegExp rx(path, Qt::CaseInsensitive, QRegExp::Wildcard);
1251 FOREACH (ShaVect, it, revData->revOrder) {
1253 if (!revsFiles.contains(*it))
1254 continue;
1256 // case insensitive, wildcard search
1257 const RevFile* rf = revsFiles[*it];
1258 for (int i = 0; i < rf->count(); ++i)
1259 if (filePath(*rf, i).contains(rx)) {
1260 shaSet.insert(*it);
1261 break;
1266 bool Git::getPatchFilter(SCRef exp, bool isRegExp, ShaSet& shaSet) {
1268 shaSet.clear();
1269 QString buf;
1270 FOREACH (ShaVect, it, revData->revOrder)
1271 if (*it != ZERO_SHA_RAW)
1272 buf.append(*it).append('\n');
1274 if (buf.isEmpty())
1275 return true;
1277 EM_PROCESS_EVENTS; // 'git diff-tree' could be slow
1279 QString runCmd("git diff-tree --no-color -r -s --stdin "), runOutput;
1280 if (isRegExp)
1281 runCmd.append("--pickaxe-regex ");
1283 runCmd.append(quote("-S" + exp));
1284 if (!run(runCmd, &runOutput, NULL, buf))
1285 return false;
1287 const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts));
1288 FOREACH_SL (it, sl)
1289 shaSet.insert(*it);
1291 return true;
1294 bool Git::resetCommits(int parentDepth) {
1296 QString runCmd("git reset --soft HEAD~");
1297 runCmd.append(QString::number(parentDepth));
1298 return run(runCmd);
1301 bool Git::applyPatchFile(SCRef patchPath, bool fold, bool isDragDrop) {
1303 if (isStGIT) {
1304 if (fold) {
1305 bool ok = run("stg fold " + quote(patchPath)); // merge in working directory
1306 if (ok)
1307 ok = run("stg refresh"); // update top patch
1308 return ok;
1309 } else
1310 return run("stg import --mail " + quote(patchPath));
1312 QString runCmd("git am --utf8 --3way ");
1314 QSettings settings;
1315 const QString APOpt(settings.value(AM_P_OPT_KEY).toString());
1316 if (!APOpt.isEmpty())
1317 runCmd.append(APOpt.trimmed() + " ");
1319 if (isDragDrop)
1320 runCmd.append("--keep ");
1322 if (testFlag(SIGN_PATCH_F))
1323 runCmd.append("--signoff ");
1325 return run(runCmd + quote(patchPath));
1328 const QStringList Git::sortShaListByIndex(SCList shaList) {
1330 QStringList orderedShaList;
1331 FOREACH_SL (it, shaList)
1332 appendNamesWithId(orderedShaList, *it, QStringList(*it), true);
1334 orderedShaList.sort();
1335 QStringList::iterator itN(orderedShaList.begin());
1336 for ( ; itN != orderedShaList.end(); ++itN) // strip 'idx'
1337 (*itN) = (*itN).section(' ', -1, -1);
1339 return orderedShaList;
1342 bool Git::formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir) {
1344 bool remote = !remoteDir.isEmpty();
1345 QSettings settings;
1346 const QString FPOpt(settings.value(FMT_P_OPT_KEY).toString());
1348 QString runCmd("git format-patch --no-color");
1349 if (testFlag(NUMBERS_F) && !remote)
1350 runCmd.append(" -n");
1352 if (remote)
1353 runCmd.append(" --keep-subject");
1355 runCmd.append(" -o " + quote(dirPath));
1356 if (!FPOpt.isEmpty())
1357 runCmd.append(" " + FPOpt.trimmed());
1359 const QString tmp(workDir);
1360 if (remote)
1361 workDir = remoteDir; // run() uses workDir value
1363 // shaList is ordered by newest to oldest
1364 runCmd.append(" " + shaList.last());
1365 runCmd.append(QString::fromLatin1("^..") + shaList.first());
1366 bool ret = run(runCmd);
1367 workDir = tmp;
1368 return ret;
1371 const QStringList Git::getOtherFiles(SCList selFiles, bool onlyInIndex) {
1373 const RevFile* files = getFiles(ZERO_SHA); // files != NULL
1374 QStringList notSelFiles;
1375 for (int i = 0; i < files->count(); ++i) {
1376 SCRef fp = filePath(*files, i);
1377 if (selFiles.indexOf(fp) == -1) { // not selected...
1378 if (!onlyInIndex || files->statusCmp(i, RevFile::IN_INDEX))
1379 notSelFiles.append(fp);
1382 return notSelFiles;
1385 bool Git::updateIndex(SCList selFiles) {
1387 const RevFile* files = getFiles(ZERO_SHA); // files != NULL
1389 QStringList toAdd, toRemove;
1390 FOREACH_SL (it, selFiles) {
1391 int idx = findFileIndex(*files, *it);
1392 if (files->statusCmp(idx, RevFile::DELETED))
1393 toRemove << *it;
1394 else
1395 toAdd << *it;
1397 if (!toRemove.isEmpty() && !run("git rm --cached --ignore-unmatch -- " + quote(toRemove)))
1398 return false;
1400 if (!toAdd.isEmpty() && !run("git add -- " + quote(toAdd)))
1401 return false;
1403 return true;
1406 bool Git::commitFiles(SCList selFiles, SCRef msg, bool amend) {
1408 const QString msgFile(gitDir + "/qgit_cmt_msg.txt");
1409 if (!writeToFile(msgFile, msg)) // early skip
1410 return false;
1412 // add user selectable commit options
1413 QSettings settings;
1414 const QString CMArgs(settings.value(CMT_ARGS_KEY).toString());
1416 QString cmtOptions;
1417 if (!CMArgs.isEmpty())
1418 cmtOptions.append(" " + CMArgs);
1420 if (testFlag(SIGN_CMT_F))
1421 cmtOptions.append(" -s");
1423 if (testFlag(VERIFY_CMT_F))
1424 cmtOptions.append(" -v");
1426 if (amend)
1427 cmtOptions.append(" --amend");
1429 bool ret = false;
1431 // get not selected files but updated in index to restore at the end
1432 const QStringList notSel(getOtherFiles(selFiles, optOnlyInIndex));
1434 // call git reset to remove not selected files from index
1435 if (!notSel.empty() && !run("git reset -- " + quote(notSel)))
1436 goto fail;
1438 // update index with selected files
1439 if (!updateIndex(selFiles))
1440 goto fail;
1442 // now we can finally commit..
1443 if (!run("git commit" + cmtOptions + " -F " + quote(msgFile)))
1444 goto fail;
1446 // restore not selected files that were already in index
1447 if (!notSel.empty() && !updateIndex(notSel))
1448 goto fail;
1450 ret = true;
1451 fail:
1452 QDir dir(workDir);
1453 dir.remove(msgFile);
1454 return ret;
1457 bool Git::mkPatchFromWorkDir(SCRef msg, SCRef patchFile, SCList files) {
1459 /* unfortunately 'git diff' sees only files already
1460 * known to git or already in index, so update index first
1461 * to be sure also unknown files are correctly found
1463 if (!updateIndex(files))
1464 return false;
1466 QString runOutput;
1467 if (!run("git diff --no-ext-diff -C HEAD -- " + quote(files), &runOutput))
1468 return false;
1470 const QString patch("Subject: " + msg + "\n---\n" + runOutput);
1471 return writeToFile(patchFile, patch);
1474 bool Git::stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold) {
1476 /* Here the deal is to use 'stg import' and 'stg fold' to add a new
1477 * patch or refresh the current one respectively. Unfortunately refresh
1478 * does not work with partial selection of files and also does not take
1479 * patch message from a file that is needed to avoid artifacts with '\n'
1480 * and friends.
1482 * So steps are:
1484 * - Create a patch file with the changes you want to import/fold in StGit
1485 * - Stash working directory files because import/fold wants a clean directory
1486 * - Import/fold the patch
1487 * - Unstash and merge working directory modified files
1488 * - Restore index with not selected files
1491 /* Step 1: Create a patch file with the changes you want to import/fold */
1492 bool ret = false;
1493 const QString patchFile(gitDir + "/qgit_tmp_patch.txt");
1495 // in case we don't have files to restore we can shortcut various commands
1496 bool partialSelection = !getOtherFiles(selFiles, !optOnlyInIndex).isEmpty();
1498 // get not selected files but updated in index to restore at the end
1499 QStringList notSel;
1500 if (partialSelection) // otherwise notSel is for sure empty
1501 notSel = getOtherFiles(selFiles, optOnlyInIndex);
1503 // create a patch with diffs between working directory and HEAD
1504 if (!mkPatchFromWorkDir(msg, patchFile, selFiles))
1505 goto fail;
1507 /* Step 2: Stash working directory modified files */
1508 if (partialSelection) {
1509 errorReportingEnabled = false;
1510 run("git stash"); // unfortunately 'git stash' is noisy on stderr
1511 errorReportingEnabled = true;
1514 /* Step 3: Call stg import/fold */
1516 // setup a clean state
1517 if (!run("stg status --reset"))
1518 goto fail_and_unstash;
1520 if (fold) {
1521 // update patch message before to fold, note that
1522 // command 'stg edit' requires stg version 0.14 or later
1523 if (!msg.isEmpty() && !run("stg edit --message " + quote(msg.trimmed())))
1524 goto fail_and_unstash;
1526 if (!run("stg fold " + quote(patchFile)))
1527 goto fail_and_unstash;
1529 if (!run("stg refresh")) // refresh needed after fold
1530 goto fail_and_unstash;
1532 } else if (!run("stg import --mail --name " + quote(patchName) + " " + quote(patchFile)))
1533 goto fail_and_unstash;
1535 if (partialSelection) {
1537 /* Step 4: Unstash and merge working directory modified files */
1538 errorReportingEnabled = false;
1539 run("git stash pop"); // unfortunately 'git stash' is noisy on stderr
1540 errorReportingEnabled = true;
1542 /* Step 5: restore not selected files that were already in index */
1543 if (!notSel.empty() && !updateIndex(notSel))
1544 goto fail;
1547 ret = true;
1548 goto exit;
1550 fail_and_unstash:
1552 if (partialSelection) {
1553 run("git reset");
1554 errorReportingEnabled = false;
1555 run("git stash pop");
1556 errorReportingEnabled = true;
1558 fail:
1559 exit:
1560 QDir dir(workDir);
1561 dir.remove(patchFile);
1562 return ret;
1565 bool Git::makeBranch(SCRef sha, SCRef branchName) {
1567 return run("git branch " + branchName + " " + sha);
1570 bool Git::makeTag(SCRef sha, SCRef tagName, SCRef msg) {
1572 if (msg.isEmpty())
1573 return run("git tag " + tagName + " " + sha);
1575 return run("git tag -m \"" + msg + "\" " + tagName + " " + sha);
1578 bool Git::deleteTag(SCRef sha) {
1580 const QStringList tags(getRefName(sha, TAG));
1581 if (!tags.empty())
1582 return run("git tag -d " + tags.first()); // only one
1584 return false;
1587 bool Git::stgPush(SCRef sha) {
1589 const QStringList patch(getRefName(sha, UN_APPLIED));
1590 if (patch.count() != 1) {
1591 dbp("ASSERT in Git::stgPush, found %1 patches instead of 1", patch.count());
1592 return false;
1594 return run("stg push " + quote(patch.first()));
1597 bool Git::stgPop(SCRef sha) {
1599 const QStringList patch(getRefName(sha, APPLIED));
1600 if (patch.count() != 1) {
1601 dbp("ASSERT in Git::stgPop, found %1 patches instead of 1", patch.count());
1602 return false;
1604 return run("stg pop " + quote(patch));
1608 //! cache for dates conversion. Common among qgit windows
1609 static QHash<QString, QString> localDates;
1611 * Accesses a cache that avoids slow date calculation
1613 * @param gitDate
1614 * the reference from which we want to get the date
1616 * @return
1617 * human-readable date
1619 const QString Git::getLocalDate(SCRef gitDate) {
1620 QString localDate(localDates.value(gitDate));
1622 // cache miss
1623 if (localDate.isEmpty()) {
1624 static QDateTime d;
1625 d.setTime_t(gitDate.toUInt());
1626 localDate = d.toString(Qt::SystemLocaleShortDate);
1628 // save to cache
1629 localDates[gitDate] = localDate;
1632 return localDate;
1635 const QStringList Git::getArgs(bool* quit, bool repoChanged) {
1637 QString args;
1638 if (startup) {
1639 for (int i = 1; i < qApp->argc(); i++) {
1640 // in arguments with spaces double quotes
1641 // are stripped by Qt, so re-add them
1642 QString arg(qApp->argv()[i]);
1643 if (arg.contains(' '))
1644 arg.prepend('\"').append('\"');
1646 args.append(arg + ' ');
1649 if (testFlag(RANGE_SELECT_F) && (!startup || args.isEmpty())) {
1651 RangeSelectImpl rs((QWidget*)parent(), &args, repoChanged, this);
1652 *quit = (rs.exec() == QDialog::Rejected); // modal execution
1653 if (*quit)
1654 return QStringList();
1656 startup = false;
1657 return MyProcess::splitArgList(args);
1660 bool Git::getGitDBDir(SCRef wd, QString& gd, bool& changed) {
1661 // we could run from a subdirectory, so we need to get correct directories
1663 QString runOutput, tmp(workDir);
1664 workDir = wd;
1665 errorReportingEnabled = false;
1666 bool success = run("git rev-parse --git-dir", &runOutput); // run under newWorkDir
1667 errorReportingEnabled = true;
1668 workDir = tmp;
1669 runOutput = runOutput.trimmed();
1670 if (success) {
1671 // 'git rev-parse --git-dir' output could be a relative
1672 // to working directory (as ex .git) or an absolute path
1673 QDir d(runOutput.startsWith("/") ? runOutput : wd + "/" + runOutput);
1674 changed = (d.absolutePath() != gitDir);
1675 gd = d.absolutePath();
1677 return success;
1680 bool Git::getBaseDir(SCRef wd, QString& bd, bool& changed) {
1681 // we could run from a subdirectory, so we need to get correct directories
1683 // We use --show-cdup and not --git-dir for this, in order to take into account configurations
1684 // in which .git is indeed a "symlink", a text file containing the path of the actual .git database dir.
1685 // In that particular case, the parent directory of the one given by --git-dir is *not* necessarily
1686 // the base directory of the repository.
1688 QString runOutput, tmp(workDir);
1689 workDir = wd;
1690 errorReportingEnabled = false;
1691 bool success = run("git rev-parse --show-cdup", &runOutput); // run under newWorkDir
1692 errorReportingEnabled = true;
1693 workDir = tmp;
1694 runOutput = runOutput.trimmed();
1695 if (success) {
1696 // 'git rev-parse --show-cdup' is relative to working directory.
1697 QDir d(wd + "/" + runOutput);
1698 bd = d.absolutePath();
1699 changed = (bd != workDir);
1701 else {
1702 changed = true;
1703 bd = wd;
1705 return success;
1708 Git::Reference* Git::lookupOrAddReference(const ShaString& sha) {
1709 RefMap::iterator it(refsShaMap.find(sha));
1710 if (it == refsShaMap.end()) it = refsShaMap.insert(sha, Reference());
1711 return &(*it);
1714 Git::Reference* Git::lookupReference(const ShaString& sha) {
1715 RefMap::iterator it(refsShaMap.find(sha));
1716 if (it == refsShaMap.end()) return 0;
1717 return &(*it);
1720 bool Git::getRefs() {
1722 // check for a StGIT stack
1723 QDir d(gitDir);
1724 QString stgCurBranch;
1725 if (d.exists("patches")) { // early skip
1726 errorReportingEnabled = false;
1727 isStGIT = run("stg branch", &stgCurBranch); // slow command
1728 errorReportingEnabled = true;
1729 stgCurBranch = stgCurBranch.trimmed();
1730 } else
1731 isStGIT = false;
1733 // check for a merge and read current branch sha
1734 isMergeHead = d.exists("MERGE_HEAD");
1735 QString curBranchSHA, curBranchName;
1736 if (!run("git rev-parse --revs-only HEAD", &curBranchSHA))
1737 return false;
1739 if (!run("git branch", &curBranchName))
1740 return false;
1742 curBranchSHA = curBranchSHA.trimmed();
1743 curBranchName = curBranchName.prepend('\n').section("\n*", 1);
1744 curBranchName = curBranchName.section('\n', 0, 0).trimmed();
1746 // read refs, normally unsorted
1747 QString runOutput;
1748 if (!run("git show-ref -d", &runOutput))
1749 return false;
1751 refsShaMap.clear();
1752 shaBackupBuf.clear(); // revs are already empty now
1754 QString prevRefSha;
1755 QStringList patchNames, patchShas;
1756 const QStringList rLst(runOutput.split('\n', QString::SkipEmptyParts));
1757 FOREACH_SL (it, rLst) {
1759 SCRef revSha = (*it).left(40);
1760 SCRef refName = (*it).mid(41);
1762 if (refName.startsWith("refs/patches/")) {
1764 // save StGIT patch sha, to be used later
1765 SCRef patchesDir("refs/patches/" + stgCurBranch + "/");
1766 if (refName.startsWith(patchesDir)) {
1767 patchNames.append(refName.mid(patchesDir.length()));
1768 patchShas.append(revSha);
1770 // StGIT patches should not be added to refs,
1771 // but an applied StGIT patch could be also an head or
1772 // a tag in this case will be added in another loop cycle
1773 continue;
1775 // one rev could have many tags
1776 Reference* cur = lookupOrAddReference(toPersistentSha(revSha, shaBackupBuf));
1778 if (refName.startsWith("refs/tags/")) {
1780 if (refName.endsWith("^{}")) { // tag dereference
1782 // we assume that a tag dereference follows strictly
1783 // the corresponding tag object in rLst. So the
1784 // last added tag is a tag object, not a commit object
1785 cur->tags.append(refName.mid(10, refName.length() - 13));
1787 // store tag object. Will be used to fetching
1788 // tag message (if any) when necessary.
1789 cur->tagObj = prevRefSha;
1791 // tagObj must be removed from ref map
1792 if (!prevRefSha.isEmpty())
1793 refsShaMap.remove(toTempSha(prevRefSha));
1795 } else
1796 cur->tags.append(refName.mid(10));
1798 cur->type |= TAG;
1800 } else if (refName.startsWith("refs/heads/")) {
1802 cur->branches.append(refName.mid(11));
1803 cur->type |= BRANCH;
1804 if (curBranchSHA == revSha) {
1805 cur->type |= CUR_BRANCH;
1806 cur->currentBranch = curBranchName;
1808 } else if (refName.startsWith("refs/remotes/") && !refName.endsWith("HEAD")) {
1810 cur->remoteBranches.append(refName.mid(13));
1811 cur->type |= RMT_BRANCH;
1813 } else if (!refName.startsWith("refs/bases/") && !refName.endsWith("HEAD")) {
1815 cur->refs.append(refName);
1816 cur->type |= REF;
1818 prevRefSha = revSha;
1820 if (isStGIT && !patchNames.isEmpty())
1821 parseStGitPatches(patchNames, patchShas);
1823 return !refsShaMap.empty();
1826 void Git::parseStGitPatches(SCList patchNames, SCList patchShas) {
1828 patchesStillToFind = 0;
1830 // get patch names and status of current branch
1831 QString runOutput;
1832 if (!run("stg series", &runOutput))
1833 return;
1835 const QStringList pl(runOutput.split('\n', QString::SkipEmptyParts));
1836 FOREACH_SL (it, pl) {
1838 SCRef status = (*it).left(1);
1839 SCRef patchName = (*it).mid(2);
1841 bool applied = (status == "+" || status == ">");
1842 int pos = patchNames.indexOf(patchName);
1843 if (pos == -1) {
1844 dbp("ASSERT in Git::parseStGitPatches(), patch %1 "
1845 "not found in references list.", patchName);
1846 continue;
1848 const ShaString& ss = toPersistentSha(patchShas.at(pos), shaBackupBuf);
1849 Reference* cur = lookupOrAddReference(ss);
1850 cur->stgitPatch = patchName;
1851 cur->type |= (applied ? APPLIED : UN_APPLIED);
1853 if (applied)
1854 patchesStillToFind++;
1858 const QStringList Git::getOthersFiles() {
1859 // add files present in working directory but not in git archive
1861 QString runCmd("git ls-files --others");
1862 QSettings settings;
1863 QString exFile(settings.value(EX_KEY, EX_DEF).toString());
1864 if (!exFile.isEmpty()) {
1865 QString path = (exFile.startsWith("/")) ? exFile : workDir + "/" + exFile;
1866 if (QFile::exists(path))
1867 runCmd.append(" --exclude-from=" + quote(exFile));
1869 QString exPerDir(settings.value(EX_PER_DIR_KEY, EX_PER_DIR_DEF).toString());
1870 if (!exPerDir.isEmpty())
1871 runCmd.append(" --exclude-per-directory=" + quote(exPerDir));
1873 QString runOutput;
1874 run(runCmd, &runOutput);
1875 return runOutput.split('\n', QString::SkipEmptyParts);
1878 Rev* Git::fakeRevData(SCRef sha, SCList parents, SCRef author, SCRef date, SCRef log, SCRef longLog,
1879 SCRef patch, int idx, FileHistory* fh) {
1881 QString data('>' + sha + 'X' + parents.join(" ") + " \n");
1882 data.append(author + '\n' + author + '\n' + date + '\n');
1883 data.append(log + '\n' + longLog);
1885 QString header("log size " + QString::number(QByteArray(data.toAscii()).length() - 1) + '\n');
1886 data.prepend(header);
1887 if (!patch.isEmpty())
1888 data.append('\n' + patch);
1890 QByteArray* ba = new QByteArray(data.toAscii());
1891 ba->append('\0');
1893 fh->rowData.append(ba);
1894 int dummy;
1895 Rev* c = new Rev(*ba, 0, idx, &dummy, !isMainHistory(fh));
1896 return c;
1899 const Rev* Git::fakeWorkDirRev(SCRef parent, SCRef log, SCRef longLog, int idx, FileHistory* fh) {
1901 QString patch;
1902 if (!isMainHistory(fh))
1903 patch = getWorkDirDiff(fh->fileNames().first());
1905 QString date(QString::number(QDateTime::currentDateTime().toTime_t()));
1906 QString author("-");
1907 QStringList parents(parent);
1908 Rev* c = fakeRevData(ZERO_SHA, parents, author, date, log, longLog, patch, idx, fh);
1909 c->isDiffCache = true;
1910 c->lanes.append(EMPTY);
1911 return c;
1914 const RevFile* Git::fakeWorkDirRevFile(const WorkingDirInfo& wd) {
1916 FileNamesLoader fl;
1917 RevFile* rf = new RevFile();
1918 parseDiffFormat(*rf, wd.diffIndex, fl);
1919 rf->onlyModified = false;
1921 FOREACH_SL (it, wd.otherFiles) {
1923 appendFileName(*rf, *it, fl);
1924 rf->status.append(RevFile::UNKNOWN);
1925 rf->mergeParent.append(1);
1927 RevFile cachedFiles;
1928 parseDiffFormat(cachedFiles, wd.diffIndexCached, fl);
1929 flushFileNames(fl);
1931 for (int i = 0; i < rf->count(); i++)
1932 if (findFileIndex(cachedFiles, filePath(*rf, i)) != -1)
1933 rf->status[i] |= RevFile::IN_INDEX;
1934 return rf;
1937 void Git::getDiffIndex() {
1939 QString status;
1940 if (!run("git status", &status)) // git status refreshes the index, run as first
1941 return;
1943 QString head;
1944 if (!run("git rev-parse --revs-only HEAD", &head))
1945 return;
1947 head = head.trimmed();
1948 if (!head.isEmpty()) { // repository initialized but still no history
1950 if (!run("git diff-index " + head, &workingDirInfo.diffIndex))
1951 return;
1953 // check for files already updated in cache, we will
1954 // save this information in status third field
1955 if (!run("git diff-index --cached " + head, &workingDirInfo.diffIndexCached))
1956 return;
1958 // get any file not in tree
1959 workingDirInfo.otherFiles = getOthersFiles();
1961 // now mockup a RevFile
1962 revsFiles.insert(ZERO_SHA_RAW, fakeWorkDirRevFile(workingDirInfo));
1964 // then mockup the corresponding Rev
1965 SCRef log = (isNothingToCommit() ? "Nothing to commit" : "Working directory changes");
1966 const Rev* r = fakeWorkDirRev(head, log, status, revData->revOrder.count(), revData);
1967 revData->revs.insert(ZERO_SHA_RAW, r);
1968 revData->revOrder.append(ZERO_SHA_RAW);
1969 revData->earlyOutputCntBase = revData->revOrder.count();
1971 // finally send it to GUI
1972 emit newRevsAdded(revData, revData->revOrder);
1975 void Git::parseDiffFormatLine(RevFile& rf, SCRef line, int parNum, FileNamesLoader& fl) {
1977 if (line[1] == ':') { // it's a combined merge
1979 /* For combined merges rename/copy information is useless
1980 * because nor the original file name, nor similarity info
1981 * is given, just the status tracks that in the left/right
1982 * branch a renamed/copy occurred (as example status could
1983 * be RM or MR). For visualization purposes we could consider
1984 * the file as modified
1986 appendFileName(rf, line.section('\t', -1), fl);
1987 setStatus(rf, "M");
1988 rf.mergeParent.append(parNum);
1989 } else { // faster parsing in normal case
1991 if (line.at(98) == '\t') {
1992 appendFileName(rf, line.mid(99), fl);
1993 setStatus(rf, line.at(97));
1994 rf.mergeParent.append(parNum);
1995 } else
1996 // it's a rename or a copy, we are not in fast path now!
1997 setExtStatus(rf, line.mid(97), parNum, fl);
2001 //CT TODO can go in RevFile
2002 void Git::setStatus(RevFile& rf, SCRef rowSt) {
2004 char status = rowSt.at(0).toLatin1();
2005 switch (status) {
2006 case 'M':
2007 case 'T':
2008 case 'U':
2009 rf.status.append(RevFile::MODIFIED);
2010 break;
2011 case 'D':
2012 rf.status.append(RevFile::DELETED);
2013 rf.onlyModified = false;
2014 break;
2015 case 'A':
2016 rf.status.append(RevFile::NEW);
2017 rf.onlyModified = false;
2018 break;
2019 case '?':
2020 rf.status.append(RevFile::UNKNOWN);
2021 rf.onlyModified = false;
2022 break;
2023 default:
2024 dbp("ASSERT in Git::setStatus, unknown status <%1>. "
2025 "'MODIFIED' will be used instead.", rowSt);
2026 rf.status.append(RevFile::MODIFIED);
2027 break;
2031 void Git::setExtStatus(RevFile& rf, SCRef rowSt, int parNum, FileNamesLoader& fl) {
2033 const QStringList sl(rowSt.split('\t', QString::SkipEmptyParts));
2034 if (sl.count() != 3) {
2035 dbp("ASSERT in setExtStatus, unexpected status string %1", rowSt);
2036 return;
2038 // we want store extra info with format "orig --> dest (Rxx%)"
2039 // but git give us something like "Rxx\t<orig>\t<dest>"
2040 SCRef type = sl[0];
2041 SCRef orig = sl[1];
2042 SCRef dest = sl[2];
2043 const QString extStatusInfo(orig + " --> " + dest + " (" + type + "%)");
2046 NOTE: we set rf.extStatus size equal to position of latest
2047 copied/renamed file. So it can have size lower then
2048 rf.count() if after copied/renamed file there are
2049 others. Here we have no possibility to know final
2050 dimension of this RefFile. We are still in parsing.
2053 // simulate new file
2054 appendFileName(rf, dest, fl);
2055 rf.mergeParent.append(parNum);
2056 rf.status.append(RevFile::NEW);
2057 rf.extStatus.resize(rf.status.size());
2058 rf.extStatus[rf.status.size() - 1] = extStatusInfo;
2060 // simulate deleted orig file only in case of rename
2061 if (type.at(0) == 'R') { // renamed file
2062 appendFileName(rf, orig, fl);
2063 rf.mergeParent.append(parNum);
2064 rf.status.append(RevFile::DELETED);
2065 rf.extStatus.resize(rf.status.size());
2066 rf.extStatus[rf.status.size() - 1] = extStatusInfo;
2068 rf.onlyModified = false;
2071 //CT TODO utility function; can go elsewhere
2072 void Git::parseDiffFormat(RevFile& rf, SCRef buf, FileNamesLoader& fl) {
2074 int parNum = 1, startPos = 0, endPos = buf.indexOf('\n');
2075 while (endPos != -1) {
2077 SCRef line = buf.mid(startPos, endPos - startPos);
2078 if (line[0] == ':') // avoid sha's in merges output
2079 parseDiffFormatLine(rf, line, parNum, fl);
2080 else
2081 parNum++;
2083 startPos = endPos + 1;
2084 endPos = buf.indexOf('\n', endPos + 99);
2088 bool Git::startParseProc(SCList initCmd, FileHistory* fh, SCRef buf) {
2090 DataLoader* dl = new DataLoader(this, fh); // auto-deleted when done
2092 connect(this, SIGNAL(cancelLoading(const FileHistory*)),
2093 dl, SLOT(on_cancel(const FileHistory*)));
2095 connect(dl, SIGNAL(newDataReady(const FileHistory*)),
2096 this, SLOT(on_newDataReady(const FileHistory*)));
2098 connect(dl, SIGNAL(loaded(FileHistory*, ulong, int,
2099 bool, const QString&, const QString&)), this,
2100 SLOT(on_loaded(FileHistory*, ulong, int,
2101 bool, const QString&, const QString&)));
2103 return dl->start(initCmd, workDir, buf);
2106 bool Git::startRevList(SCList args, FileHistory* fh) {
2108 QString baseCmd("git log --topo-order --no-color "
2110 #ifndef Q_OS_WIN32
2111 "--log-size " // FIXME broken on Windows
2112 #endif
2113 "--parents --boundary -z "
2114 "--pretty=format:%m%HX%PX%n%cn<%ce>%n%an<%ae>%n%at%n%s%n");
2116 // we don't need log message body for file history
2117 if (isMainHistory(fh))
2118 baseCmd.append("%b");
2120 QStringList initCmd(baseCmd.split(' '));
2121 if (!isMainHistory(fh)) {
2123 NOTE: we don't use '--remove-empty' option because
2124 in case a file is deleted and then a new file with
2125 the same name is created again in the same directory
2126 then, with this option, file history is truncated to
2127 the file deletion revision.
2129 initCmd << QString("-r -m -p --full-index").split(' ');
2130 } else
2131 {} // initCmd << QString("--early-output"); currently disabled
2133 return startParseProc(initCmd + args, fh, QString());
2136 bool Git::startUnappliedList() {
2138 QStringList unAppliedShaList(getAllRefSha(UN_APPLIED));
2139 if (unAppliedShaList.isEmpty())
2140 return false;
2142 // WARNING: with this command 'git log' could send spurious
2143 // revs so we need some filter out logic during loading
2144 QString cmd("git log --no-color --parents -z "
2146 #ifndef Q_OS_WIN32
2147 "--log-size " // FIXME broken on Windows
2148 #endif
2149 "--pretty=format:%m%HX%PX%n%an<%ae>%n%at%n%s%n%b ^HEAD");
2151 QStringList sl(cmd.split(' '));
2152 sl << unAppliedShaList;
2153 return startParseProc(sl, revData, QString());
2156 void Git::stop(bool saveCache) {
2157 // normally called when changing directory or closing
2159 EM_RAISE(exGitStopped);
2161 // stop all data sending from process and asks them
2162 // to terminate. Note that process could still keep
2163 // running for a while although silently
2164 emit cancelAllProcesses(); // non blocking
2166 // after cancelAllProcesses() procFinished() is not called anymore
2167 // TODO perhaps is better to call procFinished() also if process terminated
2168 // incorrectly as QProcess does. BUt first we need to fix FileView::on_loadCompleted()
2169 emit fileNamesLoad(1, revsFiles.count() - filesLoadingStartOfs);
2171 if (cacheNeedsUpdate && saveCache) {
2173 cacheNeedsUpdate = false;
2174 if (!filesLoadingCurSha.isEmpty()) // we are in the middle of a loading
2175 revsFiles.remove(toTempSha(filesLoadingCurSha)); // remove partial data
2177 if (!revsFiles.isEmpty()) {
2178 SHOW_MSG("Saving cache. Please wait...");
2179 if (!Cache::save(gitDir, revsFiles, dirNamesVec, fileNamesVec))
2180 dbs("ERROR unable to save file names cache");
2185 void Git::clearRevs() {
2187 revData->clear();
2188 patchesStillToFind = 0; // TODO TEST WITH FILTERING
2189 firstNonStGitPatch = "";
2190 workingDirInfo.clear();
2191 revsFiles.remove(ZERO_SHA_RAW);
2194 void Git::clearFileNames() {
2196 qDeleteAll(revsFiles);
2197 revsFiles.clear();
2198 fileNamesMap.clear();
2199 dirNamesMap.clear();
2200 dirNamesVec.clear();
2201 fileNamesVec.clear();
2202 revsFilesShaBackupBuf.clear();
2203 cacheNeedsUpdate = false;
2206 bool Git::init(SCRef wd, bool askForRange, const QStringList* passedArgs, bool overwriteArgs, bool* quit) {
2207 // normally called when changing git directory. Must be called after stop()
2209 *quit = false;
2210 clearRevs();
2212 /* we only update filtering info here, original arguments
2213 * are not overwritten. Only getArgs() can update arguments,
2214 * an exception is if flag overwriteArgs is set
2216 loadArguments.filteredLoading = (!overwriteArgs && passedArgs != NULL);
2217 if (loadArguments.filteredLoading)
2218 loadArguments.filterList = *passedArgs;
2220 if (overwriteArgs) // in this case must be passedArgs != NULL
2221 loadArguments.args = *passedArgs;
2223 try {
2224 setThrowOnStop(true);
2226 const QString msg1("Path is '" + workDir + "' Loading ");
2228 // check if repository is valid
2229 bool repoChanged;
2230 isGIT = getGitDBDir(wd, gitDir, repoChanged);
2232 if (repoChanged) {
2233 bool dummy;
2234 getBaseDir(wd, workDir, dummy);
2235 localDates.clear();
2236 clearFileNames();
2237 fileCacheAccessed = false;
2239 SHOW_MSG(msg1 + "file names cache...");
2240 loadFileCache();
2241 SHOW_MSG("");
2243 if (!isGIT) {
2244 setThrowOnStop(false);
2245 return false;
2247 if (!passedArgs) {
2249 // update text codec according to repo settings
2250 bool dummy;
2251 QTextCodec::setCodecForCStrings(getTextCodec(&dummy));
2253 // load references
2254 SHOW_MSG(msg1 + "refs...");
2255 if (!getRefs())
2256 dbs("WARNING: no tags or heads found");
2258 // startup input range dialog
2259 SHOW_MSG("");
2260 if (startup || askForRange) {
2261 loadArguments.args = getArgs(quit, repoChanged); // must be called with refs loaded
2262 if (*quit) {
2263 setThrowOnStop(false);
2264 return false;
2267 // load StGit unapplied patches, must be after getRefs()
2268 if (isStGIT) {
2269 loadingUnAppliedPatches = startUnappliedList();
2270 if (loadingUnAppliedPatches) {
2272 SHOW_MSG(msg1 + "StGIT unapplied patches...");
2273 setThrowOnStop(false);
2275 // we will continue with init2() at
2276 // the end of loading...
2277 return true;
2281 init2();
2282 setThrowOnStop(false);
2283 return true;
2285 } catch (int i) {
2287 setThrowOnStop(false);
2289 if (isThrowOnStopRaised(i, "initializing 1")) {
2290 EM_THROW_PENDING;
2291 return false;
2293 const QString info("Exception \'" + EM_DESC(i) + "\' "
2294 "not handled in init...re-throw");
2295 dbs(info);
2296 throw;
2300 void Git::init2() {
2302 const QString msg1("Path is '" + workDir + "' Loading ");
2304 // after loading unapplied patch update base early output offset to
2305 // avoid losing unapplied patches at first early output event
2306 if (isStGIT)
2307 revData->earlyOutputCntBase = revData->revOrder.count();
2309 try {
2310 setThrowOnStop(true);
2312 // load working directory files
2313 if (!loadArguments.filteredLoading && testFlag(DIFF_INDEX_F)) {
2314 SHOW_MSG(msg1 + "working directory changed files...");
2315 getDiffIndex(); // blocking, we could be in setRepository() now
2317 SHOW_MSG(msg1 + "revisions...");
2319 // build up command line arguments
2320 QStringList args(loadArguments.args);
2321 if (loadArguments.filteredLoading) {
2322 if (!args.contains("--"))
2323 args << "--";
2325 args << loadArguments.filterList;
2327 if (!startRevList(args, revData))
2328 SHOW_MSG("ERROR: unable to start 'git log'");
2330 setThrowOnStop(false);
2332 } catch (int i) {
2334 setThrowOnStop(false);
2336 if (isThrowOnStopRaised(i, "initializing 2")) {
2337 EM_THROW_PENDING;
2338 return;
2340 const QString info("Exception \'" + EM_DESC(i) + "\' "
2341 "not handled in init2...re-throw");
2342 dbs(info);
2343 throw;
2347 void Git::on_newDataReady(const FileHistory* fh) {
2349 emit newRevsAdded(fh , fh->revOrder);
2352 void Git::on_loaded(FileHistory* fh, ulong byteSize, int loadTime,
2353 bool normalExit, SCRef cmd, SCRef errorDesc) {
2355 if (!errorDesc.isEmpty()) {
2356 MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc);
2357 QApplication::postEvent(parent(), e);
2359 if (normalExit) { // do not send anything if killed
2361 on_newDataReady(fh);
2363 if (!loadingUnAppliedPatches) {
2365 fh->loadTime += loadTime;
2367 ulong kb = byteSize / 1024;
2368 double mbs = (double)byteSize / fh->loadTime / 1000;
2369 QString tmp;
2370 tmp.sprintf("Loaded %i revisions (%li KB), "
2371 "time elapsed: %i ms (%.2f MB/s)",
2372 fh->revs.count(), kb, fh->loadTime, mbs);
2374 if (!tryFollowRenames(fh))
2375 emit loadCompleted(fh, tmp);
2377 if (isMainHistory(fh))
2378 // wait the dust to settle down before to start
2379 // background file names loading for new revisions
2380 QTimer::singleShot(500, this, SLOT(loadFileNames()));
2383 if (loadingUnAppliedPatches) {
2384 loadingUnAppliedPatches = false;
2385 revData->lns->clear(); // again to reset lanes
2386 init2(); // continue with loading of remaining revisions
2390 bool Git::tryFollowRenames(FileHistory* fh) {
2392 if (isMainHistory(fh))
2393 return false;
2395 QStringList oldNames;
2396 QMutableStringListIterator it(fh->renamedRevs);
2397 while (it.hasNext())
2398 if (!populateRenamedPatches(it.next(), fh->curFNames, fh, &oldNames, false))
2399 it.remove();
2401 if (fh->renamedRevs.isEmpty())
2402 return false;
2404 QStringList args;
2405 args << fh->renamedRevs << "--" << oldNames;
2406 fh->fNames << oldNames;
2407 fh->curFNames = oldNames;
2408 fh->renamedRevs.clear();
2409 return startRevList(args, fh);
2412 bool Git::populateRenamedPatches(SCRef renamedSha, SCList newNames, FileHistory* fh,
2413 QStringList* oldNames, bool backTrack) {
2415 QString runOutput;
2416 if (!run("git diff-tree -r -M " + renamedSha, &runOutput))
2417 return false;
2419 // find the first renamed file with the new file name in renamedFiles list
2420 QString line;
2421 FOREACH_SL (it, newNames) {
2422 if (backTrack) {
2423 line = runOutput.section('\t' + *it + '\t', 0, 0,
2424 QString::SectionIncludeTrailingSep);
2425 line.chop(1);
2426 } else
2427 line = runOutput.section('\t' + *it + '\n', 0, 0);
2429 if (!line.isEmpty())
2430 break;
2432 if (line.contains('\n'))
2433 line = line.section('\n', -1, -1);
2435 SCRef status = line.section('\t', -2, -2).section(' ', -1, -1);
2436 if (!status.startsWith('R'))
2437 return false;
2439 if (backTrack) {
2440 SCRef nextFile = runOutput.section(line, 1, 1).section('\t', 1, 1);
2441 oldNames->append(nextFile.section('\n', 0, 0));
2442 return true;
2444 // get the diff betwen two files
2445 SCRef prevFileSha = line.section(' ', 2, 2);
2446 SCRef lastFileSha = line.section(' ', 3, 3);
2447 if (prevFileSha == lastFileSha) // just renamed
2448 runOutput.clear();
2449 else if (!run("git diff --no-ext-diff -r --full-index " + prevFileSha + " " + lastFileSha, &runOutput))
2450 return false;
2452 SCRef prevFile = line.section('\t', -1, -1);
2453 if (!oldNames->contains(prevFile))
2454 oldNames->append(prevFile);
2456 // save the patch, will be used later to create a
2457 // proper graft sha with correct parent info
2458 if (fh) {
2459 QString tmp(!runOutput.isEmpty() ? runOutput : "diff --no-ext-diff --\nsimilarity index 100%\n");
2460 fh->renamedPatches.insert(renamedSha, tmp);
2462 return true;
2465 void Git::populateFileNamesMap() {
2467 for (int i = 0; i < dirNamesVec.count(); ++i)
2468 dirNamesMap.insert(dirNamesVec[i], i);
2470 for (int i = 0; i < fileNamesVec.count(); ++i)
2471 fileNamesMap.insert(fileNamesVec[i], i);
2474 void Git::loadFileCache() {
2476 if (!fileCacheAccessed) {
2478 fileCacheAccessed = true;
2479 QByteArray shaBuf;
2480 if (Cache::load(gitDir, revsFiles, dirNamesVec, fileNamesVec, shaBuf)) {
2481 revsFilesShaBackupBuf.append(shaBuf);
2482 populateFileNamesMap();
2483 } else
2484 dbs("ERROR: unable to load file names cache");
2488 void Git::loadFileNames() {
2490 indexTree(); // we are sure data loading is finished at this point
2492 int revCnt = 0;
2493 QString diffTreeBuf;
2494 FOREACH (ShaVect, it, revData->revOrder) {
2496 if (!revsFiles.contains(*it)) {
2497 const Rev* c = revLookup(*it);
2498 if (c->parentsCount() == 1) { // skip initials and merges
2499 diffTreeBuf.append(*it).append('\n');
2500 revCnt++;
2504 if (!diffTreeBuf.isEmpty()) {
2505 filesLoadingPending = filesLoadingCurSha = "";
2506 filesLoadingStartOfs = revsFiles.count();
2507 emit fileNamesLoad(3, revCnt);
2509 const QString runCmd("git diff-tree --no-color -r -C --stdin");
2510 runAsync(runCmd, this, diffTreeBuf);
2514 bool Git::filterEarlyOutputRev(FileHistory* fh, Rev* rev) {
2516 if (fh->earlyOutputCnt < fh->revOrder.count()) {
2518 const ShaString& sha = fh->revOrder[fh->earlyOutputCnt++];
2519 const Rev* c = revLookup(sha, fh);
2520 if (c) {
2521 if (rev->sha() != sha || rev->parents() != c->parents()) {
2522 // mismatch found! set correct value, 'rev' will
2523 // overwrite 'c' upon returning
2524 rev->orderIdx = c->orderIdx;
2525 revData->clear(false); // flush the tail
2526 } else
2527 return true; // filter out 'rev'
2530 // we have new revisions, exit from early output state
2531 fh->setEarlyOutputState(false);
2532 return false;
2535 int Git::addChunk(FileHistory* fh, const QByteArray& ba, int start) {
2537 RevMap& r = fh->revs;
2538 int nextStart;
2539 Rev* rev;
2541 do {
2542 // only here we create a new rev
2543 rev = new Rev(ba, start, fh->revOrder.count(), &nextStart, !isMainHistory(fh));
2545 if (nextStart == -2) {
2546 delete rev;
2547 fh->setEarlyOutputState(true);
2548 start = ba.indexOf('\n', start) + 1;
2551 } while (nextStart == -2);
2553 if (nextStart == -1) { // half chunk detected
2554 delete rev;
2555 return -1;
2558 const ShaString& sha = rev->sha();
2560 if (fh->earlyOutputCnt != -1 && filterEarlyOutputRev(fh, rev)) {
2561 delete rev;
2562 return nextStart;
2565 if (isStGIT) {
2566 if (loadingUnAppliedPatches) { // filter out possible spurious revs
2568 Reference* rf = lookupReference(sha);
2569 if (!(rf && (rf->type & UN_APPLIED))) {
2570 delete rev;
2571 return nextStart;
2574 // remove StGIT spurious revs filter
2575 if (!firstNonStGitPatch.isEmpty() && firstNonStGitPatch == sha)
2576 firstNonStGitPatch = "";
2578 // StGIT called with --all option creates spurious revs so filter
2579 // out unknown revs until no more StGIT patches are waited and
2580 // firstNonStGitPatch is reached
2581 if (!(firstNonStGitPatch.isEmpty() && patchesStillToFind == 0) &&
2582 !loadingUnAppliedPatches && isMainHistory(fh)) {
2584 Reference* rf = lookupReference(sha);
2585 if (!(rf && (rf->type & APPLIED))) {
2586 delete rev;
2587 return nextStart;
2590 if (r.contains(sha)) {
2591 // StGIT unapplied patches could be sent again by
2592 // 'git log' as example if called with --all option.
2593 if (r[sha]->isUnApplied) {
2594 delete rev;
2595 return nextStart;
2597 // could be a side effect of 'git log -m', see below
2598 if (isMainHistory(fh) || rev->parentsCount() < 2)
2599 dbp("ASSERT: addChunk sha <%1> already received", sha);
2602 if (r.isEmpty() && !isMainHistory(fh)) {
2603 bool added = copyDiffIndex(fh, sha);
2604 rev->orderIdx = added ? 1 : 0;
2606 if ( !isMainHistory(fh)
2607 && !fh->renamedPatches.isEmpty()
2608 && fh->renamedPatches.contains(sha)) {
2610 // this is the new rev with renamed file, the rev is correct but
2611 // the patch, create a new rev with proper patch and use that instead
2612 const Rev* prevSha = revLookup(sha, fh);
2613 Rev* c = fakeRevData(sha, rev->parents(), rev->author(),
2614 rev->authorDate(), rev->shortLog(), rev->longLog(),
2615 fh->renamedPatches[sha], prevSha->orderIdx, fh);
2617 r.insert(sha, c); // overwrite old content
2618 fh->renamedPatches.remove(sha);
2619 return nextStart;
2621 if (!isMainHistory(fh) && rev->parentsCount() > 1 && r.contains(sha)) {
2622 /* In this case git log is called with -m option and merges are splitted
2623 in one commit per parent but all them have the same sha.
2624 So we add only the first to fh->revOrder to display history correctly,
2625 but we nevertheless add all the commits to 'r' so that annotation code
2626 can get the patches.
2628 QString mergeSha;
2629 int i = 0;
2631 mergeSha = QString::number(++i) + " m " + sha;
2632 while (r.contains(toTempSha(mergeSha)));
2634 const ShaString& ss = toPersistentSha(mergeSha, shaBackupBuf);
2635 r.insert(ss, rev);
2636 } else {
2637 r.insert(sha, rev);
2638 fh->revOrder.append(sha);
2640 if (rev->parentsCount() == 0 && !isMainHistory(fh))
2641 fh->renamedRevs.append(sha);
2643 if (isStGIT) {
2644 // updateLanes() is called too late, after loadingUnAppliedPatches
2645 // has been reset so update the lanes now.
2646 if (loadingUnAppliedPatches) {
2648 Rev* c = const_cast<Rev*>(revLookup(sha, fh));
2649 c->isUnApplied = true;
2650 c->lanes.append(UNAPPLIED);
2652 } else if (patchesStillToFind > 0 || !isMainHistory(fh)) { // try to avoid costly lookup
2654 Reference* rf = lookupReference(sha);
2655 if (rf && (rf->type & APPLIED)) {
2657 Rev* c = const_cast<Rev*>(revLookup(sha, fh));
2658 c->isApplied = true;
2659 if (isMainHistory(fh)) {
2660 patchesStillToFind--;
2661 if (patchesStillToFind == 0)
2662 // any rev will be discarded until
2663 // firstNonStGitPatch arrives
2664 firstNonStGitPatch = c->parent(0);
2669 return nextStart;
2672 bool Git::copyDiffIndex(FileHistory* fh, SCRef parent) {
2673 // must be called with empty revs and empty revOrder
2675 if (!fh->revOrder.isEmpty() || !fh->revs.isEmpty()) {
2676 dbs("ASSERT in copyDiffIndex: called with wrong context");
2677 return false;
2679 const Rev* r = revLookup(ZERO_SHA);
2680 if (!r)
2681 return false;
2683 const RevFile* files = getFiles(ZERO_SHA);
2684 if (!files || findFileIndex(*files, fh->fileNames().first()) == -1)
2685 return false;
2687 // insert a custom ZERO_SHA rev with proper parent
2688 const Rev* rf = fakeWorkDirRev(parent, "Working directory changes", "long log\n", 0, fh);
2689 fh->revs.insert(ZERO_SHA_RAW, rf);
2690 fh->revOrder.append(ZERO_SHA_RAW);
2691 return true;
2694 void Git::setLane(SCRef sha, FileHistory* fh) {
2696 Lanes* l = fh->lns;
2697 uint i = fh->firstFreeLane;
2698 QVector<QByteArray> ba;
2699 const ShaString& ss = toPersistentSha(sha, ba);
2700 const ShaVect& shaVec(fh->revOrder);
2702 for (uint cnt = shaVec.count(); i < cnt; ++i) {
2704 const ShaString& curSha = shaVec[i];
2705 Rev* r = const_cast<Rev*>(revLookup(curSha, fh));
2706 if (r->lanes.count() == 0)
2707 updateLanes(*r, *l, curSha);
2709 if (curSha == ss)
2710 break;
2712 fh->firstFreeLane = ++i;
2715 void Git::updateLanes(Rev& c, Lanes& lns, SCRef sha) {
2716 // we could get third argument from c.sha(), but we are in fast path here
2717 // and c.sha() involves a deep copy, so we accept a little redundancy
2719 if (lns.isEmpty())
2720 lns.init(sha);
2722 bool isDiscontinuity;
2723 bool isFork = lns.isFork(sha, isDiscontinuity);
2724 bool isMerge = (c.parentsCount() > 1);
2725 bool isInitial = (c.parentsCount() == 0);
2727 if (isDiscontinuity)
2728 lns.changeActiveLane(sha); // uses previous isBoundary state
2730 lns.setBoundary(c.isBoundary()); // update must be here
2732 if (isFork)
2733 lns.setFork(sha);
2734 if (isMerge)
2735 lns.setMerge(c.parents());
2736 if (c.isApplied)
2737 lns.setApplied();
2738 if (isInitial)
2739 lns.setInitial();
2741 lns.getLanes(c.lanes); // here lanes are snapshotted
2743 SCRef nextSha = (isInitial) ? "" : QString(c.parent(0));
2745 lns.nextParent(nextSha);
2747 if (c.isApplied)
2748 lns.afterApplied();
2749 if (isMerge)
2750 lns.afterMerge();
2751 if (isFork)
2752 lns.afterFork();
2753 if (lns.isBranch())
2754 lns.afterBranch();
2756 // QString tmp = "", tmp2;
2757 // for (uint i = 0; i < c.lanes.count(); i++) {
2758 // tmp2.setNum(c.lanes[i]);
2759 // tmp.append(tmp2 + "-");
2760 // }
2761 // qDebug("%s %s", tmp.toUtf8().data(), sha.toUtf8().data());
2764 void Git::procFinished() {
2766 flushFileNames(fileLoader);
2767 filesLoadingPending = filesLoadingCurSha = "";
2768 emit fileNamesLoad(1, revsFiles.count() - filesLoadingStartOfs);
2771 void Git::procReadyRead(const QByteArray& fileChunk) {
2773 if (filesLoadingPending.isEmpty())
2774 filesLoadingPending = fileChunk;
2775 else
2776 filesLoadingPending.append(fileChunk); // add to previous half lines
2778 RevFile* rf = NULL;
2779 if (!filesLoadingCurSha.isEmpty() && revsFiles.contains(toTempSha(filesLoadingCurSha)))
2780 rf = const_cast<RevFile*>(revsFiles[toTempSha(filesLoadingCurSha)]);
2782 int nextEOL = filesLoadingPending.indexOf('\n');
2783 int lastEOL = -1;
2784 while (nextEOL != -1) {
2786 SCRef line(filesLoadingPending.mid(lastEOL + 1, nextEOL - lastEOL - 1));
2787 if (line.at(0) != ':') {
2788 SCRef sha = line.left(40);
2789 if (!rf || sha != filesLoadingCurSha) { // new commit
2790 rf = new RevFile();
2791 revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf);
2792 filesLoadingCurSha = sha;
2793 cacheNeedsUpdate = true;
2794 } else
2795 dbp("ASSERT: repeated sha %1 in file names loading", sha);
2796 } else // line.constref(0) == ':'
2797 parseDiffFormatLine(*rf, line, 1, fileLoader);
2799 lastEOL = nextEOL;
2800 nextEOL = filesLoadingPending.indexOf('\n', lastEOL + 1);
2802 if (lastEOL != -1)
2803 filesLoadingPending.remove(0, lastEOL + 1);
2805 emit fileNamesLoad(2, revsFiles.count() - filesLoadingStartOfs);
2808 void Git::flushFileNames(FileNamesLoader& fl) {
2810 if (!fl.rf)
2811 return;
2813 QByteArray& b = fl.rf->pathsIdx;
2814 QVector<int>& dirs = fl.rfDirs;
2816 b.clear();
2817 b.resize(2 * dirs.size() * sizeof(int));
2819 int* d = (int*)(b.data());
2821 for (int i = 0; i < dirs.size(); i++) {
2823 d[i] = dirs.at(i);
2824 d[dirs.size() + i] = fl.rfNames.at(i);
2826 dirs.clear();
2827 fl.rfNames.clear();
2828 fl.rf = NULL;
2831 void Git::appendFileName(RevFile& rf, SCRef name, FileNamesLoader& fl) {
2833 if (fl.rf != &rf) {
2834 flushFileNames(fl);
2835 fl.rf = &rf;
2837 int idx = name.lastIndexOf('/') + 1;
2838 SCRef dr = name.left(idx);
2839 SCRef nm = name.mid(idx);
2841 QHash<QString, int>::const_iterator it(dirNamesMap.constFind(dr));
2842 if (it == dirNamesMap.constEnd()) {
2843 int idx = dirNamesVec.count();
2844 dirNamesMap.insert(dr, idx);
2845 dirNamesVec.append(dr);
2846 fl.rfDirs.append(idx);
2847 } else
2848 fl.rfDirs.append(*it);
2850 it = fileNamesMap.constFind(nm);
2851 if (it == fileNamesMap.constEnd()) {
2852 int idx = fileNamesVec.count();
2853 fileNamesMap.insert(nm, idx);
2854 fileNamesVec.append(nm);
2855 fl.rfNames.append(idx);
2856 } else
2857 fl.rfNames.append(*it);
2860 void Git::updateDescMap(const Rev* r,uint idx, QHash<QPair<uint, uint>, bool>& dm,
2861 QHash<uint, QVector<int> >& dv) {
2863 QVector<int> descVec;
2864 if (r->descRefsMaster != -1) {
2866 const Rev* tmp = revLookup(revData->revOrder[r->descRefsMaster]);
2867 const QVector<int>& nr = tmp->descRefs;
2869 for (int i = 0; i < nr.count(); i++) {
2871 if (!dv.contains(nr[i])) {
2872 dbp("ASSERT descendant for %1 not found", r->sha());
2873 return;
2875 const QVector<int>& dvv = dv[nr[i]];
2877 // copy the whole vector instead of each element
2878 // in the first iteration of the loop below
2879 descVec = dvv; // quick (shared) copy
2881 for (int y = 0; y < dvv.count(); y++) {
2883 uint v = (uint)dvv[y];
2884 QPair<uint, uint> key = qMakePair(idx, v);
2885 QPair<uint, uint> keyN = qMakePair(v, idx);
2886 dm.insert(key, true);
2887 dm.insert(keyN, false);
2889 // we don't want duplicated entry, otherwise 'dvv' grows
2890 // greatly in repos with many tagged development branches
2891 if (i > 0 && !descVec.contains(v)) // i > 0 is rare, no
2892 descVec.append(v); // need to optimize
2896 descVec.append(idx);
2897 dv.insert(idx, descVec);
2900 void Git::mergeBranches(Rev* p, const Rev* r) {
2902 int r_descBrnMaster = (checkRef(r->sha(), BRANCH | RMT_BRANCH) ? r->orderIdx : r->descBrnMaster);
2904 if (p->descBrnMaster == r_descBrnMaster || r_descBrnMaster == -1)
2905 return;
2907 // we want all the descendant branches, so just avoid duplicates
2908 const QVector<int>& src1 = revLookup(revData->revOrder[p->descBrnMaster])->descBranches;
2909 const QVector<int>& src2 = revLookup(revData->revOrder[r_descBrnMaster])->descBranches;
2910 QVector<int> dst(src1);
2911 for (int i = 0; i < src2.count(); i++)
2912 if (qFind(src1.constBegin(), src1.constEnd(), src2[i]) == src1.constEnd())
2913 dst.append(src2[i]);
2915 p->descBranches = dst;
2916 p->descBrnMaster = p->orderIdx;
2919 void Git::mergeNearTags(bool down, Rev* p, const Rev* r, const QHash<QPair<uint, uint>, bool>& dm) {
2921 bool isTag = checkRef(r->sha(), TAG);
2922 int r_descRefsMaster = isTag ? r->orderIdx : r->descRefsMaster;
2923 int r_ancRefsMaster = isTag ? r->orderIdx : r->ancRefsMaster;
2925 if (down && (p->descRefsMaster == r_descRefsMaster || r_descRefsMaster == -1))
2926 return;
2928 if (!down && (p->ancRefsMaster == r_ancRefsMaster || r_ancRefsMaster == -1))
2929 return;
2931 // we want the nearest tag only, so remove any tag
2932 // that is ancestor of any other tag in p U r
2933 const ShaVect& ro = revData->revOrder;
2934 const ShaString& sha1 = down ? ro[p->descRefsMaster] : ro[p->ancRefsMaster];
2935 const ShaString& sha2 = down ? ro[r_descRefsMaster] : ro[r_ancRefsMaster];
2936 const QVector<int>& src1 = down ? revLookup(sha1)->descRefs : revLookup(sha1)->ancRefs;
2937 const QVector<int>& src2 = down ? revLookup(sha2)->descRefs : revLookup(sha2)->ancRefs;
2938 QVector<int> dst(src1);
2940 for (int s2 = 0; s2 < src2.count(); s2++) {
2942 bool add = false;
2943 for (int s1 = 0; s1 < src1.count(); s1++) {
2945 if (src2[s2] == src1[s1]) {
2946 add = false;
2947 break;
2949 QPair<uint, uint> key = qMakePair((uint)src2[s2], (uint)src1[s1]);
2951 if (!dm.contains(key)) { // could be empty if all tags are independent
2952 add = true; // could be an independent path
2953 continue;
2955 add = (down && dm[key]) || (!down && !dm[key]);
2956 if (add)
2957 dst[s1] = -1; // mark for removing
2958 else
2959 break;
2961 if (add)
2962 dst.append(src2[s2]);
2964 QVector<int>& nearRefs = (down ? p->descRefs : p->ancRefs);
2965 int& nearRefsMaster = (down ? p->descRefsMaster : p->ancRefsMaster);
2967 nearRefs.clear();
2968 for (int s2 = 0; s2 < dst.count(); s2++)
2969 if (dst[s2] != -1)
2970 nearRefs.append(dst[s2]);
2972 nearRefsMaster = p->orderIdx;
2975 void Git::indexTree() {
2977 const ShaVect& ro = revData->revOrder;
2978 if (ro.count() == 0)
2979 return;
2981 // we keep the pairs(x, y). Value is true if x is
2982 // ancestor of y or false if y is ancestor of x
2983 QHash<QPair<uint, uint>, bool> descMap;
2984 QHash<uint, QVector<int> > descVect;
2986 // walk down the tree from latest to oldest,
2987 // compute children and nearest descendants
2988 for (uint i = 0, cnt = ro.count(); i < cnt; i++) {
2990 uint type = checkRef(ro[i]);
2991 bool isB = (type & (BRANCH | RMT_BRANCH));
2992 bool isT = (type & TAG);
2994 const Rev* r = revLookup(ro[i]);
2996 if (isB) {
2997 Rev* rr = const_cast<Rev*>(r);
2998 if (r->descBrnMaster != -1) {
2999 const ShaString& sha = ro[r->descBrnMaster];
3000 rr->descBranches = revLookup(sha)->descBranches;
3002 rr->descBranches.append(i);
3004 if (isT) {
3005 updateDescMap(r, i, descMap, descVect);
3006 Rev* rr = const_cast<Rev*>(r);
3007 rr->descRefs.clear();
3008 rr->descRefs.append(i);
3010 for (uint y = 0; y < r->parentsCount(); y++) {
3012 Rev* p = const_cast<Rev*>(revLookup(r->parent(y)));
3013 if (p) {
3014 p->children.append(i);
3016 if (p->descBrnMaster == -1)
3017 p->descBrnMaster = isB ? r->orderIdx : r->descBrnMaster;
3018 else
3019 mergeBranches(p, r);
3021 if (p->descRefsMaster == -1)
3022 p->descRefsMaster = isT ? r->orderIdx : r->descRefsMaster;
3023 else
3024 mergeNearTags(optGoDown, p, r, descMap);
3028 // walk backward through the tree and compute nearest tagged ancestors
3029 for (int i = ro.count() - 1; i >= 0; i--) {
3031 const Rev* r = revLookup(ro[i]);
3032 bool isTag = checkRef(ro[i], TAG);
3034 if (isTag) {
3035 Rev* rr = const_cast<Rev*>(r);
3036 rr->ancRefs.clear();
3037 rr->ancRefs.append(i);
3039 for (int y = 0; y < r->children.count(); y++) {
3041 Rev* c = const_cast<Rev*>(revLookup(ro[r->children[y]]));
3042 if (c) {
3043 if (c->ancRefsMaster == -1)
3044 c->ancRefsMaster = isTag ? r->orderIdx:r->ancRefsMaster;
3045 else
3046 mergeNearTags(!optGoDown, c, r, descMap);