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 | |
16 | const char s_colourCodes[] = "\u001B[[0-9]*m" ; |
17 | |
18 | using namespace Phabricator; |
19 | |
20 | bool 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 | |
66 | void 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 | |
78 | void 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 | |
85 | QString 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 | |
94 | QStringList 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 | |
105 | NewDiffRev::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 | |
113 | void 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 | |
135 | UpdateDiffRev::UpdateDiffRev(const QUrl &patch, const QString &basedir, const QString &id, const QString &, 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 | |
150 | void 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 | |
171 | DiffRevList::DiffRevList(const QString &projectDir, QObject *parent) |
172 | : DifferentialRevision(QString(), parent) |
173 | , m_projectDir(projectDir) |
174 | { |
175 | buildArcCommand(workDir: m_projectDir); |
176 | } |
177 | |
178 | bool 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 | |
202 | void 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 | |