1// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5#include "reposwidget.h"
6#include "amendswidget.h"
7#include "historyview.h"
8#include "historylogwidget.h"
9#include "historydiffwidget.h"
10#include "historydisplaywidget.h"
11#include "loggindialog.h"
12#include "filesourceview.h"
13#include "filemodifyview.h"
14#include "historydiffview.h"
15
16#include "common/common.h"
17
18#include "FileDiffView.h"
19#include "DiffHelper.h"
20
21#include <QApplication>
22#include <QButtonGroup>
23#include <QDir>
24#include <QLabel>
25#include <QMainWindow>
26#include <QMenu>
27#include <QSet>
28#include <QSplitter>
29#include <QToolBar>
30#include <QToolButton>
31#include <QFileSystemWatcher>
32#include <QDirIterator>
33
34class ReposWidgetPrivate
35{
36 friend class ReposWidget;
37 QSplitter *splitter{nullptr};
38 // QTimer fileModifyTimer;
39 FileSourceView *fileSrcView{nullptr};
40 AmendsWidget *amendsWidget{nullptr};
41 HistoryDisplayWidget *historyWidget{nullptr};
42 QVBoxLayout *vLayout{nullptr};
43 LogginDialog *logginDialog{nullptr};
44 QToolBar *controlBar{nullptr};
45 QButtonGroup *controlGroup{nullptr};
46 QToolButton *refreshButton{nullptr};
47 QToolButton *updateButton{nullptr};
48 QToolButton *optionButton{nullptr};
49 QToolButton *historyButton{nullptr};
50 QFileSystemWatcher *watcher{nullptr};
51 QString reposPath;
52 QString name;
53 QString passwd;
54 HistoryData currHistoryData;
55 RevisionFile currRevisonFile;
56 bool logginResult = false;
57};
58
59ReposWidget::ReposWidget(QWidget *parent)
60 : QWidget(parent)
61 , d(new ReposWidgetPrivate)
62{
63 d->vLayout = new QVBoxLayout();
64 d->logginDialog = new LogginDialog();
65 d->vLayout->addWidget(d->logginDialog);
66 d->vLayout->setAlignment(d->logginDialog, Qt::AlignCenter);
67 setLayout(d->vLayout);
68
69 d->splitter = new QSplitter(Qt::Horizontal);
70 d->splitter->setHandleWidth(2);
71 d->fileSrcView = new FileSourceView();
72 d->fileSrcView->setMinimumWidth(300);
73 d->amendsWidget = new AmendsWidget();
74 d->amendsWidget->setMinimumWidth(300);
75 d->historyWidget = new HistoryDisplayWidget();
76 d->historyWidget->setMinimumWidth(300);
77
78 d->watcher = new QFileSystemWatcher(this);
79
80 QObject::connect(d->watcher, &QFileSystemWatcher::directoryChanged,
81 [=](const QString &filePath){
82 this->reloadRevisionFiles();
83 });
84 QObject::connect(d->logginDialog, &LogginDialog::logginOk, this, &ReposWidget::doLoggin);
85}
86
87ReposWidget::~ReposWidget()
88{
89 if (d) {
90 delete d;
91 }
92}
93
94QString ReposWidget::getReposPath() const
95{
96 return d->reposPath;
97}
98
99void ReposWidget::setReposPath(const QString &path)
100{
101 d->reposPath = path;
102
103 d->logginDialog->setTitleText(LogginDialog::tr("loggin user from svn\nrepos path: %0")
104 .arg(d->reposPath));
105
106 QDirIterator dirItera(d->reposPath, QDir::Filter::NoDotAndDotDot|QDir::Dirs|QDir::NoSymLinks);
107 while (dirItera.hasNext()) {
108 dirItera.next();
109 qInfo() << dirItera.filePath();
110 d->watcher->addPath(dirItera.filePath());
111 }
112 d->watcher->addPath(path);
113 d->fileSrcView->setRootPath(d->reposPath);
114}
115
116void ReposWidget::loadRevisionFiles()
117{
118 if (svnProgram().isEmpty()) {
119 return;
120 }
121 QProcess process;
122 process.setProgram(svnProgram());
123 process.setWorkingDirectory(d->reposPath);
124 process.setArguments({"status"});
125 process.start();
126 process.waitForStarted();
127 process.waitForFinished();
128 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
129 ContextDialog::ok(process.readAllStandardError());
130 }
131
132 d->amendsWidget->modView()->setUpdatesEnabled(false);
133 RevisionFiles files;
134 while (process.canReadLine()) {
135 QString line = process.readLine();
136 QStringList lineList = line.replace("\n", "").split(" ");
137 QString filePath = d->reposPath + QDir::separator() + *lineList.rbegin();
138 RevisionFile file(*lineList.rbegin(), filePath, lineList.first());
139 d->amendsWidget->modView()->addFile(file);
140 files << file;
141 }
142 d->amendsWidget->modView()->setUpdatesEnabled(true);
143}
144
145void ReposWidget::reloadRevisionFiles()
146{
147 if (svnProgram().isEmpty()) {
148 return;
149 }
150 QProcess process;
151 process.setProgram(svnProgram());
152 process.setWorkingDirectory(d->reposPath);
153 process.setArguments({"status"});
154 process.start();
155 process.waitForStarted();
156 process.waitForFinished();
157 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
158 ContextDialog::ok(process.readAllStandardError());
159 }
160
161 d->amendsWidget->modView()->setUpdatesEnabled(false);
162 RevisionFiles newFiles;
163 while (process.canReadLine()) {
164 QString line = process.readLine();
165 QStringList lineList = line.replace("\n", "").split(" ");
166 QString filePath = d->reposPath + QDir::separator() + *lineList.rbegin();
167 RevisionFile file(*lineList.rbegin(), filePath, lineList.first());
168 newFiles << file;
169 }
170
171 // to changed added file list
172 for(int row = 0; row < d->amendsWidget->modView()->rowCount(); row ++) {
173 auto rowFile = d->amendsWidget->modView()->file(row); // row revision file
174 if (!newFiles.contains(rowFile)) {
175 if (d->amendsWidget->modView()->removeFile(rowFile)) //从界面删除节点
176 row --;
177 }
178 for (auto newFile : newFiles) {
179 if (rowFile == newFile) { // 包含则删除 相同数据
180 newFiles.removeOne(newFile); // 新的文件中删除当前数据
181 break;
182 } else if (rowFile.isSameFilePath(newFile) // 路径一致,类型不一致
183 && !rowFile.isSameReviType(newFile)) {
184 if (d->amendsWidget->modView()->removeFile(rowFile)) //从界面删除节点
185 row --;
186 }
187 }
188 }
189 d->amendsWidget->modView()->addFiles(newFiles);
190 d->amendsWidget->modView()->setUpdatesEnabled(true);
191}
192
193void ReposWidget::loadHistory()
194{
195 if (svnProgram().isEmpty()) {
196 return;
197 }
198 QProcess process;
199 process.setProgram(svnProgram());
200 process.setWorkingDirectory(d->reposPath);
201 process.setArguments({"log", "-v"});
202 process.start();
203 process.waitForStarted();
204 process.waitForFinished();
205 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
206 ContextDialog::ok(process.readAllStandardError());
207 return;
208 }
209
210 d->historyWidget->logWidget()->historyView()->setUpdatesEnabled(false);
211 HistoryDatas datas;
212 QString line = process.readLine();
213 while (process.canReadLine()) {
214 if (svnLogSplitStr() == line) {
215 HistoryData data;
216 // header
217 line = process.readLine(); // line 1
218 if (line.count("|") == 3) {
219 auto list = line.replace("\n", "").split(" | ");
220 if (list.size() == 4) {
221 data.revision = list[0];
222 data.user = list[1];
223 data.dateTime = list[2];
224 data.lineCount = list[3];
225 } else {
226 qCritical() << "Failed, Unkown error header from svn log -v";
227 abort();
228 }
229 }
230
231 // files
232 line = process.readLine(); // line 2
233 for (line = process.readLine(); line != "\n"; line = process.readLine()) {
234 while (line.startsWith(" "))
235 line = line.remove(0, 1);
236 auto list = line.replace("\n", "").split(" ");
237 RevisionFile rFile;
238 if (list.size() == 2) {
239 rFile.revisionType = list[0];
240 auto fileNameTemp = list[1];
241 if (fileNameTemp.startsWith("/"))
242 fileNameTemp.remove(0, 1);
243 rFile.displayName = fileNameTemp;
244 rFile.filePath = d->reposPath + QDir::separator() + fileNameTemp;
245 }
246 if (rFile.isValid()) {
247 data.changedFiles << rFile;
248 } else {
249 qCritical() << "Failed, Unkown error revision file from svn log -v";
250 }
251 }
252
253 // desc
254 QString descStr;
255 for (line = process.readLine();
256 line != "\n" && line != svnLogSplitStr();
257 line = process.readLine()) {
258 descStr += line;
259 }
260 data.description = descStr;
261 descStr.clear();
262 datas << data;
263 for (;line != svnLogSplitStr(); line = process.readLine()) {
264
265 }
266 }
267 }
268 d->historyWidget->logWidget()->historyView()->setDatas(datas);
269 d->historyWidget->logWidget()->historyView()->setUpdatesEnabled(true);
270}
271
272void ReposWidget::reloadHistory()
273{
274 if (svnProgram().isEmpty()) {
275 return;
276 }
277 QProcess process;
278 process.setProgram(svnProgram());
279 process.setWorkingDirectory(d->reposPath);
280 process.setArguments({"log", "-v"});
281 process.start();
282 process.waitForStarted();
283 process.waitForFinished();
284 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
285 ContextDialog::ok(process.readAllStandardError());
286 return;
287 }
288
289 d->historyWidget->logWidget()->historyView()->setUpdatesEnabled(false);
290 HistoryDatas datas;
291 QString line = process.readLine();
292 while (process.canReadLine()) {
293 if (svnLogSplitStr() == line) {
294 HistoryData data;
295 // header
296 line = process.readLine(); // line 1
297 if (line.count("|") == 3) {
298 auto list = line.replace("\n", "").split(" | ");
299 if (list.size() == 4) {
300 data.revision = list[0];
301 data.user = list[1];
302 data.dateTime = list[2];
303 data.lineCount = list[3];
304 } else {
305 qCritical() << "Failed, Unkown error header from svn log -v";
306 abort();
307 }
308 }
309
310 // files
311 line = process.readLine(); // line 2
312 for (line = process.readLine(); line != "\n"; line = process.readLine()) {
313 while (line.startsWith(" "))
314 line = line.remove(0, 1);
315 auto list = line.replace("\n", "").split(" ");
316 RevisionFile rFile;
317 if (list.size() == 2) {
318 rFile.revisionType = list[0];
319 auto fileNameTemp = list[1];
320 if (fileNameTemp.startsWith("/"))
321 fileNameTemp.remove(0, 1);
322 rFile.displayName = fileNameTemp;
323 rFile.filePath = d->reposPath + QDir::separator() + fileNameTemp;
324 }
325 if (rFile.isValid()) {
326 data.changedFiles << rFile;
327 } else {
328 qCritical() << "Failed, Unkown error revision file from svn log -v";
329 }
330 }
331
332 // desc
333 QString descStr;
334 for (line = process.readLine();
335 line != "\n" && line != svnLogSplitStr();
336 line = process.readLine()) {
337 descStr += line;
338 }
339 data.description = descStr;
340 descStr.clear();
341 if (data == d->historyWidget->logWidget()->historyView()->data(0)) {
342 break;
343 }
344 datas << data;
345 for (;line != svnLogSplitStr(); line = process.readLine()) {
346
347 }
348 }
349 }
350 d->historyWidget->logWidget()->historyView()->insertTopDatas(datas);
351 d->historyWidget->logWidget()->historyView()->setUpdatesEnabled(true);
352}
353
354void ReposWidget::modFileMenu(const RevisionFile &file, const QPoint &pos)
355{
356 QMenu menu;
357 if (file.revisionType == AmendsState_Col1::get()->ADD) {
358 QAction *action = menu.addAction("revert");
359 QObject::connect(action, &QAction::triggered, [=](){
360 this->revert(file.displayName);
361 this->reloadRevisionFiles();
362 });
363 }
364 if (file.revisionType == AmendsState_Col1::get()->SRC) {
365 QAction *action = menu.addAction("add");
366 QObject::connect(action, &QAction::triggered, [=](){
367 this->add(file.displayName);
368 this->reloadRevisionFiles();
369 });
370 }
371 menu.exec(pos);
372}
373
374void ReposWidget::historyDataClicked(const QModelIndex &index)
375{
376 auto hisView = d->historyWidget->logWidget()->historyView();
377 d->currHistoryData = hisView->data(index.row());
378}
379
380void ReposWidget::historyFileClicked(const QModelIndex &index)
381{
382 auto chaView = d->historyWidget->logWidget()->fileChangedView();
383 d->currRevisonFile = chaView->file(index.row());
384 if (d->currHistoryData.isValid() && d->currRevisonFile.isValid()) {
385 doDiffFileAtRevision();
386 }
387}
388
389void ReposWidget::setSrcViewReviFiles(const QString &path)
390{
391 qInfo() << path;
392 reloadRevisionFiles();
393 d->amendsWidget->modView()->files();
394}
395
396void ReposWidget::doLoggin()
397{
398 // 设置用户名密码缓存
399 setName(d->logginDialog->name());
400 setPasswd(d->logginDialog->passwd());
401
402 // 获取登录结果
403 d->logginResult = testUserLoggin(d->reposPath, d->name, d->passwd);
404
405 // 无法登录 直接中断流程
406 if (!d->logginResult) {
407 return;
408 }
409
410 // 设置界面
411 d->vLayout->addWidget(initControlBar(), 0, Qt::AlignHCenter);
412 d->vLayout->addWidget(d->splitter);
413 d->splitter->addWidget(d->fileSrcView);
414 d->splitter->setCollapsible(0, false);
415 d->splitter->addWidget(d->amendsWidget);
416 d->splitter->setCollapsible(1, false);
417 d->splitter->addWidget(d->historyWidget);
418 d->splitter->setCollapsible(2, false);
419
420 QObject::connect(d->amendsWidget->modView(),
421 &FileModifyView::menuRequest,
422 this, &ReposWidget::modFileMenu);
423
424 // 历史记录操作
425 auto hisView = d->historyWidget->logWidget()->historyView();
426 QObject::connect(hisView, &HistoryView::clicked,
427 this, &ReposWidget::historyDataClicked);
428
429 auto chaView = d->historyWidget->logWidget()->fileChangedView();
430 QObject::connect(chaView, &FileModifyView::clicked,
431 this, &ReposWidget::historyFileClicked);
432
433 loadRevisionFiles(); // 创建修订文件
434 loadHistory(); // 添加提交历史
435
436 // 提交信息操作
437 QObject::connect(d->amendsWidget, &AmendsWidget::commitClicked,
438 this, &ReposWidget::doAmendsCommit);
439
440 QObject::connect(d->amendsWidget, &AmendsWidget::revertAllClicked,
441 this, &ReposWidget::doAmendsRevertAll);
442
443 // 删除登录界面
444 if (d->logginDialog)
445 delete d->logginDialog;
446}
447
448void ReposWidget::doUpdateRepos()
449{
450 if (svnProgram().isEmpty()) {
451 return;
452 }
453 QProcess processUpdate;
454 processUpdate.setWorkingDirectory(d->reposPath);
455 processUpdate.setProgram(svnProgram());
456 processUpdate.setArguments({"update"});
457 processUpdate.start();
458 processUpdate.waitForFinished();
459 if (processUpdate.exitCode() != 0 || processUpdate.exitStatus() != QProcess::ExitStatus::NormalExit) {
460 ContextDialog::ok(processUpdate.readAllStandardError());
461 }
462 doRefresh();
463}
464
465void ReposWidget::doRefresh()
466{
467 reloadRevisionFiles();
468 reloadHistory();
469}
470
471void ReposWidget::doAmendsCommit()
472{
473 if (svnProgram().isEmpty()) {
474 return;
475 }
476 QProcess processCommit;
477 processCommit.setWorkingDirectory(d->reposPath);
478 processCommit.setProgram(svnProgram());
479 QString commitDesc = d->amendsWidget->description();
480 processCommit.setArguments({"commit", "-m", commitDesc, "--username", d->name, "--password", d->passwd});
481 processCommit.start();
482 processCommit.waitForFinished();
483 if (processCommit.exitCode() != 0 || processCommit.exitStatus() != QProcess::ExitStatus::NormalExit) {
484 ContextDialog::ok(processCommit.readAllStandardError());
485 }
486 reloadRevisionFiles();
487}
488
489void ReposWidget::doAmendsRevertAll()
490{
491 if (svnProgram().isEmpty()) {
492 return;
493 }
494 QProcess process;
495 process.setProgram(svnProgram());
496 process.setWorkingDirectory(d->reposPath);
497 process.setArguments({"revert", d->reposPath, "--depth", "infinity"});
498 process.start();
499 process.waitForFinished();
500 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
501 ContextDialog::ok(process.readAllStandardError());
502 return;
503 }
504 qInfo() << process.errorString() << QString::fromUtf8(process.readAll());
505 reloadRevisionFiles();
506}
507
508void ReposWidget::doDiffFileAtRevision()
509{
510 if (svnProgram().isEmpty()) {
511 return;
512 }
513 QProcess process;
514 process.setProgram(svnProgram());
515 process.setWorkingDirectory(d->reposPath);
516 process.setArguments({"diff", "--git", d->currRevisonFile.displayName, "-r", d->currHistoryData.revision});
517 process.start();
518 process.waitForFinished();
519 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
520 ContextDialog::ok(process.readAllStandardError());
521 return;
522 }
523
524 QString text = process.readAll();
525 QPair<QStringList, QVector<ChunkDiffInfo::ChunkInfo>> newFileData;
526 QPair<QStringList, QVector<ChunkDiffInfo::ChunkInfo>> oldFileData;
527 auto info = DiffHelper::processDiff(text, newFileData, oldFileData);
528
529 d->historyWidget->diffWidget()->getOldView()->getDiffView()->loadDiff(oldFileData.first.join('\n'), oldFileData.second);
530 d->historyWidget->diffWidget()->getNewView()->getDiffView()->loadDiff(newFileData.first.join('\n'), newFileData.second);
531 qInfo() << "jump";
532}
533
534bool ReposWidget::testUserLoggin(const QString &reposPath, const QString &name, const QString &passwd)
535{
536 if (svnProgram().isEmpty()) {
537 return false;
538 }
539 QProcess process;
540 process.setProgram(svnProgram());
541 process.setArguments({"list", reposPath, "--username", name, "--password", passwd});
542 process.start();
543 process.waitForFinished();
544 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
545 ContextDialog::ok(process.readAllStandardError());
546 return false;
547 }
548 return true;
549}
550
551QWidget *ReposWidget::initControlBar()
552{
553 static int barHeight = 48;
554 static int buttonWidth = 40;
555 static int buttonHeight = 40;
556 d->controlBar = new QToolBar();
557 d->controlBar->setFixedHeight(barHeight);
558 d->controlBar->setOrientation(Qt::Orientation::Horizontal);
559 d->controlBar->setIconSize({buttonWidth, buttonHeight});
560
561 d->updateButton = new QToolButton();
562 d->updateButton->setFixedSize(buttonWidth, buttonHeight);
563 d->updateButton->setIcon(QIcon(":/icons/git_pull"));
564 d->updateButton->setToolTip(QToolButton::tr("update local from remote repos"));
565 QObject::connect(d->updateButton, &QToolButton::clicked, this, &ReposWidget::doUpdateRepos);
566 d->controlBar->addWidget(d->updateButton);
567
568 d->refreshButton = new QToolButton();
569 d->refreshButton->setFixedSize(buttonWidth, buttonHeight);
570 d->refreshButton->setIcon(QIcon(":/icons/refresh"));
571 d->refreshButton->setToolTip(QToolButton::tr("refresh current local to display"));
572 QObject::connect(d->refreshButton, &QToolButton::clicked, this, &ReposWidget::doRefresh);
573 d->controlBar->addWidget(d->refreshButton);
574 d->controlBar->addSeparator();
575
576 d->optionButton = new QToolButton();
577 d->optionButton->setFixedSize(buttonWidth, buttonHeight);
578 d->optionButton->setIcon(QIcon(":/icons/blame"));
579 d->optionButton->setToolTip(QToolButton::tr("show repos operation"));
580 d->optionButton->setCheckable(true);
581 d->controlBar->addWidget(d->optionButton);
582
583 d->historyButton = new QToolButton();
584 d->historyButton->setFixedSize(buttonWidth, buttonHeight);
585 d->historyButton->setIcon(QIcon(":/icons/git_orange"));
586 d->historyButton->setToolTip(QToolButton::tr("show repos history"));
587 d->historyButton->setCheckable(true);
588 d->controlBar->addWidget(d->historyButton);
589
590 d->controlGroup = new QButtonGroup(d->controlBar);
591 d->controlGroup->addButton(d->optionButton);
592 d->controlGroup->addButton(d->historyButton);
593
594 QObject::connect(d->controlGroup, QOverload<QAbstractButton *, bool>
595 ::of(&QButtonGroup::buttonToggled),
596 [=](QAbstractButton *button, bool checked){
597 if (button == d->optionButton) {
598 if (checked) {
599 d->fileSrcView->show();
600 d->amendsWidget->show();
601 } else {
602 d->fileSrcView->hide();
603 d->amendsWidget->hide();
604 }
605 }
606 if (button == d->historyButton) {
607 if (checked) {
608 d->historyWidget->show();
609 } else {
610 d->historyWidget->hide();
611 }
612 }
613 });
614
615 d->fileSrcView->hide();
616 d->amendsWidget->hide();
617 d->historyWidget->hide();
618
619 d->optionButton->setChecked(true);
620
621 return d->controlBar;
622}
623
624bool ReposWidget::add(const QString &display)
625{
626 if (svnProgram().isEmpty()) {
627 return false;
628 }
629 QProcess process;
630 process.setProgram(svnProgram());
631 process.setWorkingDirectory(d->reposPath);
632 process.setArguments({"add", display});
633 process.start();
634 process.waitForStarted();
635 process.waitForFinished();
636 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
637 ContextDialog::ok(process.readAllStandardError());
638 return false;
639 }
640 return true;
641}
642
643bool ReposWidget::revert(const QString &display)
644{
645 if (svnProgram().isEmpty()) {
646 return false;
647 }
648 QProcess process;
649 process.setProgram(svnProgram());
650 process.setWorkingDirectory(d->reposPath);
651 process.setArguments({"revert", display});
652 process.start();
653 process.waitForStarted();
654 process.waitForFinished();
655 if (process.exitCode() != 0 || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
656 ContextDialog::ok(process.readAllStandardError());
657 return false;
658 }
659 return true;
660}
661
662QString ReposWidget::getName() const
663{
664 return d->name;
665}
666
667void ReposWidget::setName(const QString &value)
668{
669 d->name = value;
670 d->logginDialog->setName(value);
671}
672
673QString ReposWidget::getPasswd() const
674{
675 return d->passwd;
676}
677
678void ReposWidget::setPasswd(const QString &value)
679{
680 d->passwd = value;
681 d->logginDialog->setPasswd(value);
682}
683
684void ReposWidget::setLogginDisplay(const QString &name, const QString &passwd)
685{
686 d->logginDialog->setName(name);
687 d->logginDialog->setPasswd(passwd);
688}
689