1 /*
2 SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "gitutils.h"
8
9 #include <gitprocess.h>
10
11 #include <QDateTime>
12 #include <QDebug>
13 #include <QProcess>
14
isGitRepo(const QString & repo)15 bool GitUtils::isGitRepo(const QString &repo)
16 {
17 QProcess git;
18 if (!setupGitProcess(git, repo, {QStringLiteral("rev-parse"), QStringLiteral("--is-inside-work-tree")})) {
19 return false;
20 }
21
22 git.start(QProcess::ReadOnly);
23 if (git.waitForStarted() && git.waitForFinished(-1)) {
24 return git.readAll().trimmed() == "true";
25 }
26 return false;
27 }
28
getDotGitPath(const QString & repo)29 std::optional<QString> GitUtils::getDotGitPath(const QString &repo)
30 {
31 /* This call is intentionally blocking because we need git path for everything else */
32 QProcess git;
33 if (!setupGitProcess(git, repo, {QStringLiteral("rev-parse"), QStringLiteral("--absolute-git-dir")})) {
34 return std::nullopt;
35 }
36
37 git.start(QProcess::ReadOnly);
38 if (git.waitForStarted() && git.waitForFinished(-1)) {
39 if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
40 return std::nullopt;
41 }
42 QString dotGitPath = QString::fromUtf8(git.readAllStandardOutput());
43 if (dotGitPath.endsWith(QLatin1String("\n"))) {
44 dotGitPath.remove(QLatin1String(".git\n"));
45 } else {
46 dotGitPath.remove(QLatin1String(".git"));
47 }
48 return dotGitPath;
49 }
50 return std::nullopt;
51 }
52
getCurrentBranchName(const QString & repo)53 QString GitUtils::getCurrentBranchName(const QString &repo)
54 {
55 // clang-format off
56 QStringList argsList[3] =
57 {
58 {QStringLiteral("symbolic-ref"), QStringLiteral("--short"), QStringLiteral("HEAD")},
59 {QStringLiteral("describe"), QStringLiteral("--exact-match"), QStringLiteral("HEAD")},
60 {QStringLiteral("rev-parse"), QStringLiteral("--short"), QStringLiteral("HEAD")}
61 };
62 // clang-format on
63
64 for (int i = 0; i < 3; ++i) {
65 QProcess git;
66 if (!setupGitProcess(git, repo, argsList[i])) {
67 return QString();
68 }
69
70 git.start(QProcess::ReadOnly);
71 if (git.waitForStarted() && git.waitForFinished(-1)) {
72 if (git.exitStatus() == QProcess::NormalExit && git.exitCode() == 0) {
73 return QString::fromUtf8(git.readAllStandardOutput().trimmed());
74 }
75 }
76 }
77
78 // give up
79 return QString();
80 }
81
checkoutBranch(const QString & repo,const QString & branch)82 GitUtils::CheckoutResult GitUtils::checkoutBranch(const QString &repo, const QString &branch)
83 {
84 QProcess git;
85 if (!setupGitProcess(git, repo, {QStringLiteral("checkout"), branch})) {
86 return CheckoutResult{};
87 }
88
89 git.start(QProcess::ReadOnly);
90 CheckoutResult res;
91 res.branch = branch;
92 if (git.waitForStarted() && git.waitForFinished(-1)) {
93 res.returnCode = git.exitCode();
94 res.error = QString::fromUtf8(git.readAllStandardError());
95 }
96 return res;
97 }
98
checkoutNewBranch(const QString & repo,const QString & newBranch,const QString & fromBranch)99 GitUtils::CheckoutResult GitUtils::checkoutNewBranch(const QString &repo, const QString &newBranch, const QString &fromBranch)
100 {
101 QProcess git;
102 QStringList args{QStringLiteral("checkout"), QStringLiteral("-q"), QStringLiteral("-b"), newBranch};
103 if (!fromBranch.isEmpty()) {
104 args.append(fromBranch);
105 }
106
107 if (!setupGitProcess(git, repo, args)) {
108 return CheckoutResult{};
109 }
110
111 git.start(QProcess::ReadOnly);
112 CheckoutResult res;
113 res.branch = newBranch;
114 if (git.waitForStarted() && git.waitForFinished(-1)) {
115 res.returnCode = git.exitCode();
116 res.error = QString::fromUtf8(git.readAllStandardError());
117 }
118 return res;
119 }
120
parseLocalBranch(const QString & raw)121 static GitUtils::Branch parseLocalBranch(const QString &raw)
122 {
123 static const int len = QStringLiteral("refs/heads/").length();
124 return GitUtils::Branch{raw.mid(len), QString(), GitUtils::Head};
125 }
126
parseRemoteBranch(const QString & raw)127 static GitUtils::Branch parseRemoteBranch(const QString &raw)
128 {
129 static const int len = QStringLiteral("refs/remotes/").length();
130 int indexofRemote = raw.indexOf(QLatin1Char('/'), len);
131 return GitUtils::Branch{raw.mid(len), raw.mid(len, indexofRemote - len), GitUtils::Remote};
132 }
133
getAllBranchesAndTags(const QString & repo,RefType ref)134 QVector<GitUtils::Branch> GitUtils::getAllBranchesAndTags(const QString &repo, RefType ref)
135 {
136 // git for-each-ref --format '%(refname)' --sort=-committerdate ...
137 QProcess git;
138
139 QStringList args{QStringLiteral("for-each-ref"), QStringLiteral("--format"), QStringLiteral("%(refname)"), QStringLiteral("--sort=-committerdate")};
140 if (ref & RefType::Head) {
141 args.append(QStringLiteral("refs/heads"));
142 }
143 if (ref & RefType::Remote) {
144 args.append(QStringLiteral("refs/remotes"));
145 }
146 if (ref & RefType::Tag) {
147 args.append(QStringLiteral("refs/tags"));
148 args.append(QStringLiteral("--sort=-taggerdate"));
149 }
150
151 if (!setupGitProcess(git, repo, args)) {
152 return {};
153 }
154
155 git.start(QProcess::ReadOnly);
156 QVector<Branch> branches;
157 if (git.waitForStarted() && git.waitForFinished(-1)) {
158 QString gitout = QString::fromUtf8(git.readAllStandardOutput());
159 QStringList out = gitout.split(QLatin1Char('\n'));
160
161 branches.reserve(out.size());
162 // clang-format off
163 for (const auto &o : out) {
164 if (ref & Head && o.startsWith(QLatin1String("refs/heads"))) {
165 branches.append(parseLocalBranch(o));
166 } else if (ref & Remote && o.startsWith(QLatin1String("refs/remotes"))) {
167 branches.append(parseRemoteBranch(o));
168 } else if (ref & Tag && o.startsWith(QLatin1String("refs/tags/"))) {
169 static const int len = QStringLiteral("refs/tags/").length();
170 branches.append({o.mid(len), {}, RefType::Tag});
171 }
172 }
173 // clang-format on
174 }
175
176 return branches;
177 }
178
getAllBranches(const QString & repo)179 QVector<GitUtils::Branch> GitUtils::getAllBranches(const QString &repo)
180 {
181 return getAllBranchesAndTags(repo, static_cast<RefType>(RefType::Head | RefType::Remote));
182 }
183
getLastCommitMessage(const QString & repo)184 std::pair<QString, QString> GitUtils::getLastCommitMessage(const QString &repo)
185 {
186 // git log -1 --pretty=%B
187 QProcess git;
188 if (!setupGitProcess(git, repo, {QStringLiteral("log"), QStringLiteral("-1"), QStringLiteral("--pretty=%B")})) {
189 return {};
190 }
191
192 git.start(QProcess::ReadOnly);
193 if (git.waitForStarted() && git.waitForFinished(-1)) {
194 if (git.exitCode() != 0 || git.exitStatus() != QProcess::NormalExit) {
195 return {};
196 }
197
198 QList<QByteArray> output = git.readAllStandardOutput().split('\n');
199 if (output.isEmpty()) {
200 return {};
201 }
202
203 QString msg = QString::fromUtf8(output.at(0));
204 QString desc;
205 if (output.size() > 1) {
206 desc = std::accumulate(output.cbegin() + 1, output.cend(), QString::fromUtf8(output.at(1)), [](const QString &line, const QByteArray &ba) {
207 return QString(line + QString::fromUtf8(ba) + QStringLiteral("\n"));
208 });
209 desc = desc.trimmed();
210 }
211 return {msg, desc};
212 }
213 return {};
214 }
215
deleteBranches(const QStringList & branches,const QString & repo)216 GitUtils::Result GitUtils::deleteBranches(const QStringList &branches, const QString &repo)
217 {
218 QStringList args = {QStringLiteral("branch"), QStringLiteral("-D")};
219 args << branches;
220
221 QProcess git;
222 if (!setupGitProcess(git, repo, args)) {
223 return {};
224 }
225
226 git.start(QProcess::ReadOnly);
227 if (git.waitForStarted() && git.waitForFinished(-1)) {
228 QString out = QString::fromLatin1(git.readAllStandardError()) + QString::fromLatin1(git.readAllStandardOutput());
229 return {out, git.exitCode()};
230 }
231 Q_UNREACHABLE();
232 return {QString(), -1};
233 }
234