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