1 | #include "AGitProcess.h" |
2 | |
3 | #include <GitQlientSettings.h> |
4 | #include <QTemporaryFile> |
5 | #include <QTextStream> |
6 | |
7 | #include <QLogger.h> |
8 | |
9 | using namespace QLogger; |
10 | |
11 | namespace |
12 | { |
13 | QString 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 | |
27 | void 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 | |
55 | QStringList 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 | |
118 | AGitProcess::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 | |
129 | void AGitProcess::onCancel() |
130 | { |
131 | |
132 | mCanceling = true; |
133 | |
134 | waitForFinished(); |
135 | } |
136 | |
137 | void 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 | |
149 | bool 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 | |
181 | void 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 | |