#include "git_client.h" #include "update.h" #include "worker.h" #include "utils.h" #include <QRegularExpression> #include <QDebug> #include <QDir> GitClient::GitClient(QString const &customerNrStr, QString const &customerRepository, QString const &workingDirectory, QString const &branchName, QObject *parent) : QObject(parent) , m_worker(qobject_cast<Worker *>(parent)) , m_repositoryPath(QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(customerNrStr)) , m_customerNr(customerNrStr) , m_workingDirectory(workingDirectory) , m_branchName(branchName) , m_customerRepository(customerRepository) { if (!m_worker) { qCritical() << "ERROR CASTING PARENT TO WORKER FAILED"; } } bool GitClient::gitCloneCustomerRepository() { /* Blobless clone ============== When using the --filter=blob:none option, the initial git clone will download all reachable commits and trees, and only download the blobs for commits when you do a git checkout. This includes the first checkout inside the git clone operation. The important thing to notice is that we have a copy of every blob at HEAD but the blobs in the history are not present. If your repository has a deep history full of large blobs, then this option can significantly reduce your git clone times. The commit and tree data is still present, so any subsequent git checkout only needs to download the missing blobs. The Git client knows how to batch these requests to ask the server only for the missing blobs. Further, when running git fetch in a blobless clone, the server only sends the new commits and trees. The new blobs are downloaded only after a git checkout. Note that git pull runs git fetch and then git merge, so it will download the necessary blobs during the git merge command. When using a blobless clone, you will trigger a blob download whenever you need the contents of a file, but you will not need one if you only need the OID (object-id) of a file. This means that git log can detect which commits changed a given path without needing to download extra data. This means that blobless clones can perform commands like git merge-base, git log, or even git log -- <path> with the same performance as a full clone. Commands like git diff or git blame <path> require the contents of the paths to compute diffs, so these will trigger blob downloads the first time they are run. However, the good news is that after that you will have those blobs in your repository and do not need to download them a second time. Most developers only need to run git blame on a small number of files, so this tradeoff of a slightly slower git blame command is worth the faster clone and fetch times. Note: git v2.18 does not support treeless clones: --filter=tree:0. */ // Note: for some reason it is necessary to pass "--progress ---v", // otherwise QProcess returns an error of 128 = 0x80 for the command. QString gitCommand("git clone --progress -vvv --filter=blob:none "); gitCommand += m_repositoryPath; Command c(gitCommand); qInfo() << "IN CURRENT WD" << m_workingDirectory << "CLONE VIA COMMAND" << gitCommand; if (c.execute(m_workingDirectory)) { // execute the command in wd QString const result = c.getCommandResult(); if (!result.isEmpty()) { // Cloning into 'customer_281'...\n int customer = -1; int cloning = result.indexOf("Cloning", 0, Qt::CaseInsensitive); if (cloning != -1) { customer = result.indexOf("customer_", cloning, Qt::CaseInsensitive); if (customer != -1) { QString customerNr = result.mid(customer); static constexpr char const ch = '\''; int i = customerNr.indexOf(QChar(ch)); if (i != -1) { if ((customerNr = customerNr.mid(0, i)) == m_customerNr) { qInfo() << "CLONING" << m_repositoryPath << "OK"; return true; } Utils::printCriticalErrorMsg( QString("ERROR CLONE RESULT HAS WRONG CUSTOMER-NR. (%1 != %2) CLONE_RESULT=%3") .arg(customerNr) .arg(m_customerNr) .arg(result)); return false; } } } Utils::printCriticalErrorMsg( QString("ERROR CLONE RESULT HAS WRONG FORMAT. CLONING=%1 CUSTOMER=%2 CLONE_RESULT=%3") .arg(cloning) .arg(customer) .arg(result)); return false; } Utils::printCriticalErrorMsg("ERROR CLONE RESULT IS EMPTY"); } return false; } bool GitClient::copyGitConfigFromMaster() { // only allowed when called in // master branch (???) if (QDir(m_customerRepository).exists()) { QString const cp = QString("cp .gitconfig .git/config"); Command c("bash"); if (c.execute(m_customerRepository, QStringList() << "-c" << cp)) { qInfo() << "cp .gitconfig .git/config OK"; return true; } qCritical() << "ERROR cp .gitconfig .git/config"; } return false; } QStringList GitClient::gitBranchNames() { // git config --global pager.branch false QStringList bNames; if (QDir(m_customerRepository).exists()) { QString gitCommand("git branch -a"); Command c(gitCommand); if (c.execute(m_customerRepository)) { QString const result = c.getCommandResult(); return result.split('\n'); } } return bNames; } bool GitClient::gitCheckoutBranch() { // TODO: nachsehen, ob der Branch ueberhaupt existiert if (QDir(m_customerRepository).exists()) { int zoneNr = Utils::read1stLineOfFile("/mnt/system_data/zone_nr"); m_branchName = (zoneNr != 0) ? QString("zg1/zone%1").arg(zoneNr) : "master"; QString gitCommand("git checkout "); gitCommand += m_branchName; Command c(gitCommand); return c.execute(m_customerRepository); // execute command in customerRepo } Utils::printCriticalErrorMsg(QString("ERROR ") + m_customerRepository + " DOES NOT EXIST"); return false; } bool GitClient::gitCloneAndCheckoutBranch() { qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT" << m_branchName; if (gitCloneCustomerRepository()) { //if (copyGitConfigFromMaster()) { if (gitCheckoutBranch()) { return true; } else { // TODO } //} } Utils::printCriticalErrorMsg(QString("CLONE ") + m_repositoryPath + " AND CHECKOUT FAILED"); return false; } QStringList GitClient::gitShowReason(QString branchName) { QStringList lst; if (QDir(m_customerRepository).exists()) { // %h: commit (short form) // %s: commit message // %cI: commit date, strict ISO 8601 format // Note: branch with branchName has to exist: format zg1/zone1 Command c(QString("git show origin/%1 -s --format=\"c=%h m=%s d=%cI\"").arg(branchName)); if (c.execute(m_customerRepository)) { QString const s = c.getCommandResult().trimmed(); int const c = s.indexOf("c="); int const m = s.indexOf("m="); int const d = s.indexOf("d="); QString msg = IsmasClient::getReasonForLastSendVersion(); QString commit{""}, date{""}; if (c != -1) { int start = c + 2; if (m >= start) { int length = m - start; commit = s.mid(start, length).trimmed(); start = m + 2; if (d >= start) { length = d - start; msg += " ("; msg = s.mid(start, length).trimmed(); msg += ")"; start = d + 2; date = s.mid(start); } } if (!commit.isEmpty() && !msg.isEmpty() && !date.isEmpty()) { lst << commit << msg << date; } } } } else { qCritical() << "CUSTOMER_REPOSITORY" << m_customerRepository << "DOES NOT EXIST"; } return lst; } /* Zu beachten: wird eine datei neu hinzugefuegt (git add/commit) dann aber gleich wieder geloscht, so wird sie im diff nicht angezeigt. */ std::optional<QStringList> GitClient::gitDiff(QString const &commits) { if (QDir(m_customerRepository).exists()) { // 409f198..6c22726 QString gitCommand("git diff --compact-summary "); gitCommand += commits; Command c(gitCommand); if (c.execute(m_customerRepository)) { // execute command in local customerRepo QString s = c.getCommandResult().trimmed(); Utils::printInfoMsg("GIT DIFF RESULT " + s); QStringList lines = Update::split(s, '\n'); QStringList fileNames; // each line has the format "etc/psa_config/DC2C_print01.json | 1 + // or the format "etc/psa_config/DC2C_print01.json (new) | 1 + // the filenames are relativ to the repository for (int i = 0; i < lines.size(); ++i) { QString const &line = lines.at(i); int newIndex = line.indexOf("(new"); // for new files int goneIndex = line.indexOf("(gone"); // for removed files int modeIndex = line.indexOf("(mode"); int pipeIndex = line.indexOf('|'); if (newIndex != -1) { QString file = line.left(newIndex).trimmed(); qInfo() << "FILE (NEW)" << file; fileNames << file; } else if (modeIndex != -1) { QString const file = line.left(modeIndex).trimmed(); qInfo() << "FILE (MODE)" << file; fileNames << file; } else if (goneIndex != -1) { QString const file = line.left(goneIndex).trimmed(); qCritical() << "FILE (GONE)" << file; } else if (pipeIndex != -1) { QString const file = line.left(pipeIndex).trimmed(); qInfo() << "FILE (PIPE)" << file; fileNames << file; } } if (!fileNames.isEmpty()) { return fileNames; } } } return std::nullopt; } bool GitClient::gitFsck() { bool r = false; if (QDir(m_customerRepository).exists()) { Command c("git fsck"); if ((r = c.execute(m_customerRepository)) == false) { QString const &s = c.getCommandResult().trimmed(); Utils::printCriticalErrorMsg(QString("GIT FSCK FAILED: %1").arg(s)); } } return r; } /* Hat sich nichts geaendert, so werden auch keine Commits <>..<> angezeigt */ std::optional<QString> GitClient::gitPull() { if (QDir(m_customerRepository).exists()) { qInfo() << "BRANCH NAME" << m_branchName; Command c("git pull"); if (c.execute(m_customerRepository)) { QString const s = c.getCommandResult().trimmed(); if (!s.isEmpty()) { QStringList lines = Update::split(s, '\n'); worker()->CONSOLE(lines) << Worker::UPDATE_STEP::UPDATE_REPOSITORY; if (!lines.empty()) { static const QRegularExpression alreadyUpToDate("^\\s*Already\\s+up\\s+to\\s+date.*$"); if (std::none_of(lines.cbegin(), lines.cend(), [](QString const &s) { return s.contains(alreadyUpToDate); })) { int zoneNr = Utils::read1stLineOfFile("/mnt/system_data/zone_nr"); m_branchName = (zoneNr != 0) ? QString("zg1/zone%1").arg(zoneNr) : "master"; // lines can look like this: // From https://git.mimbach49.de/GerhardHoffmann/customer_281 // 41ec581..5d25ac3 master -> origin/master // ff10f57..43530a1 zg1/zone1 -> origin/zg1/zone1 // 6ed893f..5d9882c zg1/zone2 -> origin/zg1/zone2 // 4384d17..77045d8 zg1/zone3 -> origin/zg1/zone3 // 89d2812..36a0d74 zg1/zone5 -> origin/zg1/zone5 bool found = false; for (int i=0; i < lines.size(); ++i) { if (lines.at(i).contains(m_branchName)) { found = true; // 409f198..6c22726 zg1/zone1 -> origin/zg1/zone1 static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)"); QRegularExpressionMatch match = re.match(lines.at(i)); if (match.hasMatch()) { if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches QString const matchCaptured = match.captured(2); worker()->CONSOLE(QStringList(matchCaptured)) << Worker::UPDATE_STEP::UPDATE_REPOSITORY; return matchCaptured; } else { QStringList lst(QString("(wrong capture count (%1)").arg(re.captureCount())); worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE); } } else { QStringList lst("no regex-match for commits"); worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE); } } } if (!found) { QStringList lst(QString("unknown branch name ") + m_branchName); worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE); } } else { return "Already up to date"; } } else { QStringList lst(QString("WRONG FORMAT FOR RESULT OF 'GIT PULL' ") + s); worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE); } } else { QStringList lst("EMPTY RESULT FOR 'GIT PULL'"); worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE); } } } else { QStringList lst(QString("REPOSITORY ") + m_customerRepository + " DOES NOT EXIST"); worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE); } return std::nullopt; } std::optional<QStringList> GitClient::gitMerge() { Command c("git merge"); if (c.execute(m_workingDirectory)) { QString s = c.getCommandResult(); QStringList lst = Update::split(s, '\n'); return lst; } return std::nullopt; } QString GitClient::gitLastCommit(QString fileName) { if (QDir(m_customerRepository).exists()) { QString const filePath = QDir::cleanPath(m_customerRepository + QDir::separator() + fileName); if (QFile(filePath).exists()) { QString const gitCommand = QString("git log %1 | head -n 1").arg(fileName); Command c("bash"); if (c.execute(m_customerRepository, QStringList() << "-c" << gitCommand)) { QString const r = c.getCommandResult(); int const idx = r.indexOf("commit "); if (idx != -1) { return r.mid(idx + 8).trimmed(); } } } } return ""; } // fileName has to an absolute path QString GitClient::gitBlob(QString fileName) { QFileInfo fi(fileName); if (fi.exists()) { QString const gitCommand = QString("git hash-object %1").arg(fileName); Command c(gitCommand); if (c.execute("/tmp")) { return c.getCommandResult().trimmed(); } } return "N/A"; } QString GitClient::gitCommitForBlob(QString blob) { if (QDir(m_customerRepository).exists()) { QString const gitCommand = QString("git whatchanged --all --find-object=%1 | head -n 1").arg(blob); Command c(gitCommand); if (c.execute(m_customerRepository)) { return c.getCommandResult(); } } return ""; } bool GitClient::gitIsFileTracked(QString fName) { if (QDir(m_customerRepository).exists()) { QString const gitCommand = QString("git ls-files --error-unmatch %1").arg(fName); Command c(gitCommand); return c.execute(m_customerRepository); } return false; } //get_commit_for_blob () { // # search for the blob in all commits for the file(name) $1 // echo $(git log --all --pretty=format:%H -- $2 | // xargs -I{} bash -c "git ls-tree {} -- $2 | // grep -q $1 && echo -n {} && head -n 1") //}