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 | |
29 | using namespace QLogger; |
30 | |
31 | GitQlient::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 = new QPushButton(); |
46 | const auto = 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 | |
122 | GitQlient::~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 | |
143 | bool GitQlient::eventFilter(QObject *obj, QEvent *event) |
144 | { |
145 | |
146 | if (const auto = 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 | |
159 | void 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 | |
168 | void GitQlient::openRepoWithPath(const QString &path) |
169 | { |
170 | QDir d(path); |
171 | addRepoTab(d.absolutePath()); |
172 | } |
173 | |
174 | void 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 | |
187 | void GitQlient::initRepo() |
188 | { |
189 | CreateRepoDlg cloneDlg(CreateRepoDlgType::INIT, mGit, this); |
190 | connect(&cloneDlg, &CreateRepoDlg::signalOpenWhenFinish, this, &GitQlient::openRepoWithPath); |
191 | cloneDlg.exec(); |
192 | } |
193 | |
194 | void 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 | |
213 | void GitQlient::showError(int, QString description) |
214 | { |
215 | if (mProgressDlg) |
216 | mProgressDlg->deleteLater(); |
217 | |
218 | QMessageBox::critical(this, tr("Error!" ), description); |
219 | } |
220 | |
221 | void 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 | |
229 | bool 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 | |
240 | bool 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 | |
304 | void GitQlient::addRepoTab(const QString &repoPath) |
305 | { |
306 | addNewRepoTab(repoPath, false); |
307 | } |
308 | |
309 | void 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 | |
388 | void 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 | |
406 | void 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 | |
415 | void GitQlient::onSuccessOpen(const QString &fullPath) |
416 | { |
417 | GitQlientSettings().setProjectOpened(fullPath); |
418 | |
419 | mConfigWidget->onRepoOpened(); |
420 | } |
421 | |
422 | void 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 | |
437 | void 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 | |