1#include "GitQlient.h"
2
3#include <CreateRepoDlg.h>
4#include <GitBase.h>
5#include <GitConfig.h>
6#include <GitQlientRepo.h>
7#include <GitQlientSettings.h>
8#include <GitQlientStyles.h>
9#include <InitScreen.h>
10#include <InitialRepoConfig.h>
11#include <ProgressDlg.h>
12#include <QPinnableTabWidget.h>
13
14#include <QCommandLineParser>
15#include <QEvent>
16#include <QFile>
17#include <QFileDialog>
18#include <QMenu>
19#include <QMessageBox>
20#include <QProcess>
21#include <QPushButton>
22#include <QStackedLayout>
23#include <QTabBar>
24#include <QTextStream>
25#include <QToolButton>
26
27#include <QLogger.h>
28
29using namespace QLogger;
30
31GitQlient::GitQlient(QWidget *parent)
32 : QWidget(parent)
33 , mStackedLayout(new QStackedLayout(this))
34 , mRepos(new QPinnableTabWidget())
35 , mConfigWidget(new InitScreen())
36
37{
38 QLog_Info("UI", "*******************************************");
39 QLog_Info("UI", "* GitQlient has started *");
40 QLog_Info("UI", QString("* %1 *").arg(VER));
41 QLog_Info("UI", "*******************************************");
42
43 setStyleSheet(GitQlientStyles::getStyles());
44
45 const auto homeMenu = new QPushButton();
46 const auto menu = new QMenu(homeMenu);
47
48 homeMenu->setIcon(QIcon(":/icons/burger_menu"));
49 homeMenu->setIconSize(QSize(17, 17));
50 homeMenu->setToolTip("Options");
51 homeMenu->setMenu(menu);
52 homeMenu->setObjectName("MainMenuBtn");
53
54 menu->installEventFilter(this);
55
56 const auto open = menu->addAction(tr("Open repo..."));
57 connect(open, &QAction::triggered, this, &GitQlient::openRepo);
58
59 const auto clone = menu->addAction(tr("Clone repo..."));
60 connect(clone, &QAction::triggered, this, &GitQlient::cloneRepo);
61
62 const auto init = menu->addAction(tr("New repo..."));
63 connect(init, &QAction::triggered, this, &GitQlient::initRepo);
64
65 menu->addSeparator();
66
67 GitQlientSettings settings;
68 const auto recent = new QMenu("Recent repos", menu);
69 const auto projects = settings.getRecentProjects();
70
71 for (const auto &project : projects)
72 {
73 const auto projectName = project.mid(project.lastIndexOf("/") + 1);
74 const auto action = recent->addAction(projectName);
75 action->setData(project);
76 connect(action, &QAction::triggered, this, [this, project]() { openRepoWithPath(project); });
77 }
78
79 menu->addMenu(recent);
80
81 const auto mostUsed = new QMenu("Most used repos", menu);
82 const auto recentProjects = settings.getMostUsedProjects();
83
84 for (const auto &project : recentProjects)
85 {
86 const auto projectName = project.mid(project.lastIndexOf("/") + 1);
87 const auto action = mostUsed->addAction(projectName);
88 action->setData(project);
89 connect(action, &QAction::triggered, this, [this, project]() { openRepoWithPath(project); });
90 }
91
92 menu->addMenu(mostUsed);
93
94 mRepos->setObjectName("GitQlientTab");
95 mRepos->setStyleSheet(GitQlientStyles::getStyles());
96 mRepos->setCornerWidget(homeMenu, Qt::TopLeftCorner);
97 connect(mRepos, &QTabWidget::tabCloseRequested, this, &GitQlient::closeTab);
98 connect(mRepos, &QTabWidget::currentChanged, this, &GitQlient::updateWindowTitle);
99
100 mStackedLayout->setContentsMargins(QMargins());
101 mStackedLayout->addWidget(mConfigWidget);
102 mStackedLayout->addWidget(mRepos);
103 mStackedLayout->setCurrentIndex(0);
104
105 mConfigWidget->onRepoOpened();
106
107 connect(mConfigWidget, &InitScreen::signalOpenRepo, this, &GitQlient::addRepoTab);
108
109 const auto geometry = settings.globalValue("GitQlientGeometry", saveGeometry()).toByteArray();
110
111 if (!geometry.isNull())
112 restoreGeometry(geometry);
113
114 const auto gitBase(QSharedPointer<GitBase>::create(""));
115 mGit = QSharedPointer<GitConfig>::create(gitBase);
116
117 connect(mGit.data(), &GitConfig::signalCloningProgress, this, &GitQlient::updateProgressDialog,
118 Qt::DirectConnection);
119 connect(mGit.data(), &GitConfig::signalCloningFailure, this, &GitQlient::showError, Qt::DirectConnection);
120}
121
122GitQlient::~GitQlient()
123{
124 QStringList pinnedRepos;
125 const auto totalTabs = mRepos->count();
126
127 for (auto i = 0; i < totalTabs; ++i)
128 {
129 if (mRepos->isPinned(i))
130 {
131 auto repoToRemove = dynamic_cast<GitQlientRepo *>(mRepos->widget(i));
132 pinnedRepos.append(repoToRemove->currentDir());
133 }
134 }
135
136 GitQlientSettings settings;
137 settings.setGlobalValue(GitQlientSettings::PinnedRepos, pinnedRepos);
138 settings.setGlobalValue("GitQlientGeometry", saveGeometry());
139
140 QLog_Info("UI", "* Closing GitQlient *\n\n");
141}
142
143bool GitQlient::eventFilter(QObject *obj, QEvent *event)
144{
145
146 if (const auto menu = qobject_cast<QMenu *>(obj); menu && event->type() == QEvent::Show)
147 {
148 auto localPos = menu->parentWidget()->pos();
149 auto pos = mapToGlobal(localPos);
150 menu->show();
151 pos.setY(pos.y() + menu->parentWidget()->height());
152 menu->move(pos);
153 return true;
154 }
155
156 return false;
157}
158
159void GitQlient::openRepo()
160{
161
162 const QString dirName(QFileDialog::getExistingDirectory(this, "Choose the directory of a Git project"));
163
164 if (!dirName.isEmpty())
165 openRepoWithPath(dirName);
166}
167
168void GitQlient::openRepoWithPath(const QString &path)
169{
170 QDir d(path);
171 addRepoTab(d.absolutePath());
172}
173
174void GitQlient::cloneRepo()
175{
176 CreateRepoDlg cloneDlg(CreateRepoDlgType::CLONE, mGit, this);
177 connect(&cloneDlg, &CreateRepoDlg::signalOpenWhenFinish, this, [this](const QString &path) { mPathToOpen = path; });
178
179 if (cloneDlg.exec() == QDialog::Accepted)
180 {
181 mProgressDlg = new ProgressDlg(tr("Loading repository..."), QString(), 100, false);
182 connect(mProgressDlg, &ProgressDlg::destroyed, this, [this]() { mProgressDlg = nullptr; });
183 mProgressDlg->show();
184 }
185}
186
187void GitQlient::initRepo()
188{
189 CreateRepoDlg cloneDlg(CreateRepoDlgType::INIT, mGit, this);
190 connect(&cloneDlg, &CreateRepoDlg::signalOpenWhenFinish, this, &GitQlient::openRepoWithPath);
191 cloneDlg.exec();
192}
193
194void GitQlient::updateProgressDialog(QString stepDescription, int value)
195{
196 if (value >= 0)
197 {
198 mProgressDlg->setValue(value);
199
200 if (stepDescription.contains("done", Qt::CaseInsensitive))
201 {
202 mProgressDlg->close();
203 openRepoWithPath(mPathToOpen);
204
205 mPathToOpen = "";
206 }
207 }
208
209 mProgressDlg->setLabelText(stepDescription);
210 mProgressDlg->repaint();
211}
212
213void GitQlient::showError(int, QString description)
214{
215 if (mProgressDlg)
216 mProgressDlg->deleteLater();
217
218 QMessageBox::critical(this, tr("Error!"), description);
219}
220
221void GitQlient::setRepositories(const QStringList &repositories)
222{
223 QLog_Info("UI", QString("Adding {%1} repositories").arg(repositories.count()));
224
225 for (const auto &repo : repositories)
226 addRepoTab(repo);
227}
228
229bool GitQlient::setArgumentsPostInit(const QStringList &arguments)
230{
231 QLog_Info("UI", QString("External call with the params {%1}").arg(arguments.join(",")));
232
233 QStringList repos;
234 const auto ret = parseArguments(arguments, &repos);
235 if (ret)
236 setRepositories(repos);
237 return ret;
238}
239
240bool GitQlient::parseArguments(const QStringList &arguments, QStringList *repos)
241{
242 bool ret = true;
243 GitQlientSettings settings;
244 auto logLevel
245 = static_cast<LogLevel>(settings.globalValue("logsLevel", static_cast<int>(LogLevel::Warning)).toInt());
246 bool areLogsDisabled = settings.globalValue("logsDisabled", true).toBool();
247
248 QCommandLineParser parser;
249 parser.setApplicationDescription(tr("Multi-platform Git client written with Qt"));
250 parser.addPositionalArgument("repos", tr("Git repositories to open"), tr("[repos...]"));
251
252 const QCommandLineOption helpOption = parser.addHelpOption();
253 // We don't use parser.addVersionOption() because then it is handled by Qt and we want to show Git SHA also
254 const QCommandLineOption versionOption(QStringList() << "v"
255 << "version",
256 tr("Displays version information."));
257 parser.addOption(versionOption);
258
259 const QCommandLineOption noLogOption("no-log", tr("Disables logs."));
260 parser.addOption(noLogOption);
261
262 const QCommandLineOption logLevelOption("log-level", tr("Sets log level."), tr("level"));
263 parser.addOption(logLevelOption);
264
265 parser.process(arguments);
266
267 *repos = parser.positionalArguments();
268 if (parser.isSet(noLogOption))
269 areLogsDisabled = true;
270
271 if (!areLogsDisabled)
272 {
273 if (parser.isSet(logLevelOption))
274 {
275 const auto level = static_cast<LogLevel>(parser.value(logLevelOption).toInt());
276 if (level >= QLogger::LogLevel::Trace && level <= QLogger::LogLevel::Fatal)
277 {
278 logLevel = level;
279 settings.setGlobalValue("logsLevel", static_cast<int>(level));
280 }
281 }
282
283 QLoggerManager::getInstance()->overwriteLogLevel(logLevel);
284 }
285 else
286 QLoggerManager::getInstance()->pause();
287
288 if (parser.isSet(versionOption))
289 {
290 QTextStream out(stdout);
291 out << QCoreApplication::applicationName() << ' ' << tr("version") << ' '
292 << QCoreApplication::applicationVersion() << " (" << tr("Git SHA ") << SHA_VER << ")\n";
293 ret = false;
294 }
295 if (parser.isSet(helpOption))
296 ret = false;
297
298 const auto manager = QLoggerManager::getInstance();
299 manager->addDestination("GitQlient.log", { "UI", "Git", "Cache" }, logLevel);
300
301 return ret;
302}
303
304void GitQlient::addRepoTab(const QString &repoPath)
305{
306 addNewRepoTab(repoPath, false);
307}
308
309void GitQlient::addNewRepoTab(const QString &repoPathArg, bool pinned)
310{
311 const auto repoPath = QFileInfo(repoPathArg).canonicalFilePath();
312
313 if (!mCurrentRepos.contains(repoPath))
314 {
315 QFileInfo info(QString("%1/.git").arg(repoPath));
316
317 if (info.isFile() || info.isDir())
318 {
319 const auto repoName = repoPath.contains("/") ? repoPath.split("/").last() : "";
320
321 if (repoName.isEmpty())
322 {
323 QMessageBox::critical(
324 this, tr("Not a repository"),
325 tr("The selected folder is not a Git repository. Please make sure you open a Git repository."));
326 QLog_Error("UI", "The selected folder is not a Git repository");
327 return;
328 }
329
330 QSharedPointer<GitBase> git(new GitBase(repoPath));
331 QSharedPointer<GitQlientSettings> settings(new GitQlientSettings(git->getGitDir()));
332
333 conditionallyOpenPreConfigDlg(git, settings);
334
335 const auto repo = new GitQlientRepo(git, settings);
336 const auto index = pinned ? mRepos->addPinnedTab(repo, repoName) : mRepos->addTab(repo, repoName);
337
338 connect(repo, &GitQlientRepo::signalOpenSubmodule, this, &GitQlient::addRepoTab);
339 connect(repo, &GitQlientRepo::repoOpened, this, &GitQlient::onSuccessOpen);
340 connect(repo, &GitQlientRepo::currentBranchChanged, this, &GitQlient::updateWindowTitle);
341
342 repo->setRepository(repoName);
343
344 if (!repoPath.isEmpty())
345 {
346 QProcess p;
347 p.setWorkingDirectory(repoPath);
348 p.start("git rev-parse", { "--show-superproject-working-tree" });
349 p.waitForFinished(5000);
350
351 const auto output = p.readAll().trimmed();
352 const auto isSubmodule = !output.isEmpty();
353
354 mRepos->setTabIcon(index, QIcon(isSubmodule ? QString(":/icons/submodules") : QString(":/icons/local")));
355
356 QLog_Info("UI", "Attaching repository to a new tab");
357
358 if (isSubmodule)
359 {
360 const auto parentRepo = QString::fromUtf8(output.split('/').last());
361
362 mRepos->setTabText(index, QString("%1 \u2192 %2").arg(parentRepo, repoName));
363
364 QLog_Info("UI",
365 QString("Opening the submodule {%1} from the repo {%2} on tab index {%3}")
366 .arg(repoName, parentRepo)
367 .arg(index));
368 }
369 }
370
371 mRepos->setCurrentIndex(index);
372 mStackedLayout->setCurrentIndex(1);
373
374 mCurrentRepos.insert(repoPath);
375 }
376 else
377 {
378 QLog_Info("UI", "Trying to open a directory that is not a Git repository.");
379 QMessageBox::information(
380 this, tr("Not a Git repository"),
381 tr("The selected path is not a Git repository. Please make sure you opened the correct directory."));
382 }
383 }
384 else
385 QLog_Warning("UI", QString("Repository at {%1} already opened. Skip adding it again.").arg(repoPath));
386}
387
388void GitQlient::closeTab(int tabIndex)
389{
390 const auto repoToRemove = dynamic_cast<GitQlientRepo *>(mRepos->widget(tabIndex));
391
392 QLog_Info("UI", QString("Removing repository {%1}").arg(repoToRemove->currentDir()));
393
394 mCurrentRepos.remove(repoToRemove->currentDir());
395 repoToRemove->close();
396
397 const auto totalTabs = mRepos->count() - 1;
398
399 if (totalTabs == 0)
400 {
401 mStackedLayout->setCurrentIndex(0);
402 setWindowTitle(QString("GitQlient %1").arg(VER));
403 }
404}
405
406void GitQlient::restorePinnedRepos()
407{
408 const auto pinnedRepos
409 = GitQlientSettings().globalValue(GitQlientSettings::PinnedRepos, QStringList()).toStringList();
410
411 for (auto &repo : pinnedRepos)
412 addNewRepoTab(repo, true);
413}
414
415void GitQlient::onSuccessOpen(const QString &fullPath)
416{
417 GitQlientSettings().setProjectOpened(fullPath);
418
419 mConfigWidget->onRepoOpened();
420}
421
422void GitQlient::conditionallyOpenPreConfigDlg(const QSharedPointer<GitBase> &git,
423 const QSharedPointer<GitQlientSettings> &settings)
424{
425 QScopedPointer<GitConfig> config(new GitConfig(git));
426
427 const auto showDlg = settings->localValue("ShowInitConfigDialog", true).toBool();
428 const auto maxCommits = settings->localValue("MaxCommits", -1).toInt();
429
430 if (maxCommits == -1 || (config->getServerHost().contains("https") && showDlg))
431 {
432 const auto preConfig = new InitialRepoConfig(git, settings, this);
433 preConfig->exec();
434 }
435}
436
437void GitQlient::updateWindowTitle()
438{
439
440 if (const auto currentTab = dynamic_cast<GitQlientRepo *>(mRepos->currentWidget()))
441 {
442 if (const auto repoPath = currentTab->currentDir(); !repoPath.isEmpty())
443 {
444 const auto currentName = repoPath.split("/").last();
445 const auto currentBranch = currentTab->currentBranch();
446
447 setWindowTitle(QString("GitQlient %1 - %2 (%3)").arg(VER, currentName, currentBranch));
448 }
449 }
450}
451