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