1/*
2 SPDX-FileCopyrightText: 2017 René J.V. Bertin <rjvbertin@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "phabricatorjobs.h"
8#include "debug.h"
9
10#include <QDir>
11#include <QRegularExpression>
12#include <QStandardPaths>
13
14#include <KLocalizedString>
15
16const char s_colourCodes[] = "\u001B[[0-9]*m";
17
18using namespace Phabricator;
19
20bool DifferentialRevision::buildArcCommand(const QString &workDir, const QString &patchFile, bool doBrowse)
21{
22 bool ret;
23 QString arc = QStandardPaths::findExecutable(QStringLiteral("arc"));
24 if (!arc.isEmpty()) {
25 QStringList args;
26 args << QStringLiteral("diff");
27 if (m_id.isEmpty()) {
28 // creating a new differential revision (review request)
29 // the fact we skip "--create" means we'll be creating a new "differential diff"
30 // which obliges the user to fill in the details we cannot provide through the plugin ATM.
31 // TODO: grab the TARGET_GROUPS from .reviewboardrc and pass that via --reviewers
32 } else {
33 // updating an existing differential revision (review request)
34 args << QStringLiteral("--update") << m_id;
35 }
36 args << QStringLiteral("--excuse") << QStringLiteral("patch submitted with the purpose/phabricator plugin");
37 if (m_commit.isEmpty()) {
38 args << QStringLiteral("--raw");
39 } else {
40 args << QStringLiteral("--allow-untracked") << QStringLiteral("--ignore-unsound-tests") << QStringLiteral("--nolint") << QStringLiteral("-nounit")
41 << QStringLiteral("--verbatim") << m_commit;
42 }
43 if (doBrowse) {
44 args << QStringLiteral("--browse");
45 }
46 m_arcCmd.setProgram(arc);
47 m_arcCmd.setArguments(args);
48 if (!patchFile.isEmpty()) {
49 m_arcCmd.setStandardInputFile(patchFile);
50 m_arcInput = patchFile;
51 }
52 m_arcCmd.setWorkingDirectory(workDir);
53 connect(sender: &m_arcCmd, signal: &QProcess::finished, context: this, slot: &DifferentialRevision::done);
54 setPercent(33);
55 ret = true;
56 } else {
57 qCWarning(PLUGIN_PHABRICATOR) << "Could not find 'arc' in the path";
58 setError(KJob::UserDefinedError + 3);
59 setErrorText(i18n("Could not find the 'arc' command"));
60 setErrorString(errorText());
61 ret = false;
62 }
63 return ret;
64}
65
66void DifferentialRevision::start()
67{
68 if (!m_arcCmd.program().isEmpty()) {
69 qCDebug(PLUGIN_PHABRICATOR) << "starting" << m_arcCmd.program() << m_arcCmd.arguments();
70 qCDebug(PLUGIN_PHABRICATOR) << "\twordDir=" << m_arcCmd.workingDirectory() << "stdin=" << m_arcInput;
71 m_arcCmd.start();
72 if (m_arcCmd.waitForStarted(msecs: 5000)) {
73 setPercent(66);
74 }
75 }
76}
77
78void DifferentialRevision::setErrorString(const QString &msg)
79{
80 QRegularExpression unwanted(QString::fromUtf8(utf8: s_colourCodes));
81 m_errorString = msg;
82 m_errorString.replace(re: unwanted, after: QString());
83}
84
85QString DifferentialRevision::scrubbedResult()
86{
87 QString result = QString::fromUtf8(ba: m_arcCmd.readAllStandardOutput());
88 // the return string can contain terminal text colour codes: remove them.
89 QRegularExpression unwanted(QString::fromUtf8(utf8: s_colourCodes));
90 result.replace(re: unwanted, after: QString());
91 return result;
92}
93
94QStringList DifferentialRevision::scrubbedResultList()
95{
96 QStringList result = QString::fromUtf8(ba: m_arcCmd.readAllStandardOutput()).split(sep: QChar::LineFeed);
97 // the return string can contain terminal text colour codes: remove them.
98 QRegularExpression unwanted(QString::fromUtf8(utf8: s_colourCodes));
99 result.replaceInStrings(re: unwanted, after: QString());
100 // remove all (now) empty strings
101 result.removeAll(t: QString());
102 return result;
103}
104
105NewDiffRev::NewDiffRev(const QUrl &patch, const QString &projectPath, bool doBrowse, QObject *parent)
106 : DifferentialRevision(QString(), parent)
107 , m_patch(patch)
108 , m_project(projectPath)
109{
110 buildArcCommand(workDir: projectPath, patchFile: patch.toLocalFile(), doBrowse);
111}
112
113void NewDiffRev::done(int exitCode, QProcess::ExitStatus exitStatus)
114{
115 if (exitStatus != QProcess::NormalExit || exitCode) {
116 setError(KJob::UserDefinedError + exitCode);
117 setErrorText(i18n("Could not create the new \"differential diff\""));
118 setErrorString(QString::fromUtf8(ba: m_arcCmd.readAllStandardError()));
119 qCWarning(PLUGIN_PHABRICATOR) << "Could not create the new \"differential diff\":" << m_arcCmd.error() << ";" << errorString();
120 } else {
121 setPercent(99);
122 const QString arcOutput = scrubbedResult();
123 const char *diffOpCode = "Diff URI: ";
124 int diffOffset = arcOutput.indexOf(s: QLatin1String(diffOpCode));
125 if (diffOffset >= 0) {
126 m_diffURI = arcOutput.mid(position: diffOffset + strlen(s: diffOpCode)).split(sep: QChar::LineFeed).at(i: 0);
127 } else {
128 m_diffURI = arcOutput;
129 }
130 }
131
132 emitResult();
133}
134
135UpdateDiffRev::UpdateDiffRev(const QUrl &patch, const QString &basedir, const QString &id, const QString &updateComment, bool doBrowse, QObject *parent)
136 : DifferentialRevision(id, parent)
137 , m_patch(patch)
138 , m_basedir(basedir)
139{
140 buildArcCommand(workDir: m_basedir, patchFile: m_patch.toLocalFile(), doBrowse);
141 QStringList args = m_arcCmd.arguments();
142 if (updateComment.isEmpty()) {
143 args << QStringLiteral("--message") << QStringLiteral("<placeholder: patch updated via the purpose/phabricator plugin>");
144 } else {
145 args << QStringLiteral("--message") << updateComment;
146 }
147 m_arcCmd.setArguments(args);
148}
149
150void UpdateDiffRev::done(int exitCode, QProcess::ExitStatus exitStatus)
151{
152 if (exitStatus != QProcess::NormalExit || exitCode) {
153 setError(KJob::UserDefinedError + exitCode);
154 setErrorText(i18n("Patch upload to Phabricator failed"));
155 setErrorString(QString::fromUtf8(ba: m_arcCmd.readAllStandardError()));
156 qCWarning(PLUGIN_PHABRICATOR) << "Patch upload to Phabricator failed with exit code" << exitCode << ", error" << m_arcCmd.error() << ";"
157 << errorString();
158 } else {
159 const QString arcOutput = scrubbedResult();
160 const char *diffOpCode = "Revision URI: ";
161 int diffOffset = arcOutput.indexOf(s: QLatin1String(diffOpCode));
162 if (diffOffset >= 0) {
163 m_diffURI = arcOutput.mid(position: diffOffset + strlen(s: diffOpCode)).split(sep: QChar::LineFeed).at(i: 0);
164 } else {
165 m_diffURI = arcOutput;
166 }
167 }
168 emitResult();
169}
170
171DiffRevList::DiffRevList(const QString &projectDir, QObject *parent)
172 : DifferentialRevision(QString(), parent)
173 , m_projectDir(projectDir)
174{
175 buildArcCommand(workDir: m_projectDir);
176}
177
178bool Phabricator::DiffRevList::buildArcCommand(const QString &workDir, const QString &unused, bool)
179{
180 Q_UNUSED(unused)
181 bool ret;
182 QString arc = QStandardPaths::findExecutable(QStringLiteral("arc"));
183 if (!arc.isEmpty()) {
184 QStringList args;
185 args << QStringLiteral("list");
186 m_arcCmd.setProgram(arc);
187 m_arcCmd.setArguments(args);
188 m_arcCmd.setWorkingDirectory(workDir);
189 connect(sender: &m_arcCmd, signal: &QProcess::finished, context: this, slot: &DiffRevList::done);
190 setPercent(33);
191 ret = true;
192 } else {
193 qCWarning(PLUGIN_PHABRICATOR) << "Could not find 'arc' in the path";
194 setError(KJob::UserDefinedError + 3);
195 setErrorText(i18n("Could not find the 'arc' command"));
196 setErrorString(errorText());
197 ret = false;
198 }
199 return ret;
200}
201
202void DiffRevList::done(int exitCode, QProcess::ExitStatus exitStatus)
203{
204 if (exitStatus != QProcess::NormalExit || exitCode) {
205 setError(KJob::UserDefinedError + exitCode);
206 setErrorText(i18n("Could not get list of differential revisions in %1", QDir::currentPath()));
207 setErrorString(QString::fromUtf8(ba: m_arcCmd.readAllStandardError()));
208 qCWarning(PLUGIN_PHABRICATOR) << "Could not get list of differential revisions" << m_arcCmd.error() << ";" << errorString();
209 } else {
210 setPercent(99);
211 const QStringList reviews = scrubbedResultList();
212 qCDebug(PLUGIN_PHABRICATOR) << "arc list returned:" << reviews;
213 const QRegularExpression revIDExpr(QStringLiteral(" D[0-9][0-9]*: "));
214 for (auto rev : reviews) {
215 int idStart = rev.indexOf(re: revIDExpr);
216 if (idStart >= 0) {
217 QString revID = rev.mid(position: idStart + 1).split(sep: QString::fromUtf8(utf8: ": ")).at(i: 0);
218 QString revTitle = rev.section(re: revIDExpr, start: 1);
219 if (rev.startsWith(QStringLiteral("* Accepted "))) {
220 // append a Unicode "Heavy Check Mark" to signal accepted revisions
221 revTitle += QStringLiteral(" ") + QString(QChar(0x2714));
222 m_statusMap[revTitle] = Accepted;
223 } else if (rev.startsWith(QStringLiteral("* Needs Revision "))) {
224 // append a Unicode "Heavy Ballot X" for lack of a Unicode glyph
225 // resembling the icon used on the Phab site.
226 revTitle += QStringLiteral(" ") + QString(QChar(0x2718));
227 m_statusMap[revTitle] = NeedsRevision;
228 } else if (rev.startsWith(QStringLiteral("* Needs Review "))) {
229 m_statusMap[revTitle] = NeedsReview;
230 }
231 m_reviews << qMakePair(value1&: revID, value2&: revTitle);
232 m_revMap[revTitle] = revID;
233 }
234 }
235 }
236 emitResult();
237}
238
239#include "moc_phabricatorjobs.cpp"
240

source code of purpose/src/plugins/phabricator/phabricatorjobs.cpp