1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 Petar Perisin.
4 ** Contact: petar.perisin@gmail.com
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "gerritpushdialog.h"
27 #include "ui_gerritpushdialog.h"
28 #include "branchcombobox.h"
29 
30 #include "../gitclient.h"
31 #include "../gitconstants.h"
32 
33 #include <utils/icon.h>
34 #include <utils/stringutils.h>
35 #include <utils/theme/theme.h>
36 
37 #include <QApplication>
38 #include <QDateTime>
39 #include <QDir>
40 #include <QPushButton>
41 #include <QRegularExpressionValidator>
42 #include <QVersionNumber>
43 
44 using namespace Git::Internal;
45 
46 namespace Gerrit {
47 namespace Internal {
48 
49 static const int ReasonableDistance = 100;
50 
51 class PushItemDelegate : public IconItemDelegate
52 {
53 public:
PushItemDelegate(LogChangeWidget * widget)54     PushItemDelegate(LogChangeWidget *widget)
55         : IconItemDelegate(widget, Utils::Icon(":/git/images/arrowup.png"))
56     {
57     }
58 
59 protected:
hasIcon(int row) const60     bool hasIcon(int row) const override
61     {
62         return row >= currentRow();
63     }
64 };
65 
determineRemoteBranch(const QString & localBranch)66 QString GerritPushDialog::determineRemoteBranch(const QString &localBranch)
67 {
68     const QString earliestCommit = m_ui->commitView->earliestCommit();
69     if (earliestCommit.isEmpty())
70         return {};
71 
72     QString output;
73     QString error;
74 
75     if (!GitClient::instance()->synchronousBranchCmd(
76                 m_workingDir, {"-r", "--contains", earliestCommit + '^'}, &output, &error)) {
77         return QString();
78     }
79     const QString head = "/HEAD";
80     const QStringList refs = output.split('\n');
81 
82     QString remoteTrackingBranch;
83     if (localBranch != "HEAD")
84         remoteTrackingBranch = GitClient::instance()->synchronousTrackingBranch(m_workingDir, localBranch);
85 
86     QString remoteBranch;
87     for (const QString &reference : refs) {
88         const QString ref = reference.trimmed();
89         if (ref.contains(head) || ref.isEmpty())
90             continue;
91 
92         if (remoteBranch.isEmpty())
93             remoteBranch = ref;
94 
95         // Prefer remote tracking branch if it exists and contains the latest remote commit
96         if (ref == remoteTrackingBranch)
97             return ref;
98     }
99     return remoteBranch;
100 }
101 
initRemoteBranches()102 void GerritPushDialog::initRemoteBranches()
103 {
104     QString output;
105     const QString head = "/HEAD";
106 
107     QString remotesPrefix("refs/remotes/");
108     if (!GitClient::instance()->synchronousForEachRefCmd(
109                 m_workingDir, {"--format=%(refname)\t%(committerdate:raw)", remotesPrefix}, &output)) {
110         return;
111     }
112 
113     const QStringList refs = output.split("\n");
114     for (const QString &reference : refs) {
115         QStringList entries = reference.split('\t');
116         if (entries.count() < 2 || entries.first().endsWith(head))
117             continue;
118         const QString ref = entries.at(0).mid(remotesPrefix.size());
119         int refBranchIndex = ref.indexOf('/');
120         qint64 timeT = entries.at(1).left(entries.at(1).indexOf(' ')).toLongLong();
121         BranchDate bd(ref.mid(refBranchIndex + 1), QDateTime::fromSecsSinceEpoch(timeT).date());
122         m_remoteBranches.insertMulti(ref.left(refBranchIndex), bd);
123     }
124     m_ui->remoteComboBox->updateRemotes(false);
125 }
126 
GerritPushDialog(const QString & workingDir,const QString & reviewerList,QSharedPointer<GerritParameters> parameters,QWidget * parent)127 GerritPushDialog::GerritPushDialog(const QString &workingDir, const QString &reviewerList,
128                                    QSharedPointer<GerritParameters> parameters, QWidget *parent) :
129     QDialog(parent),
130     m_workingDir(workingDir),
131     m_ui(new Ui::GerritPushDialog)
132 {
133     m_ui->setupUi(this);
134     m_ui->repositoryLabel->setText(QDir::toNativeSeparators(workingDir));
135     m_ui->remoteComboBox->setRepository(workingDir);
136     m_ui->remoteComboBox->setParameters(parameters);
137     m_ui->remoteComboBox->setAllowDups(true);
138 
139     auto delegate = new PushItemDelegate(m_ui->commitView);
140     delegate->setParent(this);
141 
142     initRemoteBranches();
143 
144     if (m_ui->remoteComboBox->isEmpty()) {
145         m_initErrorMessage = tr("Cannot find a Gerrit remote. Add one and try again.");
146         return;
147     }
148 
149     m_ui->localBranchComboBox->init(workingDir);
150     connect(m_ui->localBranchComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
151             this, &GerritPushDialog::updateCommits);
152 
153     connect(m_ui->targetBranchComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
154             this, &GerritPushDialog::setChangeRange);
155 
156     connect(m_ui->targetBranchComboBox, &QComboBox::currentTextChanged,
157             this, &GerritPushDialog::validate);
158 
159     updateCommits(m_ui->localBranchComboBox->currentIndex());
160     onRemoteChanged(true);
161 
162     QRegularExpressionValidator *noSpaceValidator = new QRegularExpressionValidator(QRegularExpression("^\\S+$"), this);
163     m_ui->reviewersLineEdit->setText(reviewerList);
164     m_ui->reviewersLineEdit->setValidator(noSpaceValidator);
165     m_ui->topicLineEdit->setValidator(noSpaceValidator);
166     m_ui->wipCheckBox->setCheckState(Qt::PartiallyChecked);
167 
168     connect(m_ui->remoteComboBox, &GerritRemoteChooser::remoteChanged,
169             this, [this] { onRemoteChanged(); });
170 }
171 
~GerritPushDialog()172 GerritPushDialog::~GerritPushDialog()
173 {
174     delete m_ui;
175 }
176 
selectedCommit() const177 QString GerritPushDialog::selectedCommit() const
178 {
179     return m_ui->commitView->commit();
180 }
181 
calculateChangeRange(const QString & branch)182 QString GerritPushDialog::calculateChangeRange(const QString &branch)
183 {
184     QString remote = selectedRemoteName();
185     remote += '/';
186     remote += selectedRemoteBranchName();
187 
188     QString number;
189     QString error;
190 
191     GitClient::instance()->synchronousRevListCmd(
192                 m_workingDir, { remote + ".." + branch, "--count" }, &number, &error);
193 
194     number.chop(1);
195     return number;
196 }
197 
setChangeRange()198 void GerritPushDialog::setChangeRange()
199 {
200     if (m_ui->targetBranchComboBox->itemData(m_ui->targetBranchComboBox->currentIndex()) == 1) {
201         setRemoteBranches(true);
202         return;
203     }
204     const QString remoteBranchName = selectedRemoteBranchName();
205     if (remoteBranchName.isEmpty())
206         return;
207     const QString branch = m_ui->localBranchComboBox->currentText();
208     const QString range = calculateChangeRange(branch);
209     if (range.isEmpty()) {
210         m_ui->infoLabel->hide();
211         return;
212     }
213     m_ui->infoLabel->show();
214     const QString remote = selectedRemoteName() + '/' + remoteBranchName;
215     QString labelText = tr("Number of commits between %1 and %2: %3").arg(branch, remote, range);
216     const int currentRange = range.toInt();
217     QPalette palette = QApplication::palette();
218     if (currentRange > ReasonableDistance) {
219         const QColor errorColor = Utils::creatorTheme()->color(Utils::Theme::TextColorError);
220         palette.setColor(QPalette::WindowText, errorColor);
221         palette.setColor(QPalette::ButtonText, errorColor);
222         labelText.append("\n" + tr("Are you sure you selected the right target branch?"));
223     }
224     m_ui->infoLabel->setPalette(palette);
225     m_ui->targetBranchComboBox->setPalette(palette);
226     m_ui->infoLabel->setText(labelText);
227 }
228 
versionSupportsWip(const QString & version)229 static bool versionSupportsWip(const QString &version)
230 {
231     return QVersionNumber::fromString(version) >= QVersionNumber(2, 15);
232 }
233 
onRemoteChanged(bool force)234 void GerritPushDialog::onRemoteChanged(bool force)
235 {
236     setRemoteBranches();
237     const QString version = m_ui->remoteComboBox->currentServer().version;
238     const QString remote = m_ui->remoteComboBox->currentRemoteName();
239 
240     m_ui->commitView->setExcludedRemote(remote);
241     const QString branch = m_ui->localBranchComboBox->itemText(m_ui->localBranchComboBox->currentIndex());
242     m_hasLocalCommits = m_ui->commitView->init(m_workingDir, branch, LogChangeWidget::Silent);
243     validate();
244 
245     const bool supportsWip = versionSupportsWip(version);
246     if (!force && supportsWip == m_currentSupportsWip)
247         return;
248     m_currentSupportsWip = supportsWip;
249     m_ui->wipCheckBox->setEnabled(supportsWip);
250     if (supportsWip) {
251         m_ui->wipCheckBox->setToolTip(tr("Checked - Mark change as WIP.\n"
252                                          "Unchecked - Mark change as ready for review.\n"
253                                          "Partially checked - Do not change current state."));
254         m_ui->draftCheckBox->setTristate(true);
255         if (m_ui->draftCheckBox->checkState() != Qt::Checked)
256             m_ui->draftCheckBox->setCheckState(Qt::PartiallyChecked);
257         m_ui->draftCheckBox->setToolTip(tr("Checked - Mark change as private.\n"
258                                            "Unchecked - Remove mark.\n"
259                                            "Partially checked - Do not change current state."));
260     } else {
261         m_ui->wipCheckBox->setToolTip(tr("Supported on Gerrit 2.15 and later."));
262         m_ui->draftCheckBox->setTristate(false);
263         if (m_ui->draftCheckBox->checkState() != Qt::Checked)
264             m_ui->draftCheckBox->setCheckState(Qt::Unchecked);
265         m_ui->draftCheckBox->setToolTip(tr("Checked - The change is a draft.\n"
266                                            "Unchecked - The change is not a draft."));
267     }
268 }
269 
initErrorMessage() const270 QString GerritPushDialog::initErrorMessage() const
271 {
272     return m_initErrorMessage;
273 }
274 
pushTarget() const275 QString GerritPushDialog::pushTarget() const
276 {
277     QStringList options;
278     QString target = selectedCommit();
279     if (target.isEmpty())
280         target = "HEAD";
281     target += ":refs/";
282     if (versionSupportsWip(m_ui->remoteComboBox->currentServer().version)) {
283         target += "for";
284         const Qt::CheckState draftState = m_ui->draftCheckBox->checkState();
285         const Qt::CheckState wipState = m_ui->wipCheckBox->checkState();
286         if (draftState == Qt::Checked)
287             options << "private";
288         else if (draftState == Qt::Unchecked)
289             options << "remove-private";
290 
291         if (wipState == Qt::Checked)
292             options << "wip";
293         else if (wipState == Qt::Unchecked)
294             options << "ready";
295     } else {
296         target += QLatin1String(m_ui->draftCheckBox->isChecked() ? "drafts" : "for");
297     }
298     target += '/' + selectedRemoteBranchName();
299     const QString topic = selectedTopic();
300     if (!topic.isEmpty())
301         target += '/' + topic;
302 
303     const QStringList reviewersInput = reviewers().split(',', Qt::SkipEmptyParts);
304     for (const QString &reviewer : reviewersInput)
305         options << "r=" + reviewer;
306 
307     if (!options.isEmpty())
308         target += '%' + options.join(',');
309     return target;
310 }
311 
storeTopic()312 void GerritPushDialog::storeTopic()
313 {
314     const QString branch = m_ui->localBranchComboBox->currentText();
315     GitClient::instance()->setConfigValue(
316                 m_workingDir, QString("branch.%1.topic").arg(branch), selectedTopic());
317 }
318 
setRemoteBranches(bool includeOld)319 void GerritPushDialog::setRemoteBranches(bool includeOld)
320 {
321     {
322         QSignalBlocker blocker(m_ui->targetBranchComboBox);
323         m_ui->targetBranchComboBox->clear();
324 
325         const QString remoteName = selectedRemoteName();
326         if (!m_remoteBranches.contains(remoteName)) {
327             const QStringList remoteBranches =
328                     GitClient::instance()->synchronousRepositoryBranches(remoteName, m_workingDir);
329             for (const QString &branch : remoteBranches)
330                 m_remoteBranches.insertMulti(remoteName, qMakePair(branch, QDate()));
331             if (remoteBranches.isEmpty()) {
332                 m_ui->targetBranchComboBox->setEditable(true);
333                 m_ui->targetBranchComboBox->setToolTip(
334                             tr("No remote branches found. This is probably the initial commit."));
335                 if (QLineEdit *lineEdit = m_ui->targetBranchComboBox->lineEdit())
336                     lineEdit->setPlaceholderText(tr("Branch name"));
337             }
338         }
339 
340         int i = 0;
341         bool excluded = false;
342         const QList<BranchDate> remoteBranches = m_remoteBranches.values(remoteName);
343         for (const BranchDate &bd : remoteBranches) {
344             const bool isSuggested = bd.first == m_suggestedRemoteBranch;
345             if (includeOld || isSuggested || !bd.second.isValid()
346                     || bd.second.daysTo(QDate::currentDate()) <= Git::Constants::OBSOLETE_COMMIT_AGE_IN_DAYS) {
347                 m_ui->targetBranchComboBox->addItem(bd.first);
348                 if (isSuggested)
349                     m_ui->targetBranchComboBox->setCurrentIndex(i);
350                 ++i;
351             } else {
352                 excluded = true;
353             }
354         }
355         if (excluded)
356             m_ui->targetBranchComboBox->addItem(tr("... Include older branches ..."), 1);
357         setChangeRange();
358     }
359     validate();
360 }
361 
updateCommits(int index)362 void GerritPushDialog::updateCommits(int index)
363 {
364     const QString branch = m_ui->localBranchComboBox->itemText(index);
365     m_hasLocalCommits = m_ui->commitView->init(m_workingDir, branch, LogChangeWidget::Silent);
366     QString topic = GitClient::instance()->readConfigValue(
367                 m_workingDir, QString("branch.%1.topic").arg(branch));
368     if (!topic.isEmpty())
369         m_ui->topicLineEdit->setText(topic);
370 
371     const QString remoteBranch = determineRemoteBranch(branch);
372     if (!remoteBranch.isEmpty()) {
373         const int slash = remoteBranch.indexOf('/');
374 
375         m_suggestedRemoteBranch = remoteBranch.mid(slash + 1);
376         const QString remote = remoteBranch.left(slash);
377 
378         if (!m_ui->remoteComboBox->setCurrentRemote(remote))
379             onRemoteChanged();
380     }
381     validate();
382 }
383 
validate()384 void GerritPushDialog::validate()
385 {
386     const bool valid = m_hasLocalCommits && !selectedRemoteBranchName().isEmpty();
387     m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
388 }
389 
selectedRemoteName() const390 QString GerritPushDialog::selectedRemoteName() const
391 {
392     return m_ui->remoteComboBox->currentRemoteName();
393 }
394 
selectedRemoteBranchName() const395 QString GerritPushDialog::selectedRemoteBranchName() const
396 {
397     return m_ui->targetBranchComboBox->currentText();
398 }
399 
selectedTopic() const400 QString GerritPushDialog::selectedTopic() const
401 {
402     return m_ui->topicLineEdit->text().trimmed();
403 }
404 
reviewers() const405 QString GerritPushDialog::reviewers() const
406 {
407     return m_ui->reviewersLineEdit->text();
408 }
409 
410 } // namespace Internal
411 } // namespace Gerrit
412