1#include "AGitProcess.h"
2
3#include <GitQlientSettings.h>
4#include <QTemporaryFile>
5#include <QTextStream>
6
7#include <QLogger.h>
8
9using namespace QLogger;
10
11namespace
12{
13QString loginApp()
14{
15 const auto askPassApp = qEnvironmentVariable("SSH_ASKPASS");
16
17 if (!askPassApp.isEmpty())
18 return QString("%1=%2").arg("SSH_ASKPASS", askPassApp);
19
20#if defined(Q_OS_WIN)
21 return QString("SSH_ASKPASS=win-ssh-askpass");
22#else
23 return QString("SSH_ASKPASS=ssh-askpass");
24#endif
25}
26
27void restoreSpaces(QString &newCmd, const QChar &sepChar)
28{
29 QChar quoteChar;
30 auto replace = false;
31 const auto newCommandLength = newCmd.length();
32
33 for (int i = 0; i < newCommandLength; ++i)
34 {
35 const auto c = newCmd[i];
36
37 if (!replace && (c == "$"[0] || c == '\"' || c == '\'') && (newCmd.count(c) % 2 == 0))
38 {
39 replace = true;
40 quoteChar = c;
41 continue;
42 }
43
44 if (replace && (c == quoteChar))
45 {
46 replace = false;
47 continue;
48 }
49
50 if (replace && c == sepChar)
51 newCmd[i] = QChar(' ');
52 }
53}
54
55QStringList splitArgList(const QString &cmd)
56{
57 // return argument list handling quotes and double quotes
58 // substring, as example from:
59 // cmd some_arg "some thing" v='some value'
60 // to (comma separated fields)
61 // sl = <cmd,some_arg,some thing,v='some value'>
62
63 // early exit the common case
64 if (!(cmd.contains("$") || cmd.contains("\"") || cmd.contains("\'")))
65#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
66 return cmd.split(' ', Qt::SkipEmptyParts);
67#else
68 return cmd.split(' ', QString::SkipEmptyParts);
69#endif
70
71 // we have some work to do...
72 // first find a possible separator
73 const QString sepList("#%&!?"); // separator candidates
74 int i = 0;
75 while (cmd.contains(sepList[i]) && i < sepList.length())
76 i++;
77
78 if (i == sepList.length())
79 return QStringList();
80
81 const QChar &sepChar(sepList[i]);
82
83 // remove all spaces
84 QString newCmd(cmd);
85 newCmd.replace(QChar(' '), sepChar);
86
87 // re-add spaces in quoted sections
88 restoreSpaces(newCmd, sepChar);
89
90 // "$" is used internally to delimit arguments
91 // with quoted text wholly inside as
92 // arg1 = <[patch] cool patch on "cool feature">
93 // and should be removed before to feed QProcess
94 newCmd.remove("$");
95
96 // QProcess::setArguments doesn't want quote
97 // delimited arguments, so remove trailing quotes
98
99#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
100 auto sl = QStringList(newCmd.split(sepChar, Qt::SkipEmptyParts));
101#else
102 auto sl = QStringList(newCmd.split(sepChar, QString::SkipEmptyParts));
103#endif
104 QStringList::iterator it(sl.begin());
105
106 for (; it != sl.end(); ++it)
107 {
108 if (it->isEmpty())
109 continue;
110
111 if (((*it).at(0) == "\"" && (*it).right(1) == "\"") || ((*it).at(0) == "\'" && (*it).right(1) == "\'"))
112 *it = (*it).mid(1, (*it).length() - 2);
113 }
114 return sl;
115}
116}
117
118AGitProcess::AGitProcess(const QString &workingDir)
119 : mWorkingDirectory(workingDir)
120{
121 setWorkingDirectory(mWorkingDirectory);
122
123 connect(this, &AGitProcess::readyReadStandardOutput, this, &AGitProcess::onReadyStandardOutput,
124 Qt::DirectConnection);
125 connect(this, static_cast<void (AGitProcess::*)(int, QProcess::ExitStatus)>(&AGitProcess::finished), this,
126 &AGitProcess::onFinished, Qt::DirectConnection);
127}
128
129void AGitProcess::onCancel()
130{
131
132 mCanceling = true;
133
134 waitForFinished();
135}
136
137void AGitProcess::onReadyStandardOutput()
138{
139 if (!mCanceling)
140 {
141 const auto standardOutput = readAllStandardOutput();
142
143 mRunOutput.append(QString::fromUtf8(standardOutput));
144
145 emit procDataReady(standardOutput);
146 }
147}
148
149bool AGitProcess::execute(const QString &command)
150{
151 mCommand = command;
152
153 auto processStarted = false;
154 auto arguments = splitArgList(mCommand);
155
156 if (!arguments.isEmpty())
157 {
158 QStringList env = QProcess::systemEnvironment();
159 env << "GIT_TRACE=0"; // avoid choking on debug traces
160 env << "GIT_FLUSH=0"; // skip the fflush() in 'git log'
161 env << loginApp();
162
163 const auto gitAlternative = GitQlientSettings().globalValue("gitLocation", "").toString();
164
165 setEnvironment(env);
166 setProgram(gitAlternative.isEmpty() ? arguments.takeFirst() : gitAlternative);
167 setArguments(arguments);
168 start();
169
170 processStarted = waitForStarted();
171
172 if (!processStarted)
173 QLog_Warning("Git", QString("Unable to start the process:\n%1\nMore info:\n%2").arg(mCommand, errorString()));
174 else
175 QLog_Debug("Git", QString("Process started: %1").arg(mCommand));
176 }
177
178 return processStarted;
179}
180
181void AGitProcess::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
182{
183 Q_UNUSED(exitCode)
184
185 QLog_Debug("Git", QString("Process {%1} finished.").arg(mCommand));
186
187 const auto errorOutput = readAllStandardError();
188
189 mErrorOutput = QString::fromUtf8(errorOutput);
190 mRealError = exitStatus != QProcess::NormalExit || mCanceling || errorOutput.contains("error") || errorOutput.contains("fatal: ")
191 || errorOutput.toLower().contains("could not read username");
192
193 if (mRealError)
194 {
195 if (!mErrorOutput.isEmpty())
196 mRunOutput = mErrorOutput;
197 }
198 else
199 mRunOutput.append(readAllStandardOutput() + mErrorOutput);
200}
201