1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
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 "gitclient.h"
27 #include "gitutils.h"
28
29 #include "commitdata.h"
30 #include "gitconstants.h"
31 #include "giteditor.h"
32 #include "gitplugin.h"
33 #include "gitsubmiteditor.h"
34 #include "mergetool.h"
35 #include "branchadddialog.h"
36 #include "gerrit/gerritplugin.h"
37
38 #include <coreplugin/coreconstants.h>
39 #include <coreplugin/editormanager/editormanager.h>
40 #include <coreplugin/icore.h>
41 #include <coreplugin/idocument.h>
42 #include <coreplugin/iversioncontrol.h>
43 #include <coreplugin/vcsmanager.h>
44
45 #include <utils/algorithm.h>
46 #include <utils/checkablemessagebox.h>
47 #include <utils/fileutils.h>
48 #include <utils/hostosinfo.h>
49 #include <utils/mimetypes/mimedatabase.h>
50 #include <utils/qtcassert.h>
51 #include <utils/qtcprocess.h>
52 #include <utils/stringutils.h>
53 #include <utils/temporaryfile.h>
54 #include <utils/theme/theme.h>
55
56 #include <vcsbase/submitfilemodel.h>
57 #include <vcsbase/vcsbasediffeditorcontroller.h>
58 #include <vcsbase/vcsbaseeditor.h>
59 #include <vcsbase/vcsbaseeditorconfig.h>
60 #include <vcsbase/vcsbaseplugin.h>
61 #include <vcsbase/vcscommand.h>
62 #include <vcsbase/vcsoutputwindow.h>
63
64 #include <diffeditor/descriptionwidgetwatcher.h>
65 #include <diffeditor/diffeditorconstants.h>
66 #include <diffeditor/diffeditorcontroller.h>
67 #include <diffeditor/diffutils.h>
68
69 #include <texteditor/fontsettings.h>
70 #include <texteditor/texteditorsettings.h>
71
72 #include <QAction>
73 #include <QCoreApplication>
74 #include <QDir>
75 #include <QFileDialog>
76 #include <QFileInfo>
77 #include <QHash>
78 #include <QMenu>
79 #include <QMessageBox>
80 #include <QPushButton>
81 #include <QRegularExpression>
82 #include <QTextBlock>
83 #include <QToolButton>
84 #include <QTextCodec>
85
86 const char GIT_DIRECTORY[] = ".git";
87 const char HEAD[] = "HEAD";
88 const char CHERRY_PICK_HEAD[] = "CHERRY_PICK_HEAD";
89 const char BRANCHES_PREFIX[] = "Branches: ";
90 const char stashNamePrefix[] = "stash@{";
91 const char noColorOption[] = "--no-color";
92 const char colorOption[] = "--color=always";
93 const char patchOption[] = "--patch";
94 const char graphOption[] = "--graph";
95 const char decorateOption[] = "--decorate";
96 const char showFormatC[] =
97 "--pretty=format:commit %H%d%n"
98 "Author: %an <%ae>, %ad (%ar)%n"
99 "Committer: %cn <%ce>, %cd (%cr)%n"
100 "%n"
101 "%B";
102
103 using namespace Core;
104 using namespace DiffEditor;
105 using namespace Utils;
106 using namespace VcsBase;
107
108 namespace Git {
109 namespace Internal {
110
111 static GitClient *m_instance = nullptr;
112
113 // Suppress git diff warnings about "LF will be replaced by CRLF..." on Windows.
diffExecutionFlags()114 static unsigned diffExecutionFlags()
115 {
116 return HostOsInfo::isWindowsHost() ? unsigned(VcsCommand::SuppressStdErr) : 0u;
117 }
118
119 const unsigned silentFlags = unsigned(VcsCommand::SuppressCommandLogging
120 | VcsCommand::SuppressStdErr
121 | VcsCommand::SuppressFailMessage);
122
branchesDisplay(const QString & prefix,QStringList * branches,bool * first)123 static QString branchesDisplay(const QString &prefix, QStringList *branches, bool *first)
124 {
125 const int limit = 12;
126 const int count = branches->count();
127 int more = 0;
128 QString output;
129 if (*first)
130 *first = false;
131 else
132 output += QString(sizeof(BRANCHES_PREFIX) - 1, ' '); // Align
133 output += prefix + ": ";
134 // If there are more than 'limit' branches, list limit/2 (first limit/4 and last limit/4)
135 if (count > limit) {
136 const int leave = limit / 2;
137 more = count - leave;
138 branches->erase(branches->begin() + leave / 2 + 1, branches->begin() + count - leave / 2);
139 (*branches)[leave / 2] = "...";
140 }
141 output += branches->join(", ");
142 //: Displayed after the untranslated message "Branches: branch1, branch2 'and %n more'"
143 // in git show.
144 if (more > 0)
145 output += ' ' + GitClient::tr("and %n more", nullptr, more);
146 return output;
147 }
148
149 class DescriptionWidgetDecorator : public QObject
150 {
151 Q_OBJECT
152 public:
153 DescriptionWidgetDecorator(DescriptionWidgetWatcher *watcher);
154
155 bool eventFilter(QObject *watched, QEvent *event) override;
156
157 signals:
158 void branchListRequested();
159
160 private:
161 bool checkContentsUnderCursor(const QTextCursor &cursor) const;
162 void highlightCurrentContents(TextEditor::TextEditorWidget *textEditor,
163 const QTextCursor &cursor);
164 void handleCurrentContents(const QTextCursor &cursor);
165 void addWatch(TextEditor::TextEditorWidget *widget);
166 void removeWatch(TextEditor::TextEditorWidget *widget);
167
168 DescriptionWidgetWatcher *m_watcher;
169 QHash<QObject *, TextEditor::TextEditorWidget *> m_viewportToTextEditor;
170 };
171
DescriptionWidgetDecorator(DescriptionWidgetWatcher * watcher)172 DescriptionWidgetDecorator::DescriptionWidgetDecorator(DescriptionWidgetWatcher *watcher)
173 : QObject(),
174 m_watcher(watcher)
175 {
176 QList<TextEditor::TextEditorWidget *> widgets = m_watcher->descriptionWidgets();
177 for (auto *widget : widgets)
178 addWatch(widget);
179
180 connect(m_watcher, &DescriptionWidgetWatcher::descriptionWidgetAdded,
181 this, &DescriptionWidgetDecorator::addWatch);
182 connect(m_watcher, &DescriptionWidgetWatcher::descriptionWidgetRemoved,
183 this, &DescriptionWidgetDecorator::removeWatch);
184 }
185
eventFilter(QObject * watched,QEvent * event)186 bool DescriptionWidgetDecorator::eventFilter(QObject *watched, QEvent *event)
187 {
188 TextEditor::TextEditorWidget *textEditor = m_viewportToTextEditor.value(watched);
189 if (!textEditor)
190 return QObject::eventFilter(watched, event);
191
192 if (event->type() == QEvent::MouseMove) {
193 auto mouseEvent = static_cast<QMouseEvent *>(event);
194 if (mouseEvent->buttons())
195 return QObject::eventFilter(watched, event);
196
197 Qt::CursorShape cursorShape;
198
199 const QTextCursor cursor = textEditor->cursorForPosition(mouseEvent->pos());
200 if (checkContentsUnderCursor(cursor)) {
201 highlightCurrentContents(textEditor, cursor);
202 cursorShape = Qt::PointingHandCursor;
203 } else {
204 textEditor->setExtraSelections(TextEditor::TextEditorWidget::OtherSelection,
205 QList<QTextEdit::ExtraSelection>());
206 cursorShape = Qt::IBeamCursor;
207 }
208
209 bool ret = QObject::eventFilter(watched, event);
210 textEditor->viewport()->setCursor(cursorShape);
211 return ret;
212 } else if (event->type() == QEvent::MouseButtonRelease) {
213 auto mouseEvent = static_cast<QMouseEvent *>(event);
214
215 if (mouseEvent->button() == Qt::LeftButton && !(mouseEvent->modifiers() & Qt::ShiftModifier)) {
216 const QTextCursor cursor = textEditor->cursorForPosition(mouseEvent->pos());
217 if (checkContentsUnderCursor(cursor)) {
218 handleCurrentContents(cursor);
219 return true;
220 }
221 }
222
223 return QObject::eventFilter(watched, event);
224 }
225 return QObject::eventFilter(watched, event);
226 }
227
checkContentsUnderCursor(const QTextCursor & cursor) const228 bool DescriptionWidgetDecorator::checkContentsUnderCursor(const QTextCursor &cursor) const
229 {
230 return cursor.block().text() == Constants::EXPAND_BRANCHES;
231 }
232
highlightCurrentContents(TextEditor::TextEditorWidget * textEditor,const QTextCursor & cursor)233 void DescriptionWidgetDecorator::highlightCurrentContents(
234 TextEditor::TextEditorWidget *textEditor, const QTextCursor &cursor)
235 {
236 QTextEdit::ExtraSelection sel;
237 sel.cursor = cursor;
238 sel.cursor.select(QTextCursor::LineUnderCursor);
239 sel.format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
240 const QColor textColor = TextEditor::TextEditorSettings::fontSettings().formatFor(TextEditor::C_TEXT).foreground();
241 sel.format.setUnderlineColor(textColor.isValid() ? textColor : textEditor->palette().color(QPalette::WindowText));
242 textEditor->setExtraSelections(TextEditor::TextEditorWidget::OtherSelection,
243 QList<QTextEdit::ExtraSelection>() << sel);
244 }
245
handleCurrentContents(const QTextCursor & cursor)246 void DescriptionWidgetDecorator::handleCurrentContents(const QTextCursor &cursor)
247 {
248 QTextCursor copy = cursor;
249
250 copy.select(QTextCursor::LineUnderCursor);
251 copy.removeSelectedText();
252 copy.insertText("Branches: Expanding...");
253 emit branchListRequested();
254 }
255
addWatch(TextEditor::TextEditorWidget * widget)256 void DescriptionWidgetDecorator::addWatch(TextEditor::TextEditorWidget *widget)
257 {
258 m_viewportToTextEditor.insert(widget->viewport(), widget);
259 widget->viewport()->installEventFilter(this);
260 }
261
removeWatch(TextEditor::TextEditorWidget * widget)262 void DescriptionWidgetDecorator::removeWatch(TextEditor::TextEditorWidget *widget)
263 {
264 widget->viewport()->removeEventFilter(this);
265 m_viewportToTextEditor.remove(widget->viewport());
266 }
267
268 ///////////////////////////////
269
270 class GitBaseDiffEditorController : public VcsBaseDiffEditorController
271 {
272 Q_OBJECT
273
274 protected:
275 explicit GitBaseDiffEditorController(IDocument *document,
276 const QString &leftCommit,
277 const QString &rightCommit);
278
279 void runCommand(const QList<QStringList> &args, QTextCodec *codec = nullptr);
280
281 QStringList addConfigurationArguments(const QStringList &args) const;
282 QStringList baseArguments() const;
283
284 public:
285 void initialize();
286
287 private:
288 void updateBranchList();
289
290 DescriptionWidgetWatcher m_watcher;
291 DescriptionWidgetDecorator m_decorator;
292 QString m_leftCommit;
293 QString m_rightCommit;
294 };
295
296 class GitDiffEditorController : public GitBaseDiffEditorController
297 {
298 public:
GitDiffEditorController(IDocument * document,const QString & leftCommit,const QString & rightCommit,const QStringList & extraArgs)299 explicit GitDiffEditorController(IDocument *document,
300 const QString &leftCommit,
301 const QString &rightCommit,
302 const QStringList &extraArgs)
303 : GitBaseDiffEditorController(document, leftCommit, rightCommit)
304 {
305 setReloader([this, extraArgs] {
306 runCommand({addConfigurationArguments(baseArguments() << extraArgs)});
307 });
308 }
309 };
310
GitBaseDiffEditorController(IDocument * document,const QString & leftCommit,const QString & rightCommit)311 GitBaseDiffEditorController::GitBaseDiffEditorController(IDocument *document,
312 const QString &leftCommit,
313 const QString &rightCommit) :
314 VcsBaseDiffEditorController(document),
315 m_watcher(this),
316 m_decorator(&m_watcher),
317 m_leftCommit(leftCommit),
318 m_rightCommit(rightCommit)
319 {
320 connect(&m_decorator, &DescriptionWidgetDecorator::branchListRequested,
321 this, &GitBaseDiffEditorController::updateBranchList);
322 setDisplayName("Git Diff");
323 }
324
initialize()325 void GitBaseDiffEditorController::initialize()
326 {
327 if (m_rightCommit.isEmpty()) {
328 // This is workaround for lack of support for merge commits and resolving conflicts,
329 // we compare the current state of working tree to the HEAD of current branch
330 // instead of showing unsupported combined diff format.
331 GitClient::CommandInProgress commandInProgress = m_instance->checkCommandInProgress(workingDirectory());
332 if (commandInProgress != GitClient::NoCommand)
333 m_rightCommit = HEAD;
334 }
335 }
336
updateBranchList()337 void GitBaseDiffEditorController::updateBranchList()
338 {
339 const QString revision = description().mid(7, 12);
340 if (revision.isEmpty())
341 return;
342
343 const QString workingDirectory = baseDirectory();
344 VcsCommand *command = m_instance->vcsExec(
345 workingDirectory, {"branch", noColorOption, "-a", "--contains", revision}, nullptr,
346 false, 0, workingDirectory);
347 connect(command, &VcsCommand::stdOutText, this, [this](const QString &text) {
348 const QString remotePrefix = "remotes/";
349 const QString localPrefix = "<Local>";
350 const int prefixLength = remotePrefix.length();
351 QString output = BRANCHES_PREFIX;
352 QStringList branches;
353 QString previousRemote = localPrefix;
354 bool first = true;
355 for (const QString &branch : text.split('\n')) {
356 const QString b = branch.mid(2).trimmed();
357 if (b.isEmpty())
358 continue;
359 if (b.startsWith(remotePrefix)) {
360 const int nextSlash = b.indexOf('/', prefixLength);
361 if (nextSlash < 0)
362 continue;
363 const QString remote = b.mid(prefixLength, nextSlash - prefixLength);
364 if (remote != previousRemote) {
365 output += branchesDisplay(previousRemote, &branches, &first) + '\n';
366 branches.clear();
367 previousRemote = remote;
368 }
369 branches << b.mid(nextSlash + 1);
370 } else {
371 branches << b;
372 }
373 }
374 if (branches.isEmpty()) {
375 if (previousRemote == localPrefix)
376 output += tr("<None>");
377 } else {
378 output += branchesDisplay(previousRemote, &branches, &first);
379 }
380 const QString branchList = output.trimmed();
381 QString newDescription = description();
382 newDescription.replace(Constants::EXPAND_BRANCHES, branchList);
383 setDescription(newDescription);
384 });
385 }
386
387 ///////////////////////////////
388
runCommand(const QList<QStringList> & args,QTextCodec * codec)389 void GitBaseDiffEditorController::runCommand(const QList<QStringList> &args, QTextCodec *codec)
390 {
391 VcsBaseDiffEditorController::runCommand(args, diffExecutionFlags(), codec);
392 }
393
addConfigurationArguments(const QStringList & args) const394 QStringList GitBaseDiffEditorController::addConfigurationArguments(const QStringList &args) const
395 {
396 QTC_ASSERT(!args.isEmpty(), return args);
397
398 QStringList realArgs = {
399 "-c",
400 "diff.color=false",
401 args.at(0),
402 "-m", // show diff against parents instead of merge commits
403 "-M", "-C", // Detect renames and copies
404 "--first-parent" // show only first parent
405 };
406 if (ignoreWhitespace())
407 realArgs << "--ignore-space-change";
408 realArgs << "--unified=" + QString::number(contextLineCount())
409 << "--src-prefix=a/" << "--dst-prefix=b/" << args.mid(1);
410
411 return realArgs;
412 }
413
baseArguments() const414 QStringList GitBaseDiffEditorController::baseArguments() const
415 {
416 QStringList res = {"diff"};
417 if (!m_leftCommit.isEmpty())
418 res << m_leftCommit;
419 if (!m_rightCommit.isEmpty())
420 res << m_rightCommit;
421 return res;
422 }
423
424 class FileListDiffController : public GitBaseDiffEditorController
425 {
426 public:
FileListDiffController(IDocument * document,const QStringList & stagedFiles,const QStringList & unstagedFiles)427 FileListDiffController(IDocument *document,
428 const QStringList &stagedFiles, const QStringList &unstagedFiles) :
429 GitBaseDiffEditorController(document, {}, {})
430 {
__anon1f7177530302null431 setReloader([this, stagedFiles, unstagedFiles] {
432 QList<QStringList> argLists;
433 if (!stagedFiles.isEmpty()) {
434 QStringList stagedArgs = QStringList({"diff", "--cached", "--"}) << stagedFiles;
435 argLists << addConfigurationArguments(stagedArgs);
436 }
437
438 if (!unstagedFiles.isEmpty())
439 argLists << addConfigurationArguments(baseArguments() << "--" << unstagedFiles);
440
441 if (!argLists.isEmpty())
442 runCommand(argLists);
443 });
444 }
445 };
446
447 class ShowController : public GitBaseDiffEditorController
448 {
449 Q_OBJECT
450 public:
ShowController(IDocument * document,const QString & id)451 ShowController(IDocument *document, const QString &id) :
452 GitBaseDiffEditorController(document, {}, {}),
453 m_id(id),
454 m_state(Idle)
455 {
456 setDisplayName("Git Show");
__anon1f7177530402null457 setReloader([this] {
458 m_state = GettingDescription;
459 const QStringList args = {"show", "-s", noColorOption, showFormatC, m_id};
460 runCommand({args}, m_instance->encoding(workingDirectory(), "i18n.commitEncoding"));
461 setStartupFile(VcsBase::source(this->document()));
462 });
463 }
464
465 void processCommandOutput(const QString &output) override;
466
467 private:
468 const QString m_id;
469 enum State { Idle, GettingDescription, GettingDiff };
470 State m_state;
471 };
472
processCommandOutput(const QString & output)473 void ShowController::processCommandOutput(const QString &output)
474 {
475 QTC_ASSERT(m_state != Idle, return);
476 if (m_state == GettingDescription) {
477 setDescription(m_instance->extendedShowDescription(workingDirectory(), output));
478 // stage 2
479 m_state = GettingDiff;
480 const QStringList args = {"show", "--format=format:", // omit header, already generated
481 noColorOption, decorateOption, m_id};
482 runCommand(QList<QStringList>() << addConfigurationArguments(args));
483 } else if (m_state == GettingDiff) {
484 m_state = Idle;
485 GitBaseDiffEditorController::processCommandOutput(output);
486 }
487 }
488
489 ///////////////////////////////
490
491 class BaseGitDiffArgumentsWidget : public VcsBaseEditorConfig
492 {
493 Q_OBJECT
494
495 public:
BaseGitDiffArgumentsWidget(GitSettings & settings,QToolBar * toolBar)496 BaseGitDiffArgumentsWidget(GitSettings &settings, QToolBar *toolBar) :
497 VcsBaseEditorConfig(toolBar)
498 {
499 m_patienceButton
500 = addToggleButton("--patience", tr("Patience"),
501 tr("Use the patience algorithm for calculating the differences."));
502 mapSetting(m_patienceButton, &settings.diffPatience);
503 m_ignoreWSButton = addToggleButton("--ignore-space-change", tr("Ignore Whitespace"),
504 tr("Ignore whitespace only changes."));
505 mapSetting(m_ignoreWSButton, &settings.ignoreSpaceChangesInDiff);
506 }
507
508 protected:
509 QAction *m_patienceButton;
510 QAction *m_ignoreWSButton;
511 };
512
513 class GitBlameArgumentsWidget : public VcsBaseEditorConfig
514 {
515 Q_OBJECT
516
517 public:
GitBlameArgumentsWidget(GitSettings & settings,QToolBar * toolBar)518 GitBlameArgumentsWidget(GitSettings &settings, QToolBar *toolBar) :
519 VcsBaseEditorConfig(toolBar)
520 {
521 mapSetting(addToggleButton(QString(), tr("Omit Date"),
522 tr("Hide the date of a change from the output.")),
523 &settings.omitAnnotationDate);
524 mapSetting(addToggleButton("-w", tr("Ignore Whitespace"),
525 tr("Ignore whitespace only changes.")),
526 &settings.ignoreSpaceChangesInBlame);
527
528 const QList<ChoiceItem> logChoices = {
529 ChoiceItem(tr("No Move Detection"), ""),
530 ChoiceItem(tr("Detect Moves Within File"), "-M"),
531 ChoiceItem(tr("Detect Moves Between Files"), "-M -C"),
532 ChoiceItem(tr("Detect Moves and Copies Between Files"), "-M -C -C")
533 };
534 mapSetting(addChoices(tr("Move detection"), {}, logChoices),
535 &settings.blameMoveDetection);
536
537 addReloadButton();
538 }
539 };
540
541 class BaseGitLogArgumentsWidget : public BaseGitDiffArgumentsWidget
542 {
543 Q_OBJECT
544
545 public:
BaseGitLogArgumentsWidget(GitSettings & settings,GitEditorWidget * editor)546 BaseGitLogArgumentsWidget(GitSettings &settings, GitEditorWidget *editor) :
547 BaseGitDiffArgumentsWidget(settings, editor->toolBar())
548 {
549 QToolBar *toolBar = editor->toolBar();
550 QAction *diffButton = addToggleButton(patchOption, tr("Diff"),
551 tr("Show difference."));
552 mapSetting(diffButton, &settings.logDiff);
553 connect(diffButton, &QAction::toggled, m_patienceButton, &QAction::setVisible);
554 connect(diffButton, &QAction::toggled, m_ignoreWSButton, &QAction::setVisible);
555 m_patienceButton->setVisible(diffButton->isChecked());
556 m_ignoreWSButton->setVisible(diffButton->isChecked());
557 auto filterAction = new QAction(tr("Filter"), toolBar);
558 filterAction->setToolTip(tr("Filter commits by message or content."));
559 filterAction->setCheckable(true);
560 connect(filterAction, &QAction::toggled, editor, &GitEditorWidget::toggleFilters);
561 toolBar->addAction(filterAction);
562 }
563 };
564
gitHasRgbColors()565 static bool gitHasRgbColors()
566 {
567 const unsigned gitVersion = GitClient::instance()->gitVersion();
568 return gitVersion >= 0x020300U;
569 }
570
logColorName(TextEditor::TextStyle style)571 static QString logColorName(TextEditor::TextStyle style)
572 {
573 using namespace TextEditor;
574
575 const ColorScheme &scheme = TextEditorSettings::fontSettings().colorScheme();
576 QColor color = scheme.formatFor(style).foreground();
577 if (!color.isValid())
578 color = scheme.formatFor(C_TEXT).foreground();
579 return color.name();
580 };
581
582 class GitLogArgumentsWidget : public BaseGitLogArgumentsWidget
583 {
584 Q_OBJECT
585
586 public:
GitLogArgumentsWidget(GitSettings & settings,bool fileRelated,GitEditorWidget * editor)587 GitLogArgumentsWidget(GitSettings &settings, bool fileRelated, GitEditorWidget *editor) :
588 BaseGitLogArgumentsWidget(settings, editor)
589 {
590 QAction *firstParentButton =
591 addToggleButton({"-m", "--first-parent"},
592 tr("First Parent"),
593 tr("Follow only the first parent on merge commits."));
594 mapSetting(firstParentButton, &settings.firstParent);
595 QAction *graphButton = addToggleButton(graphArguments(), tr("Graph"),
596 tr("Show textual graph log."));
597 mapSetting(graphButton, &settings.graphLog);
598
599 QAction *colorButton = addToggleButton(QStringList{colorOption},
600 tr("Color"), tr("Use colors in log."));
601 mapSetting(colorButton, &settings.colorLog);
602
603 if (fileRelated) {
604 QAction *followButton = addToggleButton(
605 "--follow", tr("Follow"),
606 tr("Show log also for previous names of the file."));
607 mapSetting(followButton, &settings.followRenames);
608 }
609
610 addReloadButton();
611 }
612
graphArguments() const613 QStringList graphArguments() const
614 {
615 const QString authorName = logColorName(TextEditor::C_LOG_AUTHOR_NAME);
616 const QString commitDate = logColorName(TextEditor::C_LOG_COMMIT_DATE);
617 const QString commitHash = logColorName(TextEditor::C_LOG_COMMIT_HASH);
618 const QString commitSubject = logColorName(TextEditor::C_LOG_COMMIT_SUBJECT);
619 const QString decoration = logColorName(TextEditor::C_LOG_DECORATION);
620
621 const QString formatArg = QStringLiteral(
622 "--pretty=format:"
623 "%C(%1)%h%Creset "
624 "%C(%2)%d%Creset "
625 "%C(%3)%an%Creset "
626 "%C(%4)%s%Creset "
627 "%C(%5)%ci%Creset"
628 ).arg(commitHash, decoration, authorName, commitSubject, commitDate);
629
630 QStringList graphArgs = {graphOption, "--oneline", "--topo-order"};
631
632 if (gitHasRgbColors())
633 graphArgs << formatArg;
634 else
635 graphArgs << "--pretty=format:%h %d %an %s %ci";
636
637 return graphArgs;
638 }
639 };
640
641 class GitRefLogArgumentsWidget : public BaseGitLogArgumentsWidget
642 {
643 Q_OBJECT
644
645 public:
GitRefLogArgumentsWidget(GitSettings & settings,GitEditorWidget * editor)646 GitRefLogArgumentsWidget(GitSettings &settings, GitEditorWidget *editor) :
647 BaseGitLogArgumentsWidget(settings, editor)
648 {
649 QAction *showDateButton =
650 addToggleButton("--date=iso",
651 tr("Show Date"),
652 tr("Show date instead of sequence."));
653 mapSetting(showDateButton, &settings.refLogShowDate);
654
655 addReloadButton();
656 }
657 };
658
659 class ConflictHandler final : public QObject
660 {
661 Q_OBJECT
662 public:
attachToCommand(VcsCommand * command,const QString & abortCommand=QString ())663 static void attachToCommand(VcsCommand *command, const QString &abortCommand = QString()) {
664 auto handler = new ConflictHandler(command->defaultWorkingDirectory(), abortCommand);
665 handler->setParent(command); // delete when command goes out of scope
666
667 command->addFlags(VcsCommand::ExpectRepoChanges);
668 connect(command, &VcsCommand::stdOutText, handler, &ConflictHandler::readStdOut);
669 connect(command, &VcsCommand::stdErrText, handler, &ConflictHandler::readStdErr);
670 }
671
handleResponse(const Utils::QtcProcess & proc,const QString & workingDirectory,const QString & abortCommand=QString ())672 static void handleResponse(const Utils::QtcProcess &proc,
673 const QString &workingDirectory,
674 const QString &abortCommand = QString())
675 {
676 ConflictHandler handler(workingDirectory, abortCommand);
677 // No conflicts => do nothing
678 if (proc.result() == QtcProcess::FinishedWithSuccess)
679 return;
680 handler.readStdOut(proc.stdOut());
681 handler.readStdErr(proc.stdErr());
682 }
683
684 private:
ConflictHandler(const QString & workingDirectory,const QString & abortCommand)685 ConflictHandler(const QString &workingDirectory, const QString &abortCommand) :
686 m_workingDirectory(workingDirectory),
687 m_abortCommand(abortCommand)
688 { }
689
~ConflictHandler()690 ~ConflictHandler() final
691 {
692 // If interactive rebase editor window is closed, plugin is terminated
693 // but referenced here when the command ends
694 if (m_commit.isEmpty() && m_files.isEmpty()) {
695 if (m_instance->checkCommandInProgress(m_workingDirectory) == GitClient::NoCommand)
696 m_instance->endStashScope(m_workingDirectory);
697 } else {
698 m_instance->handleMergeConflicts(m_workingDirectory, m_commit, m_files, m_abortCommand);
699 }
700 }
701
readStdOut(const QString & data)702 void readStdOut(const QString &data)
703 {
704 static const QRegularExpression patchFailedRE("Patch failed at ([^\\n]*)");
705 static const QRegularExpression conflictedFilesRE("Merge conflict in ([^\\n]*)");
706 const QRegularExpressionMatch match = patchFailedRE.match(data);
707 if (match.hasMatch())
708 m_commit = match.captured(1);
709 QRegularExpressionMatchIterator it = conflictedFilesRE.globalMatch(data);
710 while (it.hasNext())
711 m_files.append(it.next().captured(1));
712 }
713
readStdErr(const QString & data)714 void readStdErr(const QString &data)
715 {
716 static const QRegularExpression couldNotApplyRE("[Cc]ould not (?:apply|revert) ([^\\n]*)");
717 const QRegularExpressionMatch match = couldNotApplyRE.match(data);
718 if (match.hasMatch())
719 m_commit = match.captured(1);
720 }
721 private:
722 QString m_workingDirectory;
723 QString m_abortCommand;
724 QString m_commit;
725 QStringList m_files;
726 };
727
728 class GitProgressParser : public ProgressParser
729 {
730 public:
attachToCommand(VcsCommand * command)731 static void attachToCommand(VcsCommand *command)
732 {
733 command->setProgressParser(new GitProgressParser);
734 }
735
736 private:
GitProgressParser()737 GitProgressParser() : m_progressExp("\\((\\d+)/(\\d+)\\)") // e.g. Rebasing (7/42)
738 { }
739
parseProgress(const QString & text)740 void parseProgress(const QString &text) override
741 {
742 const QRegularExpressionMatch match = m_progressExp.match(text);
743 if (match.hasMatch())
744 setProgressAndMaximum(match.captured(1).toInt(), match.captured(2).toInt());
745 }
746
747 const QRegularExpression m_progressExp;
748 };
749
msgRepositoryNotFound(const QString & dir)750 static inline QString msgRepositoryNotFound(const QString &dir)
751 {
752 return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
753 }
754
msgParseFilesFailed()755 static inline QString msgParseFilesFailed()
756 {
757 return GitClient::tr("Cannot parse the file output.");
758 }
759
msgCannotLaunch(const QString & binary)760 static inline QString msgCannotLaunch(const QString &binary)
761 {
762 return GitClient::tr("Cannot launch \"%1\".").arg(QDir::toNativeSeparators(binary));
763 }
764
msgCannotRun(const QString & message,QString * errorMessage)765 static inline void msgCannotRun(const QString &message, QString *errorMessage)
766 {
767 if (errorMessage)
768 *errorMessage = message;
769 else
770 VcsOutputWindow::appendError(message);
771 }
772
msgCannotRun(const QStringList & args,const QString & workingDirectory,const QString & error,QString * errorMessage)773 static inline void msgCannotRun(const QStringList &args, const QString &workingDirectory,
774 const QString &error, QString *errorMessage)
775 {
776 const QString message = GitClient::tr("Cannot run \"%1\" in \"%2\": %3")
777 .arg("git " + args.join(' '),
778 QDir::toNativeSeparators(workingDirectory),
779 error);
780
781 msgCannotRun(message, errorMessage);
782 }
783
784 // ---------------- GitClient
785
GitClient(GitSettings * settings)786 GitClient::GitClient(GitSettings *settings)
787 : VcsBase::VcsBaseClientImpl(settings)
788 {
789 m_instance = this;
790 m_gitQtcEditor = QString::fromLatin1("\"%1\" -client -block -pid %2")
791 .arg(QCoreApplication::applicationFilePath())
792 .arg(QCoreApplication::applicationPid());
793 }
794
instance()795 GitClient *GitClient::instance()
796 {
797 return m_instance;
798 }
799
settings()800 GitSettings &GitClient::settings()
801 {
802 return static_cast<GitSettings &>(m_instance->VcsBaseClientImpl::settings());
803 }
804
findRepositoryForDirectory(const QString & directory) const805 QString GitClient::findRepositoryForDirectory(const QString &directory) const
806 {
807 if (directory.isEmpty() || directory.endsWith("/.git") || directory.contains("/.git/"))
808 return QString();
809 // QFileInfo is outside loop, because it is faster this way
810 QFileInfo fileInfo;
811 FilePath parent;
812 for (FilePath dir = FilePath::fromString(directory); !dir.isEmpty(); dir = dir.parentDir()) {
813 const FilePath gitName = dir.pathAppended(GIT_DIRECTORY);
814 if (!gitName.exists())
815 continue; // parent might exist
816 fileInfo.setFile(gitName.toString());
817 if (fileInfo.isFile())
818 return dir.toString();
819 if (gitName.pathAppended("config").exists())
820 return dir.toString();
821 }
822 return QString();
823 }
824
findGitDirForRepository(const QString & repositoryDir) const825 QString GitClient::findGitDirForRepository(const QString &repositoryDir) const
826 {
827 static QHash<QString, QString> repoDirCache;
828 QString &res = repoDirCache[repositoryDir];
829 if (!res.isEmpty())
830 return res;
831
832 synchronousRevParseCmd(repositoryDir, "--git-dir", &res);
833
834 if (!QDir(res).isAbsolute())
835 res.prepend(repositoryDir + '/');
836 return res;
837 }
838
managesFile(const QString & workingDirectory,const QString & fileName) const839 bool GitClient::managesFile(const QString &workingDirectory, const QString &fileName) const
840 {
841 QtcProcess proc;
842 vcsFullySynchronousExec(proc, workingDirectory, {"ls-files", "--error-unmatch", fileName},
843 Core::ShellCommand::NoOutput);
844 return proc.result() == QtcProcess::FinishedWithSuccess;
845 }
846
unmanagedFiles(const QStringList & filePaths) const847 QStringList GitClient::unmanagedFiles(const QStringList &filePaths) const
848 {
849 QMap<QString, QStringList> filesForDir;
850 for (const QString &filePath : filePaths) {
851 const FilePath fp = FilePath::fromString(filePath);
852 filesForDir[fp.parentDir().toString()] << fp.fileName();
853 }
854 QStringList res;
855 for (auto it = filesForDir.begin(), end = filesForDir.end(); it != end; ++it) {
856 QStringList args({"ls-files", "-z"});
857 const QDir wd(it.key());
858 args << transform(it.value(), [&wd](const QString &fp) { return wd.relativeFilePath(fp); });
859 QtcProcess proc;
860 vcsFullySynchronousExec(proc, it.key(), args, Core::ShellCommand::NoOutput);
861 if (proc.result() != QtcProcess::FinishedWithSuccess)
862 return filePaths;
863 const QStringList managedFilePaths
864 = transform(proc.stdOut().split('\0', Qt::SkipEmptyParts),
865 [&wd](const QString &fp) { return wd.absoluteFilePath(fp); });
866 res += filtered(it.value(), [&managedFilePaths, &wd](const QString &fp) {
867 return !managedFilePaths.contains(wd.absoluteFilePath(fp));
868 });
869 }
870 return res;
871 }
872
codecFor(GitClient::CodecType codecType,const QString & source) const873 QTextCodec *GitClient::codecFor(GitClient::CodecType codecType, const QString &source) const
874 {
875 if (codecType == CodecSource) {
876 return QFileInfo(source).isFile() ? VcsBaseEditor::getCodec(source)
877 : encoding(source, "gui.encoding");
878 }
879 if (codecType == CodecLogOutput)
880 return encoding(source, "i18n.logOutputEncoding");
881 return nullptr;
882 }
883
chunkActionsRequested(QMenu * menu,int fileIndex,int chunkIndex,const DiffEditor::ChunkSelection & selection)884 void GitClient::chunkActionsRequested(QMenu *menu, int fileIndex, int chunkIndex,
885 const DiffEditor::ChunkSelection &selection)
886 {
887 QPointer<DiffEditor::DiffEditorController> diffController
888 = qobject_cast<DiffEditorController *>(sender());
889
890 auto stageChunk = [this](QPointer<DiffEditor::DiffEditorController> diffController,
891 int fileIndex, int chunkIndex, DiffEditorController::PatchOptions options,
892 const DiffEditor::ChunkSelection &selection) {
893 if (diffController.isNull())
894 return;
895
896 options |= DiffEditorController::AddPrefix;
897 const QString patch = diffController->makePatch(fileIndex, chunkIndex, selection, options);
898 stage(diffController, patch, options & Revert);
899 };
900
901 menu->addSeparator();
902 QAction *stageChunkAction = menu->addAction(tr("Stage Chunk"));
903 connect(stageChunkAction, &QAction::triggered, this,
904 [stageChunk, diffController, fileIndex, chunkIndex]() {
905 stageChunk(diffController, fileIndex, chunkIndex,
906 DiffEditorController::NoOption, DiffEditor::ChunkSelection());
907 });
908 QAction *stageLinesAction = menu->addAction(tr("Stage Selection (%n Lines)", "", selection.selectedRowsCount()));
909 connect(stageLinesAction, &QAction::triggered, this,
910 [stageChunk, diffController, fileIndex, chunkIndex, selection]() {
911 stageChunk(diffController, fileIndex, chunkIndex,
912 DiffEditorController::NoOption, selection);
913 });
914 QAction *unstageChunkAction = menu->addAction(tr("Unstage Chunk"));
915 connect(unstageChunkAction, &QAction::triggered, this,
916 [stageChunk, diffController, fileIndex, chunkIndex]() {
917 stageChunk(diffController, fileIndex, chunkIndex,
918 DiffEditorController::Revert, DiffEditor::ChunkSelection());
919 });
920 QAction *unstageLinesAction = menu->addAction(tr("Unstage Selection (%n Lines)", "", selection.selectedRowsCount()));
921 connect(unstageLinesAction, &QAction::triggered, this,
922 [stageChunk, diffController, fileIndex, chunkIndex, selection]() {
923 stageChunk(diffController, fileIndex, chunkIndex,
924 DiffEditorController::Revert,
925 selection);
926 });
927 if (selection.isNull()) {
928 stageLinesAction->setVisible(false);
929 unstageLinesAction->setVisible(false);
930 }
931 if (!diffController || !diffController->chunkExists(fileIndex, chunkIndex)) {
932 stageChunkAction->setEnabled(false);
933 stageLinesAction->setEnabled(false);
934 unstageChunkAction->setEnabled(false);
935 unstageLinesAction->setEnabled(false);
936 }
937 }
938
stage(DiffEditor::DiffEditorController * diffController,const QString & patch,bool revert)939 void GitClient::stage(DiffEditor::DiffEditorController *diffController,
940 const QString &patch, bool revert)
941 {
942 Utils::TemporaryFile patchFile("git-patchfile");
943 if (!patchFile.open())
944 return;
945
946 const QString baseDir = diffController->baseDirectory();
947 QTextCodec *codec = EditorManager::defaultTextCodec();
948 const QByteArray patchData = codec
949 ? codec->fromUnicode(patch) : patch.toLocal8Bit();
950 patchFile.write(patchData);
951 patchFile.close();
952
953 QStringList args = {"--cached"};
954 if (revert)
955 args << "--reverse";
956 QString errorMessage;
957 if (synchronousApplyPatch(baseDir, patchFile.fileName(),
958 &errorMessage, args)) {
959 if (errorMessage.isEmpty()) {
960 if (revert)
961 VcsOutputWindow::appendSilently(tr("Chunk successfully unstaged"));
962 else
963 VcsOutputWindow::appendSilently(tr("Chunk successfully staged"));
964 } else {
965 VcsOutputWindow::appendError(errorMessage);
966 }
967 diffController->requestReload();
968 } else {
969 VcsOutputWindow::appendError(errorMessage);
970 }
971 }
972
requestReload(const QString & documentId,const QString & source,const QString & title,const QString & workingDirectory,std::function<GitBaseDiffEditorController * (IDocument *)> factory) const973 void GitClient::requestReload(const QString &documentId, const QString &source,
974 const QString &title, const QString &workingDirectory,
975 std::function<GitBaseDiffEditorController *(IDocument *)> factory) const
976 {
977 // Creating document might change the referenced source. Store a copy and use it.
978 const QString sourceCopy = source;
979
980 IDocument *document = DiffEditorController::findOrCreateDocument(documentId, title);
981 QTC_ASSERT(document, return);
982 GitBaseDiffEditorController *controller = factory(document);
983 QTC_ASSERT(controller, return);
984 controller->setVcsBinary(settings().gitExecutable());
985 controller->setVcsTimeoutS(settings().timeout.value());
986 controller->setProcessEnvironment(processEnvironment());
987 controller->setWorkingDirectory(workingDirectory);
988 controller->initialize();
989
990 connect(controller, &DiffEditorController::chunkActionsRequested,
991 this, &GitClient::chunkActionsRequested, Qt::DirectConnection);
992
993 VcsBase::setSource(document, sourceCopy);
994 EditorManager::activateEditorForDocument(document);
995 controller->requestReload();
996 }
997
diffFiles(const QString & workingDirectory,const QStringList & unstagedFileNames,const QStringList & stagedFileNames) const998 void GitClient::diffFiles(const QString &workingDirectory,
999 const QStringList &unstagedFileNames,
1000 const QStringList &stagedFileNames) const
1001 {
1002 const QString documentId = QLatin1String(Constants::GIT_PLUGIN)
1003 + QLatin1String(".DiffFiles.") + workingDirectory;
1004 requestReload(documentId,
1005 workingDirectory, tr("Git Diff Files"), workingDirectory,
1006 [stagedFileNames, unstagedFileNames](IDocument *doc) {
1007 return new FileListDiffController(doc, stagedFileNames, unstagedFileNames);
1008 });
1009 }
1010
diffProject(const QString & workingDirectory,const QString & projectDirectory) const1011 void GitClient::diffProject(const QString &workingDirectory, const QString &projectDirectory) const
1012 {
1013 const QString documentId = QLatin1String(Constants::GIT_PLUGIN)
1014 + QLatin1String(".DiffProject.") + workingDirectory;
1015 requestReload(documentId,
1016 workingDirectory, tr("Git Diff Project"), workingDirectory,
1017 [projectDirectory](IDocument *doc){
1018 return new GitDiffEditorController(doc, {}, {}, {"--", projectDirectory});
1019 });
1020 }
1021
diffRepository(const QString & workingDirectory,const QString & leftCommit,const QString & rightCommit) const1022 void GitClient::diffRepository(const QString &workingDirectory,
1023 const QString &leftCommit,
1024 const QString &rightCommit) const
1025 {
1026 const QString documentId = QLatin1String(Constants::GIT_PLUGIN)
1027 + QLatin1String(".DiffRepository.") + workingDirectory;
1028 requestReload(documentId, workingDirectory, tr("Git Diff Repository"), workingDirectory,
1029 [&leftCommit, &rightCommit](IDocument *doc) {
1030 return new GitDiffEditorController(doc, leftCommit, rightCommit, {});
1031 });
1032 }
1033
diffFile(const QString & workingDirectory,const QString & fileName) const1034 void GitClient::diffFile(const QString &workingDirectory, const QString &fileName) const
1035 {
1036 const QString title = tr("Git Diff \"%1\"").arg(fileName);
1037 const QString sourceFile = VcsBaseEditor::getSource(workingDirectory, fileName);
1038 const QString documentId = QLatin1String(Constants::GIT_PLUGIN)
1039 + QLatin1String(".DifFile.") + sourceFile;
1040 requestReload(documentId, sourceFile, title, workingDirectory,
1041 [&fileName](IDocument *doc) {
1042 return new GitDiffEditorController(doc, {}, {}, {"--", fileName});
1043 });
1044 }
1045
diffBranch(const QString & workingDirectory,const QString & branchName) const1046 void GitClient::diffBranch(const QString &workingDirectory, const QString &branchName) const
1047 {
1048 const QString title = tr("Git Diff Branch \"%1\"").arg(branchName);
1049 const QString documentId = QLatin1String(Constants::GIT_PLUGIN)
1050 + QLatin1String(".DiffBranch.") + branchName;
1051 requestReload(documentId, workingDirectory, title, workingDirectory,
1052 [branchName](IDocument *doc) {
1053 return new GitDiffEditorController(doc, branchName, {}, {});
1054 });
1055 }
1056
merge(const QString & workingDirectory,const QStringList & unmergedFileNames)1057 void GitClient::merge(const QString &workingDirectory,
1058 const QStringList &unmergedFileNames)
1059 {
1060 auto mergeTool = new MergeTool(this);
1061 if (!mergeTool->start(workingDirectory, unmergedFileNames))
1062 delete mergeTool;
1063 }
1064
status(const QString & workingDirectory) const1065 void GitClient::status(const QString &workingDirectory) const
1066 {
1067 VcsOutputWindow::setRepository(workingDirectory);
1068 VcsCommand *command = vcsExec(workingDirectory, {"status", "-u"}, nullptr, true);
1069 connect(command, &VcsCommand::finished, VcsOutputWindow::instance(), &VcsOutputWindow::clearRepository,
1070 Qt::QueuedConnection);
1071 }
1072
normalLogArguments()1073 static QStringList normalLogArguments()
1074 {
1075 if (!gitHasRgbColors())
1076 return {};
1077
1078 const QString authorName = logColorName(TextEditor::C_LOG_AUTHOR_NAME);
1079 const QString commitDate = logColorName(TextEditor::C_LOG_COMMIT_DATE);
1080 const QString commitHash = logColorName(TextEditor::C_LOG_COMMIT_HASH);
1081 const QString commitSubject = logColorName(TextEditor::C_LOG_COMMIT_SUBJECT);
1082 const QString decoration = logColorName(TextEditor::C_LOG_DECORATION);
1083
1084 const QString logArgs = QStringLiteral(
1085 "--pretty=format:"
1086 "commit %C(%1)%H%Creset %C(%2)%d%Creset%n"
1087 "Author: %C(%3)%an <%ae>%Creset%n"
1088 "Date: %C(%4)%cD %Creset%n%n"
1089 "%C(%5)%w(0,4,4)%s%Creset%n%n%b"
1090 ).arg(commitHash, decoration, authorName, commitDate, commitSubject);
1091
1092 return {logArgs};
1093 }
1094
log(const QString & workingDirectory,const QString & fileName,bool enableAnnotationContextMenu,const QStringList & args)1095 void GitClient::log(const QString &workingDirectory, const QString &fileName,
1096 bool enableAnnotationContextMenu, const QStringList &args)
1097 {
1098 QString msgArg;
1099 if (!fileName.isEmpty())
1100 msgArg = fileName;
1101 else if (!args.isEmpty() && !args.first().startsWith('-'))
1102 msgArg = args.first();
1103 else
1104 msgArg = workingDirectory;
1105 // Creating document might change the referenced workingDirectory. Store a copy and use it.
1106 const QString workingDir = workingDirectory;
1107 const QString title = tr("Git Log \"%1\"").arg(msgArg);
1108 const Id editorId = Git::Constants::GIT_LOG_EDITOR_ID;
1109 const QString sourceFile = VcsBaseEditor::getSource(workingDir, fileName);
1110 GitEditorWidget *editor = static_cast<GitEditorWidget *>(
1111 createVcsEditor(editorId, title, sourceFile,
1112 codecFor(CodecLogOutput), "logTitle", msgArg));
1113 VcsBaseEditorConfig *argWidget = editor->editorConfig();
1114 if (!argWidget) {
1115 argWidget = new GitLogArgumentsWidget(settings(), !fileName.isEmpty(), editor);
1116 argWidget->setBaseArguments(args);
1117 connect(argWidget, &VcsBaseEditorConfig::commandExecutionRequested, this,
1118 [=]() { this->log(workingDir, fileName, enableAnnotationContextMenu, args); });
1119 editor->setEditorConfig(argWidget);
1120 }
1121 editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
1122 editor->setWorkingDirectory(workingDir);
1123
1124 QStringList arguments = {"log", decorateOption};
1125 int logCount = settings().logCount.value();
1126 if (logCount > 0)
1127 arguments << "-n" << QString::number(logCount);
1128
1129 arguments << argWidget->arguments();
1130 if (arguments.contains(patchOption)) {
1131 arguments.removeAll(colorOption);
1132 editor->setHighlightingEnabled(true);
1133 } else if (gitHasRgbColors()) {
1134 editor->setHighlightingEnabled(false);
1135 }
1136 if (!arguments.contains(graphOption) && !arguments.contains(patchOption))
1137 arguments << normalLogArguments();
1138
1139 const QString grepValue = editor->grepValue();
1140 if (!grepValue.isEmpty())
1141 arguments << "--grep=" + grepValue;
1142
1143 const QString pickaxeValue = editor->pickaxeValue();
1144 if (!pickaxeValue.isEmpty())
1145 arguments << "-S" << pickaxeValue;
1146
1147 if ((!grepValue.isEmpty() || !pickaxeValue.isEmpty()) && !editor->caseSensitive())
1148 arguments << "-i";
1149
1150 if (!fileName.isEmpty())
1151 arguments << "--" << fileName;
1152
1153 vcsExec(workingDir, arguments, editor);
1154 }
1155
reflog(const QString & workingDirectory,const QString & ref)1156 void GitClient::reflog(const QString &workingDirectory, const QString &ref)
1157 {
1158 const QString title = tr("Git Reflog \"%1\"").arg(workingDirectory);
1159 const Id editorId = Git::Constants::GIT_REFLOG_EDITOR_ID;
1160 // Creating document might change the referenced workingDirectory. Store a copy and use it.
1161 const QString workingDir = workingDirectory;
1162 GitEditorWidget *editor = static_cast<GitEditorWidget *>(
1163 createVcsEditor(editorId, title, workingDir, codecFor(CodecLogOutput),
1164 "reflogRepository", workingDir));
1165 VcsBaseEditorConfig *argWidget = editor->editorConfig();
1166 if (!argWidget) {
1167 argWidget = new GitRefLogArgumentsWidget(settings(), editor);
1168 if (!ref.isEmpty())
1169 argWidget->setBaseArguments({ref});
1170 connect(argWidget, &VcsBaseEditorConfig::commandExecutionRequested, this,
1171 [=] { this->reflog(workingDir, ref); });
1172 editor->setEditorConfig(argWidget);
1173 }
1174 editor->setWorkingDirectory(workingDir);
1175
1176 QStringList arguments = {"reflog", noColorOption, decorateOption};
1177 arguments << argWidget->arguments();
1178 int logCount = settings().logCount.value();
1179 if (logCount > 0)
1180 arguments << "-n" << QString::number(logCount);
1181
1182 vcsExec(workingDir, arguments, editor);
1183 }
1184
1185 // Do not show "0000" or "^32ae4"
canShow(const QString & sha)1186 static inline bool canShow(const QString &sha)
1187 {
1188 return !sha.startsWith('^') && sha.count('0') != sha.size();
1189 }
1190
msgCannotShow(const QString & sha)1191 static inline QString msgCannotShow(const QString &sha)
1192 {
1193 return GitClient::tr("Cannot describe \"%1\".").arg(sha);
1194 }
1195
show(const QString & source,const QString & id,const QString & name)1196 void GitClient::show(const QString &source, const QString &id, const QString &name)
1197 {
1198 if (!canShow(id)) {
1199 VcsOutputWindow::appendError(msgCannotShow(id));
1200 return;
1201 }
1202
1203 const QString title = tr("Git Show \"%1\"").arg(name.isEmpty() ? id : name);
1204 const QFileInfo sourceFi(source);
1205 QString workingDirectory = sourceFi.isDir()
1206 ? sourceFi.absoluteFilePath() : sourceFi.absolutePath();
1207 const QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
1208 if (!repoDirectory.isEmpty())
1209 workingDirectory = repoDirectory;
1210 const QString documentId = QLatin1String(Constants::GIT_PLUGIN)
1211 + QLatin1String(".Show.") + id;
1212 requestReload(documentId, source, title, workingDirectory,
1213 [id](IDocument *doc) { return new ShowController(doc, id); });
1214 }
1215
archive(const QString & workingDirectory,QString commit)1216 void GitClient::archive(const QString &workingDirectory, QString commit)
1217 {
1218 QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
1219 if (repoDirectory.isEmpty())
1220 repoDirectory = workingDirectory;
1221 QString repoName = QFileInfo(repoDirectory).fileName();
1222
1223 QHash<QString, QString> filters;
1224 QString selectedFilter;
1225 auto appendFilter = [&filters, &selectedFilter](const QString &name, bool isSelected){
1226 const auto mimeType = Utils::mimeTypeForName(name);
1227 const auto filterString = mimeType.filterString();
1228 filters.insert(filterString, "." + mimeType.preferredSuffix());
1229 if (isSelected)
1230 selectedFilter = filterString;
1231 };
1232
1233 bool windows = HostOsInfo::isWindowsHost();
1234 appendFilter("application/zip", windows);
1235 appendFilter("application/x-compressed-tar", !windows);
1236
1237 QString output;
1238 if (synchronousRevParseCmd(repoDirectory, commit, &output))
1239 commit = output.trimmed();
1240
1241 QString archiveName = QFileDialog::getSaveFileName(
1242 ICore::dialogParent(),
1243 tr("Generate %1 archive").arg(repoName),
1244 repoDirectory + QString("/../%1-%2").arg(repoName).arg(commit.left(8)),
1245 filters.keys().join(";;"),
1246 &selectedFilter);
1247 if (archiveName.isEmpty())
1248 return;
1249 QString extension = filters.value(selectedFilter);
1250 QFileInfo archive(archiveName);
1251 if (extension != "." + archive.completeSuffix()) {
1252 archive = QFileInfo(archive.filePath() + extension);
1253 }
1254
1255 if (archive.exists()) {
1256 if (QMessageBox::warning(ICore::dialogParent(), tr("Overwrite?"),
1257 tr("An item named \"%1\" already exists at this location. "
1258 "Do you want to overwrite it?").arg(QDir::toNativeSeparators(archive.absoluteFilePath())),
1259 QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
1260 return;
1261 }
1262 }
1263
1264 vcsExec(workingDirectory, {"archive", commit, "-o", archive.absoluteFilePath()}, nullptr, true);
1265 }
1266
annotate(const QString & workingDir,const QString & file,const QString & revision,int lineNumber,const QStringList & extraOptions)1267 VcsBaseEditorWidget *GitClient::annotate(
1268 const QString &workingDir, const QString &file, const QString &revision,
1269 int lineNumber, const QStringList &extraOptions)
1270 {
1271 const Id editorId = Git::Constants::GIT_BLAME_EDITOR_ID;
1272 const QString id = VcsBaseEditor::getTitleId(workingDir, {file}, revision);
1273 const QString title = tr("Git Blame \"%1\"").arg(id);
1274 const QString sourceFile = VcsBaseEditor::getSource(workingDir, file);
1275
1276 VcsBaseEditorWidget *editor
1277 = createVcsEditor(editorId, title, sourceFile, codecFor(CodecSource, sourceFile),
1278 "blameFileName", id);
1279 VcsBaseEditorConfig *argWidget = editor->editorConfig();
1280 if (!argWidget) {
1281 argWidget = new GitBlameArgumentsWidget(settings(), editor->toolBar());
1282 argWidget->setBaseArguments(extraOptions);
1283 connect(argWidget, &VcsBaseEditorConfig::commandExecutionRequested, this,
1284 [=] {
1285 const int line = VcsBaseEditor::lineNumberOfCurrentEditor();
1286 annotate(workingDir, file, revision, line, extraOptions);
1287 } );
1288 editor->setEditorConfig(argWidget);
1289 }
1290
1291 editor->setWorkingDirectory(workingDir);
1292 QStringList arguments = {"blame", "--root"};
1293 arguments << argWidget->arguments() << "--" << file;
1294 if (!revision.isEmpty())
1295 arguments << revision;
1296 vcsExec(workingDir, arguments, editor, false, 0, lineNumber);
1297 return editor;
1298 }
1299
checkout(const QString & workingDirectory,const QString & ref,StashMode stashMode)1300 VcsCommand *GitClient::checkout(const QString &workingDirectory, const QString &ref,
1301 StashMode stashMode)
1302 {
1303 if (stashMode == StashMode::TryStash && !beginStashScope(workingDirectory, "Checkout"))
1304 return nullptr;
1305 QStringList arguments = setupCheckoutArguments(workingDirectory, ref);
1306 VcsCommand *command = vcsExec(
1307 workingDirectory, arguments, nullptr, true,
1308 VcsCommand::ExpectRepoChanges | VcsCommand::ShowSuccessMessage);
1309 connect(command, &VcsCommand::finished,
1310 this, [this, workingDirectory, stashMode](bool success) {
1311 if (stashMode == StashMode::TryStash)
1312 endStashScope(workingDirectory);
1313 if (success)
1314 updateSubmodulesIfNeeded(workingDirectory, true);
1315 });
1316 return command;
1317 }
1318
1319 /* method used to setup arguments for checkout, in case user wants to create local branch */
setupCheckoutArguments(const QString & workingDirectory,const QString & ref)1320 QStringList GitClient::setupCheckoutArguments(const QString &workingDirectory,
1321 const QString &ref)
1322 {
1323 QStringList arguments = {"checkout", ref};
1324
1325 QStringList localBranches = synchronousRepositoryBranches(workingDirectory);
1326 if (localBranches.contains(ref))
1327 return arguments;
1328
1329 if (Utils::CheckableMessageBox::doNotAskAgainQuestion(
1330 ICore::dialogParent() /*parent*/,
1331 tr("Create Local Branch") /*title*/,
1332 tr("Would you like to create a local branch?") /*message*/,
1333 ICore::settings(), "Git.CreateLocalBranchOnCheckout" /*setting*/,
1334 QDialogButtonBox::Yes | QDialogButtonBox::No /*buttons*/,
1335 QDialogButtonBox::No /*default button*/,
1336 QDialogButtonBox::No /*button to save*/) != QDialogButtonBox::Yes) {
1337 return arguments;
1338 }
1339
1340 if (synchronousCurrentLocalBranch(workingDirectory).isEmpty())
1341 localBranches.removeFirst();
1342
1343 QString refSha;
1344 if (!synchronousRevParseCmd(workingDirectory, ref, &refSha))
1345 return arguments;
1346
1347 QString output;
1348 const QStringList forEachRefArgs = {"refs/remotes/", "--format=%(objectname) %(refname:short)"};
1349 if (!synchronousForEachRefCmd(workingDirectory, forEachRefArgs, &output))
1350 return arguments;
1351
1352 QString remoteBranch;
1353 const QString head("/HEAD");
1354
1355 const QStringList refs = output.split('\n');
1356 for (const QString &singleRef : refs) {
1357 if (singleRef.startsWith(refSha)) {
1358 // branch name might be origin/foo/HEAD
1359 if (!singleRef.endsWith(head) || singleRef.count('/') > 1) {
1360 remoteBranch = singleRef.mid(refSha.length() + 1);
1361 if (remoteBranch == ref)
1362 break;
1363 }
1364 }
1365 }
1366
1367 QString target = remoteBranch;
1368 BranchTargetType targetType = BranchTargetType::Remote;
1369 if (remoteBranch.isEmpty()) {
1370 target = ref;
1371 targetType = BranchTargetType::Commit;
1372 }
1373 const QString suggestedName = suggestedLocalBranchName(
1374 workingDirectory, localBranches, target, targetType);
1375 BranchAddDialog branchAddDialog(localBranches, BranchAddDialog::Type::AddBranch, ICore::dialogParent());
1376 branchAddDialog.setBranchName(suggestedName);
1377 branchAddDialog.setTrackedBranchName(remoteBranch, true);
1378
1379 if (branchAddDialog.exec() != QDialog::Accepted)
1380 return arguments;
1381
1382 arguments.removeLast();
1383 arguments << "-b" << branchAddDialog.branchName();
1384 if (branchAddDialog.track())
1385 arguments << "--track" << remoteBranch;
1386 else
1387 arguments << "--no-track" << ref;
1388
1389 return arguments;
1390 }
1391
reset(const QString & workingDirectory,const QString & argument,const QString & commit)1392 void GitClient::reset(const QString &workingDirectory, const QString &argument, const QString &commit)
1393 {
1394 QStringList arguments = {"reset", argument};
1395 if (!commit.isEmpty())
1396 arguments << commit;
1397
1398 unsigned flags = VcsCommand::ShowSuccessMessage;
1399 if (argument == "--hard") {
1400 if (gitStatus(workingDirectory, StatusMode(NoUntracked | NoSubmodules)) != StatusUnchanged) {
1401 if (QMessageBox::question(
1402 Core::ICore::dialogParent(), tr("Reset"),
1403 tr("All changes in working directory will be discarded. Are you sure?"),
1404 QMessageBox::Yes | QMessageBox::No,
1405 QMessageBox::No) == QMessageBox::No) {
1406 return;
1407 }
1408 }
1409 flags |= VcsCommand::ExpectRepoChanges;
1410 }
1411 vcsExec(workingDirectory, arguments, nullptr, true, flags);
1412 }
1413
removeStaleRemoteBranches(const QString & workingDirectory,const QString & remote)1414 void GitClient::removeStaleRemoteBranches(const QString &workingDirectory, const QString &remote)
1415 {
1416 const QStringList arguments = {"remote", "prune", remote};
1417
1418 VcsCommand *command = vcsExec(workingDirectory, arguments, nullptr, true,
1419 VcsCommand::ShowSuccessMessage);
1420
1421 connect(command, &VcsCommand::success,
1422 this, [workingDirectory]() { GitPlugin::updateBranches(workingDirectory); });
1423 }
1424
recoverDeletedFiles(const QString & workingDirectory)1425 void GitClient::recoverDeletedFiles(const QString &workingDirectory)
1426 {
1427 QtcProcess proc;
1428 vcsFullySynchronousExec(proc, workingDirectory, {"ls-files", "--deleted"},
1429 VcsCommand::SuppressCommandLogging);
1430 if (proc.result() == QtcProcess::FinishedWithSuccess) {
1431 const QString stdOut = proc.stdOut().trimmed();
1432 if (stdOut.isEmpty()) {
1433 VcsOutputWindow::appendError(tr("Nothing to recover"));
1434 return;
1435 }
1436 const QStringList files = stdOut.split('\n');
1437 synchronousCheckoutFiles(workingDirectory, files, QString(), nullptr, false);
1438 VcsOutputWindow::append(tr("Files recovered"), VcsOutputWindow::Message);
1439 }
1440 }
1441
addFile(const QString & workingDirectory,const QString & fileName)1442 void GitClient::addFile(const QString &workingDirectory, const QString &fileName)
1443 {
1444 vcsExec(workingDirectory, {"add", fileName});
1445 }
1446
synchronousLog(const QString & workingDirectory,const QStringList & arguments,QString * output,QString * errorMessageIn,unsigned flags)1447 bool GitClient::synchronousLog(const QString &workingDirectory, const QStringList &arguments,
1448 QString *output, QString *errorMessageIn, unsigned flags)
1449 {
1450 QStringList allArguments = {"log", noColorOption};
1451
1452 allArguments.append(arguments);
1453
1454 QtcProcess proc;
1455 vcsFullySynchronousExec(proc, workingDirectory, allArguments, flags, vcsTimeoutS(),
1456 encoding(workingDirectory, "i18n.logOutputEncoding"));
1457 if (proc.result() == QtcProcess::FinishedWithSuccess) {
1458 *output = proc.stdOut();
1459 return true;
1460 } else {
1461 msgCannotRun(tr("Cannot obtain log of \"%1\": %2")
1462 .arg(QDir::toNativeSeparators(workingDirectory), proc.stdErr()), errorMessageIn);
1463 return false;
1464 }
1465 }
1466
synchronousAdd(const QString & workingDirectory,const QStringList & files,const QStringList & extraOptions)1467 bool GitClient::synchronousAdd(const QString &workingDirectory,
1468 const QStringList &files,
1469 const QStringList &extraOptions)
1470 {
1471 QStringList args{"add"};
1472 args += extraOptions + files;
1473 QtcProcess proc;
1474 vcsFullySynchronousExec(proc, workingDirectory, args);
1475 return proc.result() == QtcProcess::FinishedWithSuccess;
1476 }
1477
synchronousDelete(const QString & workingDirectory,bool force,const QStringList & files)1478 bool GitClient::synchronousDelete(const QString &workingDirectory,
1479 bool force,
1480 const QStringList &files)
1481 {
1482 QStringList arguments = {"rm"};
1483 if (force)
1484 arguments << "--force";
1485 arguments.append(files);
1486 QtcProcess proc;
1487 vcsFullySynchronousExec(proc, workingDirectory, arguments);
1488 return proc.result() == QtcProcess::FinishedWithSuccess;
1489 }
1490
synchronousMove(const QString & workingDirectory,const QString & from,const QString & to)1491 bool GitClient::synchronousMove(const QString &workingDirectory,
1492 const QString &from,
1493 const QString &to)
1494 {
1495 QtcProcess proc;
1496 vcsFullySynchronousExec(proc, workingDirectory, {"mv", from, to});
1497 return proc.result() == QtcProcess::FinishedWithSuccess;
1498 }
1499
synchronousReset(const QString & workingDirectory,const QStringList & files,QString * errorMessage)1500 bool GitClient::synchronousReset(const QString &workingDirectory,
1501 const QStringList &files,
1502 QString *errorMessage)
1503 {
1504 QStringList arguments = {"reset"};
1505 if (files.isEmpty())
1506 arguments << "--hard";
1507 else
1508 arguments << HEAD << "--" << files;
1509
1510 QtcProcess proc;
1511 vcsFullySynchronousExec(proc, workingDirectory, arguments);
1512 const QString stdOut = proc.stdOut();
1513 VcsOutputWindow::append(stdOut);
1514 // Note that git exits with 1 even if the operation is successful
1515 // Assume real failure if the output does not contain "foo.cpp modified"
1516 // or "Unstaged changes after reset" (git 1.7.0).
1517 if (proc.result() != QtcProcess::FinishedWithSuccess
1518 && (!stdOut.contains("modified") && !stdOut.contains("Unstaged changes after reset"))) {
1519 if (files.isEmpty()) {
1520 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
1521 } else {
1522 msgCannotRun(tr("Cannot reset %n files in \"%1\": %2", nullptr, files.size())
1523 .arg(QDir::toNativeSeparators(workingDirectory), proc.stdErr()),
1524 errorMessage);
1525 }
1526 return false;
1527 }
1528 return true;
1529 }
1530
1531 // Initialize repository
synchronousInit(const QString & workingDirectory)1532 bool GitClient::synchronousInit(const QString &workingDirectory)
1533 {
1534 QtcProcess proc;
1535 vcsFullySynchronousExec(proc, workingDirectory, QStringList{"init"});
1536 // '[Re]Initialized...'
1537 VcsOutputWindow::append(proc.stdOut());
1538 if (proc.result() == QtcProcess::FinishedWithSuccess) {
1539 resetCachedVcsInfo(workingDirectory);
1540 return true;
1541 } else {
1542 return false;
1543 }
1544 }
1545
1546 /* Checkout, supports:
1547 * git checkout -- <files>
1548 * git checkout revision -- <files>
1549 * git checkout revision -- . */
synchronousCheckoutFiles(const QString & workingDirectory,QStringList files,QString revision,QString * errorMessage,bool revertStaging)1550 bool GitClient::synchronousCheckoutFiles(const QString &workingDirectory, QStringList files,
1551 QString revision, QString *errorMessage,
1552 bool revertStaging)
1553 {
1554 if (revertStaging && revision.isEmpty())
1555 revision = HEAD;
1556 if (files.isEmpty())
1557 files = QStringList(".");
1558 QStringList arguments = {"checkout"};
1559 if (revertStaging)
1560 arguments << revision;
1561 arguments << "--" << files;
1562 QtcProcess proc;
1563 vcsFullySynchronousExec(proc, workingDirectory, arguments, VcsCommand::ExpectRepoChanges);
1564 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1565 const QString fileArg = files.join(", ");
1566 //: Meaning of the arguments: %1: revision, %2: files, %3: repository,
1567 //: %4: Error message
1568 msgCannotRun(tr("Cannot checkout \"%1\" of %2 in \"%3\": %4")
1569 .arg(revision, fileArg, workingDirectory, proc.stdErr()),
1570 errorMessage);
1571 return false;
1572 }
1573 return true;
1574 }
1575
msgParentRevisionFailed(const QString & workingDirectory,const QString & revision,const QString & why)1576 static inline QString msgParentRevisionFailed(const QString &workingDirectory,
1577 const QString &revision,
1578 const QString &why)
1579 {
1580 //: Failed to find parent revisions of a SHA1 for "annotate previous"
1581 return GitClient::tr("Cannot find parent revisions of \"%1\" in \"%2\": %3").arg(revision, workingDirectory, why);
1582 }
1583
msgInvalidRevision()1584 static inline QString msgInvalidRevision()
1585 {
1586 return GitClient::tr("Invalid revision");
1587 }
1588
1589 // Split a line of "<commit> <parent1> ..." to obtain parents from "rev-list" or "log".
splitCommitParents(const QString & line,QString * commit=nullptr,QStringList * parents=nullptr)1590 static inline bool splitCommitParents(const QString &line,
1591 QString *commit = nullptr,
1592 QStringList *parents = nullptr)
1593 {
1594 if (commit)
1595 commit->clear();
1596 if (parents)
1597 parents->clear();
1598 QStringList tokens = line.trimmed().split(' ');
1599 if (tokens.size() < 2)
1600 return false;
1601 if (commit)
1602 *commit = tokens.front();
1603 tokens.pop_front();
1604 if (parents)
1605 *parents = tokens;
1606 return true;
1607 }
1608
synchronousRevListCmd(const QString & workingDirectory,const QStringList & extraArguments,QString * output,QString * errorMessage) const1609 bool GitClient::synchronousRevListCmd(const QString &workingDirectory, const QStringList &extraArguments,
1610 QString *output, QString *errorMessage) const
1611 {
1612 const QStringList arguments = QStringList({"rev-list", noColorOption}) + extraArguments;
1613 QtcProcess proc;
1614 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags);
1615 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1616 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
1617 return false;
1618 }
1619 *output = proc.stdOut();
1620 return true;
1621 }
1622
1623 // Find out the immediate parent revisions of a revision of the repository.
1624 // Might be several in case of merges.
synchronousParentRevisions(const QString & workingDirectory,const QString & revision,QStringList * parents,QString * errorMessage) const1625 bool GitClient::synchronousParentRevisions(const QString &workingDirectory,
1626 const QString &revision,
1627 QStringList *parents,
1628 QString *errorMessage) const
1629 {
1630 if (parents && !isValidRevision(revision)) { // Not Committed Yet
1631 *parents = QStringList(HEAD);
1632 return true;
1633 }
1634 QString outputText;
1635 QString errorText;
1636 if (!synchronousRevListCmd(workingDirectory, {"--parents", "--max-count=1", revision},
1637 &outputText, &errorText)) {
1638 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, errorText);
1639 return false;
1640 }
1641 // Should result in one line of blank-delimited revisions, specifying current first
1642 // unless it is top.
1643 outputText.remove('\n');
1644 if (!splitCommitParents(outputText, nullptr, parents)) {
1645 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, msgInvalidRevision());
1646 return false;
1647 }
1648 return true;
1649 }
1650
synchronousShortDescription(const QString & workingDirectory,const QString & revision) const1651 QString GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision) const
1652 {
1653 // HACK: The hopefully rare "_-_" will be replaced by quotes in the output,
1654 // leaving it in breaks command line quoting on Windows, see QTCREATORBUG-23208.
1655 const QString quoteReplacement = "_-_";
1656
1657 // Short SHA1, author, subject
1658 const QString defaultShortLogFormat = "%h (%an " + quoteReplacement + "%s";
1659 const int maxShortLogLength = 120;
1660
1661 // Short SHA 1, author, subject
1662 QString output = synchronousShortDescription(workingDirectory, revision, defaultShortLogFormat);
1663 output.replace(quoteReplacement, "\"");
1664 if (output != revision) {
1665 if (output.length() > maxShortLogLength) {
1666 output.truncate(maxShortLogLength);
1667 output.append("...");
1668 }
1669 output.append("\")");
1670 }
1671 return output;
1672 }
1673
synchronousCurrentLocalBranch(const QString & workingDirectory) const1674 QString GitClient::synchronousCurrentLocalBranch(const QString &workingDirectory) const
1675 {
1676 QString branch;
1677 QtcProcess proc;
1678 vcsFullySynchronousExec(proc, workingDirectory, {"symbolic-ref", HEAD}, silentFlags);
1679 if (proc.result() == QtcProcess::FinishedWithSuccess) {
1680 branch = proc.stdOut().trimmed();
1681 } else {
1682 const QString gitDir = findGitDirForRepository(workingDirectory);
1683 const QString rebaseHead = gitDir + "/rebase-merge/head-name";
1684 QFile head(rebaseHead);
1685 if (head.open(QFile::ReadOnly))
1686 branch = QString::fromUtf8(head.readLine()).trimmed();
1687 }
1688 if (!branch.isEmpty()) {
1689 const QString refsHeadsPrefix = "refs/heads/";
1690 if (branch.startsWith(refsHeadsPrefix)) {
1691 branch.remove(0, refsHeadsPrefix.count());
1692 return branch;
1693 }
1694 }
1695 return QString();
1696 }
1697
synchronousHeadRefs(const QString & workingDirectory,QStringList * output,QString * errorMessage) const1698 bool GitClient::synchronousHeadRefs(const QString &workingDirectory, QStringList *output,
1699 QString *errorMessage) const
1700 {
1701 const QStringList arguments = {"show-ref", "--head", "--abbrev=10", "--dereference"};
1702 QtcProcess proc;
1703 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags);
1704 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1705 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
1706 return false;
1707 }
1708
1709 const QString stdOut = proc.stdOut();
1710 const QString headSha = stdOut.left(10);
1711 QString rest = stdOut.mid(15);
1712
1713 const QStringList headShaLines = Utils::filtered(
1714 rest.split('\n'), [&headSha](const QString &s) { return s.startsWith(headSha); });
1715 *output = Utils::transform(headShaLines, [](const QString &s) { return s.mid(11); }); // sha + space
1716
1717 return true;
1718 }
1719
1720 // Retrieve topic (branch, tag or HEAD hash)
synchronousTopic(const QString & workingDirectory) const1721 QString GitClient::synchronousTopic(const QString &workingDirectory) const
1722 {
1723 // First try to find branch
1724 QString branch = synchronousCurrentLocalBranch(workingDirectory);
1725 if (!branch.isEmpty())
1726 return branch;
1727
1728 // Detached HEAD, try a tag or remote branch
1729 QStringList references;
1730 if (!synchronousHeadRefs(workingDirectory, &references))
1731 return QString();
1732
1733 const QString tagStart("refs/tags/");
1734 const QString remoteStart("refs/remotes/");
1735 const QString dereference("^{}");
1736 QString remoteBranch;
1737
1738 for (const QString &ref : qAsConst(references)) {
1739 int derefInd = ref.indexOf(dereference);
1740 if (ref.startsWith(tagStart))
1741 return ref.mid(tagStart.size(), (derefInd == -1) ? -1 : derefInd - tagStart.size());
1742 if (ref.startsWith(remoteStart)) {
1743 remoteBranch = ref.mid(remoteStart.size(),
1744 (derefInd == -1) ? -1 : derefInd - remoteStart.size());
1745 }
1746 }
1747 if (!remoteBranch.isEmpty())
1748 return remoteBranch;
1749
1750 // No tag or remote branch - try git describe
1751 QtcProcess proc;
1752 vcsFullySynchronousExec(proc, workingDirectory, QStringList{"describe"}, VcsCommand::NoOutput);
1753 if (proc.result() == QtcProcess::FinishedWithSuccess) {
1754 const QString stdOut = proc.stdOut().trimmed();
1755 if (!stdOut.isEmpty())
1756 return stdOut;
1757 }
1758 return tr("Detached HEAD");
1759 }
1760
synchronousRevParseCmd(const QString & workingDirectory,const QString & ref,QString * output,QString * errorMessage) const1761 bool GitClient::synchronousRevParseCmd(const QString &workingDirectory, const QString &ref,
1762 QString *output, QString *errorMessage) const
1763 {
1764 const QStringList arguments = {"rev-parse", ref};
1765 QtcProcess proc;
1766 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags);
1767 *output = proc.stdOut().trimmed();
1768 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1769 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
1770 return false;
1771 }
1772
1773 return true;
1774 }
1775
1776 // Retrieve head revision
synchronousTopRevision(const QString & workingDirectory,QDateTime * dateTime)1777 QString GitClient::synchronousTopRevision(const QString &workingDirectory, QDateTime *dateTime)
1778 {
1779 const QStringList arguments = {"show", "-s", "--pretty=format:%H:%ct", HEAD};
1780 QtcProcess proc;
1781 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags);
1782 if (proc.result() != QtcProcess::FinishedWithSuccess)
1783 return QString();
1784 const QStringList output = proc.stdOut().trimmed().split(':');
1785 if (dateTime && output.size() > 1) {
1786 bool ok = false;
1787 const qint64 timeT = output.at(1).toLongLong(&ok);
1788 *dateTime = ok ? QDateTime::fromSecsSinceEpoch(timeT) : QDateTime();
1789 }
1790 return output.first();
1791 }
1792
synchronousTagsForCommit(const QString & workingDirectory,const QString & revision,QString & precedes,QString & follows) const1793 void GitClient::synchronousTagsForCommit(const QString &workingDirectory, const QString &revision,
1794 QString &precedes, QString &follows) const
1795 {
1796 QtcProcess proc1;
1797 vcsFullySynchronousExec(proc1, workingDirectory, {"describe", "--contains", revision}, silentFlags);
1798 precedes = proc1.stdOut();
1799 int tilde = precedes.indexOf('~');
1800 if (tilde != -1)
1801 precedes.truncate(tilde);
1802 else
1803 precedes = precedes.trimmed();
1804
1805 QStringList parents;
1806 QString errorMessage;
1807 synchronousParentRevisions(workingDirectory, revision, &parents, &errorMessage);
1808 for (const QString &p : qAsConst(parents)) {
1809 QtcProcess proc2;
1810 vcsFullySynchronousExec(proc2,
1811 workingDirectory, {"describe", "--tags", "--abbrev=0", p}, silentFlags);
1812 QString pf = proc2.stdOut();
1813 pf.truncate(pf.lastIndexOf('\n'));
1814 if (!pf.isEmpty()) {
1815 if (!follows.isEmpty())
1816 follows += ", ";
1817 follows += pf;
1818 }
1819 }
1820 }
1821
isRemoteCommit(const QString & workingDirectory,const QString & commit)1822 bool GitClient::isRemoteCommit(const QString &workingDirectory, const QString &commit)
1823 {
1824 QtcProcess proc;
1825 vcsFullySynchronousExec(proc, workingDirectory, {"branch", "-r", "--contains", commit}, silentFlags);
1826 return !proc.rawStdOut().isEmpty();
1827 }
1828
isFastForwardMerge(const QString & workingDirectory,const QString & branch)1829 bool GitClient::isFastForwardMerge(const QString &workingDirectory, const QString &branch)
1830 {
1831 QtcProcess proc;
1832 vcsFullySynchronousExec(proc, workingDirectory, {"merge-base", HEAD, branch}, silentFlags);
1833 return proc.stdOut().trimmed() == synchronousTopRevision(workingDirectory);
1834 }
1835
1836 // Format an entry in a one-liner for selection list using git log.
synchronousShortDescription(const QString & workingDirectory,const QString & revision,const QString & format) const1837 QString GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision,
1838 const QString &format) const
1839 {
1840 const QStringList arguments = {"log", noColorOption, ("--pretty=format:" + format),
1841 "--max-count=1", revision};
1842 QtcProcess proc;
1843 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags);
1844 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1845 VcsOutputWindow::appendSilently(tr("Cannot describe revision \"%1\" in \"%2\": %3")
1846 .arg(revision, workingDirectory, proc.stdErr()));
1847 return revision;
1848 }
1849 return stripLastNewline(proc.stdOut());
1850 }
1851
1852 // Create a default message to be used for describing stashes
creatorStashMessage(const QString & keyword=QString ())1853 static inline QString creatorStashMessage(const QString &keyword = QString())
1854 {
1855 QString rc = QCoreApplication::applicationName() + ' ';
1856 if (!keyword.isEmpty())
1857 rc += keyword + ' ';
1858 rc += QDateTime::currentDateTime().toString(Qt::ISODate);
1859 return rc;
1860 }
1861
1862 /* Do a stash and return the message as identifier. Note that stash names (stash{n})
1863 * shift as they are pushed, so, enforce the use of messages to identify them. Flags:
1864 * StashPromptDescription: Prompt the user for a description message.
1865 * StashImmediateRestore: Immediately re-apply this stash (used for snapshots), user keeps on working
1866 * StashIgnoreUnchanged: Be quiet about unchanged repositories (used for IVersionControl's snapshots). */
1867
synchronousStash(const QString & workingDirectory,const QString & messageKeyword,unsigned flags,bool * unchanged) const1868 QString GitClient::synchronousStash(const QString &workingDirectory, const QString &messageKeyword,
1869 unsigned flags, bool *unchanged) const
1870 {
1871 if (unchanged)
1872 *unchanged = false;
1873 QString message;
1874 bool success = false;
1875 // Check for changes and stash
1876 QString errorMessage;
1877 switch (gitStatus(workingDirectory, StatusMode(NoUntracked | NoSubmodules), nullptr, &errorMessage)) {
1878 case StatusChanged: {
1879 message = creatorStashMessage(messageKeyword);
1880 do {
1881 if ((flags & StashPromptDescription)) {
1882 if (!inputText(ICore::dialogParent(),
1883 tr("Stash Description"), tr("Description:"), &message))
1884 break;
1885 }
1886 if (!executeSynchronousStash(workingDirectory, message))
1887 break;
1888 if ((flags & StashImmediateRestore)
1889 && !synchronousStashRestore(workingDirectory, "stash@{0}"))
1890 break;
1891 success = true;
1892 } while (false);
1893 break;
1894 }
1895 case StatusUnchanged:
1896 if (unchanged)
1897 *unchanged = true;
1898 if (!(flags & StashIgnoreUnchanged))
1899 VcsOutputWindow::appendWarning(msgNoChangedFiles());
1900 break;
1901 case StatusFailed:
1902 VcsOutputWindow::appendError(errorMessage);
1903 break;
1904 }
1905 if (!success)
1906 message.clear();
1907 return message;
1908 }
1909
executeSynchronousStash(const QString & workingDirectory,const QString & message,bool unstagedOnly,QString * errorMessage) const1910 bool GitClient::executeSynchronousStash(const QString &workingDirectory,
1911 const QString &message,
1912 bool unstagedOnly,
1913 QString *errorMessage) const
1914 {
1915 QStringList arguments = {"stash", "save"};
1916 if (unstagedOnly)
1917 arguments << "--keep-index";
1918 if (!message.isEmpty())
1919 arguments << message;
1920 const unsigned flags = VcsCommand::ShowStdOut
1921 | VcsCommand::ExpectRepoChanges
1922 | VcsCommand::ShowSuccessMessage;
1923 QtcProcess proc;
1924 vcsSynchronousExec(proc, workingDirectory, arguments, flags);
1925 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1926 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
1927 return false;
1928 }
1929
1930 return true;
1931 }
1932
1933 // Resolve a stash name from message
stashNameFromMessage(const QString & workingDirectory,const QString & message,QString * name,QString * errorMessage) const1934 bool GitClient::stashNameFromMessage(const QString &workingDirectory,
1935 const QString &message, QString *name,
1936 QString *errorMessage) const
1937 {
1938 // All happy
1939 if (message.startsWith(stashNamePrefix)) {
1940 *name = message;
1941 return true;
1942 }
1943 // Retrieve list and find via message
1944 QList<Stash> stashes;
1945 if (!synchronousStashList(workingDirectory, &stashes, errorMessage))
1946 return false;
1947 for (const Stash &s : qAsConst(stashes)) {
1948 if (s.message == message) {
1949 *name = s.name;
1950 return true;
1951 }
1952 }
1953 //: Look-up of a stash via its descriptive message failed.
1954 msgCannotRun(tr("Cannot resolve stash message \"%1\" in \"%2\".")
1955 .arg(message, workingDirectory), errorMessage);
1956 return false;
1957 }
1958
synchronousBranchCmd(const QString & workingDirectory,QStringList branchArgs,QString * output,QString * errorMessage) const1959 bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs,
1960 QString *output, QString *errorMessage) const
1961 {
1962 branchArgs.push_front("branch");
1963 QtcProcess proc;
1964 vcsFullySynchronousExec(proc, workingDirectory, branchArgs);
1965 *output = proc.stdOut();
1966 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1967 msgCannotRun(branchArgs, workingDirectory, proc.stdErr(), errorMessage);
1968 return false;
1969 }
1970 return true;
1971 }
1972
synchronousTagCmd(const QString & workingDirectory,QStringList tagArgs,QString * output,QString * errorMessage) const1973 bool GitClient::synchronousTagCmd(const QString &workingDirectory, QStringList tagArgs,
1974 QString *output, QString *errorMessage) const
1975 {
1976 tagArgs.push_front("tag");
1977 QtcProcess proc;
1978 vcsFullySynchronousExec(proc, workingDirectory, tagArgs);
1979 *output = proc.stdOut();
1980 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1981 msgCannotRun(tagArgs, workingDirectory, proc.stdErr(), errorMessage);
1982 return false;
1983 }
1984 return true;
1985 }
1986
synchronousForEachRefCmd(const QString & workingDirectory,QStringList args,QString * output,QString * errorMessage) const1987 bool GitClient::synchronousForEachRefCmd(const QString &workingDirectory, QStringList args,
1988 QString *output, QString *errorMessage) const
1989 {
1990 args.push_front("for-each-ref");
1991 QtcProcess proc;
1992 vcsFullySynchronousExec(proc, workingDirectory, args, silentFlags);
1993 *output = proc.stdOut();
1994 if (proc.result() != QtcProcess::FinishedWithSuccess) {
1995 msgCannotRun(args, workingDirectory, proc.stdErr(), errorMessage);
1996 return false;
1997 }
1998 return true;
1999 }
2000
asyncForEachRefCmd(const QString & workingDirectory,QStringList args) const2001 VcsCommand *GitClient::asyncForEachRefCmd(const QString &workingDirectory, QStringList args) const
2002 {
2003 args.push_front("for-each-ref");
2004 return vcsExec(workingDirectory, args, nullptr, false, silentFlags);
2005 }
2006
synchronousRemoteCmd(const QString & workingDirectory,QStringList remoteArgs,QString * output,QString * errorMessage,bool silent) const2007 bool GitClient::synchronousRemoteCmd(const QString &workingDirectory, QStringList remoteArgs,
2008 QString *output, QString *errorMessage, bool silent) const
2009 {
2010 remoteArgs.push_front("remote");
2011 QtcProcess proc;
2012 vcsFullySynchronousExec(proc, workingDirectory, remoteArgs, silent ? silentFlags : 0);
2013
2014 const QString stdErr = proc.stdErr();
2015 *errorMessage = stdErr;
2016 *output = proc.stdOut();
2017
2018 if (proc.result() != QtcProcess::FinishedWithSuccess) {
2019 msgCannotRun(remoteArgs, workingDirectory, stdErr, errorMessage);
2020 return false;
2021 }
2022 return true;
2023 }
2024
synchronousRemotesList(const QString & workingDirectory,QString * errorMessage) const2025 QMap<QString,QString> GitClient::synchronousRemotesList(const QString &workingDirectory,
2026 QString *errorMessage) const
2027 {
2028 QMap<QString,QString> result;
2029
2030 QString output;
2031 QString error;
2032 if (!synchronousRemoteCmd(workingDirectory, {"-v"}, &output, &error, true)) {
2033 msgCannotRun(error, errorMessage);
2034 return result;
2035 }
2036
2037 const QStringList remotes = output.split("\n");
2038 for (const QString &remote : remotes) {
2039 if (!remote.endsWith(" (push)"))
2040 continue;
2041
2042 const int tabIndex = remote.indexOf('\t');
2043 if (tabIndex == -1)
2044 continue;
2045 const QString url = remote.mid(tabIndex + 1, remote.length() - tabIndex - 8);
2046 result.insert(remote.left(tabIndex), url);
2047 }
2048 return result;
2049 }
2050
synchronousSubmoduleStatus(const QString & workingDirectory,QString * errorMessage) const2051 QStringList GitClient::synchronousSubmoduleStatus(const QString &workingDirectory,
2052 QString *errorMessage) const
2053 {
2054 // get submodule status
2055 QtcProcess proc;
2056 vcsFullySynchronousExec(proc, workingDirectory, {"submodule", "status"}, silentFlags);
2057
2058 if (proc.result() != QtcProcess::FinishedWithSuccess) {
2059 msgCannotRun(tr("Cannot retrieve submodule status of \"%1\": %2")
2060 .arg(QDir::toNativeSeparators(workingDirectory), proc.stdErr()), errorMessage);
2061 return QStringList();
2062 }
2063 return splitLines(proc.stdOut());
2064 }
2065
submoduleList(const QString & workingDirectory) const2066 SubmoduleDataMap GitClient::submoduleList(const QString &workingDirectory) const
2067 {
2068 SubmoduleDataMap result;
2069 QString gitmodulesFileName = workingDirectory + "/.gitmodules";
2070 if (!QFile::exists(gitmodulesFileName))
2071 return result;
2072
2073 static QMap<QString, SubmoduleDataMap> cachedSubmoduleData;
2074
2075 if (cachedSubmoduleData.contains(workingDirectory))
2076 return cachedSubmoduleData.value(workingDirectory);
2077
2078 const QStringList allConfigs = readConfigValue(workingDirectory, "-l").split('\n');
2079 const QString submoduleLineStart = "submodule.";
2080 for (const QString &configLine : allConfigs) {
2081 if (!configLine.startsWith(submoduleLineStart))
2082 continue;
2083
2084 int nameStart = submoduleLineStart.size();
2085 int nameEnd = configLine.indexOf('.', nameStart);
2086
2087 QString submoduleName = configLine.mid(nameStart, nameEnd - nameStart);
2088
2089 SubmoduleData submoduleData;
2090 if (result.contains(submoduleName))
2091 submoduleData = result[submoduleName];
2092
2093 if (configLine.mid(nameEnd, 5) == ".url=")
2094 submoduleData.url = configLine.mid(nameEnd + 5);
2095 else if (configLine.mid(nameEnd, 8) == ".ignore=")
2096 submoduleData.ignore = configLine.mid(nameEnd + 8);
2097 else
2098 continue;
2099
2100 result.insert(submoduleName, submoduleData);
2101 }
2102
2103 // if config found submodules
2104 if (!result.isEmpty()) {
2105 QSettings gitmodulesFile(gitmodulesFileName, QSettings::IniFormat);
2106
2107 const QList<QString> submodules = result.keys();
2108 for (const QString &submoduleName : submodules) {
2109 gitmodulesFile.beginGroup("submodule \"" + submoduleName + '"');
2110 const QString path = gitmodulesFile.value("path").toString();
2111 if (path.isEmpty()) { // invalid submodule entry in config
2112 result.remove(submoduleName);
2113 } else {
2114 SubmoduleData &submoduleRef = result[submoduleName];
2115 submoduleRef.dir = path;
2116 QString ignore = gitmodulesFile.value("ignore").toString();
2117 if (!ignore.isEmpty() && submoduleRef.ignore.isEmpty())
2118 submoduleRef.ignore = ignore;
2119 }
2120 gitmodulesFile.endGroup();
2121 }
2122 }
2123 cachedSubmoduleData.insert(workingDirectory, result);
2124
2125 return result;
2126 }
2127
synchronousShow(const QString & workingDirectory,const QString & id,unsigned flags) const2128 QByteArray GitClient::synchronousShow(const QString &workingDirectory, const QString &id,
2129 unsigned flags) const
2130 {
2131 if (!canShow(id)) {
2132 VcsOutputWindow::appendError(msgCannotShow(id));
2133 return {};
2134 }
2135 const QStringList arguments = {"show", decorateOption, noColorOption, "--no-patch", id};
2136 QtcProcess proc;
2137 vcsFullySynchronousExec(proc, workingDirectory, arguments, flags);
2138 if (proc.result() != QtcProcess::FinishedWithSuccess) {
2139 msgCannotRun(arguments, workingDirectory, proc.stdErr(), nullptr);
2140 return {};
2141 }
2142 return proc.rawStdOut();
2143 }
2144
2145 // Retrieve list of files to be cleaned
cleanList(const QString & workingDirectory,const QString & modulePath,const QString & flag,QStringList * files,QString * errorMessage)2146 bool GitClient::cleanList(const QString &workingDirectory, const QString &modulePath,
2147 const QString &flag, QStringList *files, QString *errorMessage)
2148 {
2149 const QString directory = workingDirectory + '/' + modulePath;
2150 const QStringList arguments = {"clean", "--dry-run", flag};
2151
2152 QtcProcess proc;
2153 vcsFullySynchronousExec(proc, directory, arguments, VcsCommand::ForceCLocale);
2154 if (proc.result() != QtcProcess::FinishedWithSuccess) {
2155 msgCannotRun(arguments, directory, proc.stdErr(), errorMessage);
2156 return false;
2157 }
2158
2159 // Filter files that git would remove
2160 const QString relativeBase = modulePath.isEmpty() ? QString() : modulePath + '/';
2161 const QString prefix = "Would remove ";
2162 const QStringList removeLines = Utils::filtered(
2163 splitLines(proc.stdOut()), [](const QString &s) {
2164 return s.startsWith("Would remove ");
2165 });
2166 *files = Utils::transform(removeLines, [&relativeBase, &prefix](const QString &s) -> QString {
2167 return relativeBase + s.mid(prefix.size());
2168 });
2169 return true;
2170 }
2171
synchronousCleanList(const QString & workingDirectory,const QString & modulePath,QStringList * files,QStringList * ignoredFiles,QString * errorMessage)2172 bool GitClient::synchronousCleanList(const QString &workingDirectory, const QString &modulePath,
2173 QStringList *files, QStringList *ignoredFiles,
2174 QString *errorMessage)
2175 {
2176 bool res = cleanList(workingDirectory, modulePath, "-df", files, errorMessage);
2177 res &= cleanList(workingDirectory, modulePath, "-dXf", ignoredFiles, errorMessage);
2178
2179 const SubmoduleDataMap submodules = submoduleList(workingDirectory + '/' + modulePath);
2180 for (const SubmoduleData &submodule : submodules) {
2181 if (submodule.ignore != "all"
2182 && submodule.ignore != "dirty") {
2183 const QString submodulePath = modulePath.isEmpty() ? submodule.dir
2184 : modulePath + '/' + submodule.dir;
2185 res &= synchronousCleanList(workingDirectory, submodulePath,
2186 files, ignoredFiles, errorMessage);
2187 }
2188 }
2189 return res;
2190 }
2191
synchronousApplyPatch(const QString & workingDirectory,const QString & file,QString * errorMessage,const QStringList & extraArguments)2192 bool GitClient::synchronousApplyPatch(const QString &workingDirectory,
2193 const QString &file, QString *errorMessage,
2194 const QStringList &extraArguments)
2195 {
2196 QStringList arguments = {"apply", "--whitespace=fix"};
2197 arguments << extraArguments << file;
2198
2199 QtcProcess proc;
2200 vcsFullySynchronousExec(proc, workingDirectory, arguments);
2201 const QString stdErr = proc.stdErr();
2202 if (proc.result() == QtcProcess::FinishedWithSuccess) {
2203 if (!stdErr.isEmpty())
2204 *errorMessage = tr("There were warnings while applying \"%1\" to \"%2\":\n%3")
2205 .arg(file, workingDirectory, stdErr);
2206 return true;
2207 } else {
2208 *errorMessage = tr("Cannot apply patch \"%1\" to \"%2\": %3")
2209 .arg(QDir::toNativeSeparators(file), workingDirectory, stdErr);
2210 return false;
2211 }
2212 }
2213
processEnvironment() const2214 Environment GitClient::processEnvironment() const
2215 {
2216 Environment environment = VcsBaseClientImpl::processEnvironment();
2217 QString gitPath = settings().path.value();
2218 environment.prependOrSetPath(gitPath);
2219 if (HostOsInfo::isWindowsHost() && settings().winSetHomeEnvironment.value())
2220 environment.set("HOME", QDir::toNativeSeparators(QDir::homePath()));
2221 environment.set("GIT_EDITOR", m_disableEditor ? "true" : m_gitQtcEditor);
2222 return environment;
2223 }
2224
beginStashScope(const QString & workingDirectory,const QString & command,StashFlag flag,PushAction pushAction)2225 bool GitClient::beginStashScope(const QString &workingDirectory, const QString &command,
2226 StashFlag flag, PushAction pushAction)
2227 {
2228 const QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
2229 QTC_ASSERT(!repoDirectory.isEmpty(), return false);
2230 StashInfo &stashInfo = m_stashInfo[repoDirectory];
2231 return stashInfo.init(repoDirectory, command, flag, pushAction);
2232 }
2233
stashInfo(const QString & workingDirectory)2234 GitClient::StashInfo &GitClient::stashInfo(const QString &workingDirectory)
2235 {
2236 const QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
2237 QTC_CHECK(m_stashInfo.contains(repoDirectory));
2238 return m_stashInfo[repoDirectory];
2239 }
2240
endStashScope(const QString & workingDirectory)2241 void GitClient::endStashScope(const QString &workingDirectory)
2242 {
2243 const QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
2244 if (!m_stashInfo.contains(repoDirectory))
2245 return;
2246 m_stashInfo[repoDirectory].end();
2247 }
2248
isValidRevision(const QString & revision) const2249 bool GitClient::isValidRevision(const QString &revision) const
2250 {
2251 if (revision.length() < 1)
2252 return false;
2253 for (const auto i : revision)
2254 if (i != '0')
2255 return true;
2256 return false;
2257 }
2258
updateSubmodulesIfNeeded(const QString & workingDirectory,bool prompt)2259 void GitClient::updateSubmodulesIfNeeded(const QString &workingDirectory, bool prompt)
2260 {
2261 if (!m_updatedSubmodules.isEmpty() || submoduleList(workingDirectory).isEmpty())
2262 return;
2263
2264 const QStringList submoduleStatus = synchronousSubmoduleStatus(workingDirectory);
2265 if (submoduleStatus.isEmpty())
2266 return;
2267
2268 bool updateNeeded = false;
2269 for (const QString &status : submoduleStatus) {
2270 if (status.startsWith('+')) {
2271 updateNeeded = true;
2272 break;
2273 }
2274 }
2275 if (!updateNeeded)
2276 return;
2277
2278 if (prompt && QMessageBox::question(ICore::dialogParent(), tr("Submodules Found"),
2279 tr("Would you like to update submodules?"),
2280 QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
2281 return;
2282 }
2283
2284 for (const QString &statusLine : submoduleStatus) {
2285 // stash only for lines starting with +
2286 // because only they would be updated
2287 if (!statusLine.startsWith('+'))
2288 continue;
2289
2290 // get submodule name
2291 const int nameStart = statusLine.indexOf(' ', 2) + 1;
2292 const int nameLength = statusLine.indexOf(' ', nameStart) - nameStart;
2293 const QString submoduleDir = workingDirectory + '/' + statusLine.mid(nameStart, nameLength);
2294
2295 if (beginStashScope(submoduleDir, "SubmoduleUpdate")) {
2296 m_updatedSubmodules.append(submoduleDir);
2297 } else {
2298 finishSubmoduleUpdate();
2299 return;
2300 }
2301 }
2302
2303 VcsCommand *cmd = vcsExec(workingDirectory, {"submodule", "update"}, nullptr, true,
2304 VcsCommand::ExpectRepoChanges);
2305 connect(cmd, &VcsCommand::finished, this, &GitClient::finishSubmoduleUpdate);
2306 }
2307
finishSubmoduleUpdate()2308 void GitClient::finishSubmoduleUpdate()
2309 {
2310 for (const QString &submoduleDir : qAsConst(m_updatedSubmodules))
2311 endStashScope(submoduleDir);
2312 m_updatedSubmodules.clear();
2313 }
2314
gitStatus(const QString & workingDirectory,StatusMode mode,QString * output,QString * errorMessage) const2315 GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory, StatusMode mode,
2316 QString *output, QString *errorMessage) const
2317 {
2318 // Run 'status'. Note that git returns exitcode 1 if there are no added files.
2319 QStringList arguments = {"status"};
2320 if (mode & NoUntracked)
2321 arguments << "--untracked-files=no";
2322 else
2323 arguments << "--untracked-files=all";
2324 if (mode & NoSubmodules)
2325 arguments << "--ignore-submodules=all";
2326 arguments << "--porcelain" << "-b";
2327
2328 QtcProcess proc;
2329 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags);
2330 const QString stdOut = proc.stdOut();
2331
2332 if (output)
2333 *output = stdOut;
2334
2335 const bool statusRc = proc.result() == QtcProcess::FinishedWithSuccess;
2336 const bool branchKnown = !stdOut.startsWith("## HEAD (no branch)\n");
2337 // Is it something really fatal?
2338 if (!statusRc && !branchKnown) {
2339 if (errorMessage) {
2340 *errorMessage = tr("Cannot obtain status: %1").arg(proc.stdErr());
2341 }
2342 return StatusFailed;
2343 }
2344 // Unchanged (output text depending on whether -u was passed)
2345 const bool hasChanges = Utils::contains(stdOut.split('\n'), [](const QString &s) {
2346 return !s.isEmpty() && !s.startsWith('#');
2347 });
2348 return hasChanges ? StatusChanged : StatusUnchanged;
2349 }
2350
commandInProgressDescription(const QString & workingDirectory) const2351 QString GitClient::commandInProgressDescription(const QString &workingDirectory) const
2352 {
2353 switch (checkCommandInProgress(workingDirectory)) {
2354 case NoCommand:
2355 break;
2356 case Rebase:
2357 case RebaseMerge:
2358 return tr("REBASING");
2359 case Revert:
2360 return tr("REVERTING");
2361 case CherryPick:
2362 return tr("CHERRY-PICKING");
2363 case Merge:
2364 return tr("MERGING");
2365 }
2366 return QString();
2367 }
2368
checkCommandInProgress(const QString & workingDirectory) const2369 GitClient::CommandInProgress GitClient::checkCommandInProgress(const QString &workingDirectory) const
2370 {
2371 const QString gitDir = findGitDirForRepository(workingDirectory);
2372 if (QFile::exists(gitDir + "/MERGE_HEAD"))
2373 return Merge;
2374 else if (QFile::exists(gitDir + "/rebase-apply"))
2375 return Rebase;
2376 else if (QFile::exists(gitDir + "/rebase-merge"))
2377 return RebaseMerge;
2378 else if (QFile::exists(gitDir + "/REVERT_HEAD"))
2379 return Revert;
2380 else if (QFile::exists(gitDir + "/CHERRY_PICK_HEAD"))
2381 return CherryPick;
2382 else
2383 return NoCommand;
2384 }
2385
continueCommandIfNeeded(const QString & workingDirectory,bool allowContinue)2386 void GitClient::continueCommandIfNeeded(const QString &workingDirectory, bool allowContinue)
2387 {
2388 if (GitPlugin::isCommitEditorOpen())
2389 return;
2390 CommandInProgress command = checkCommandInProgress(workingDirectory);
2391 ContinueCommandMode continueMode;
2392 if (allowContinue)
2393 continueMode = command == RebaseMerge ? ContinueOnly : SkipIfNoChanges;
2394 else
2395 continueMode = SkipOnly;
2396 switch (command) {
2397 case Rebase:
2398 case RebaseMerge:
2399 continuePreviousGitCommand(workingDirectory, tr("Continue Rebase"),
2400 tr("Rebase is in progress. What do you want to do?"),
2401 tr("Continue"), "rebase", continueMode);
2402 break;
2403 case Merge:
2404 continuePreviousGitCommand(workingDirectory, tr("Continue Merge"),
2405 tr("You need to commit changes to finish merge.\nCommit now?"),
2406 tr("Commit"), "merge", continueMode);
2407 break;
2408 case Revert:
2409 continuePreviousGitCommand(workingDirectory, tr("Continue Revert"),
2410 tr("You need to commit changes to finish revert.\nCommit now?"),
2411 tr("Commit"), "revert", continueMode);
2412 break;
2413 case CherryPick:
2414 continuePreviousGitCommand(workingDirectory, tr("Continue Cherry-Picking"),
2415 tr("You need to commit changes to finish cherry-picking.\nCommit now?"),
2416 tr("Commit"), "cherry-pick", continueMode);
2417 break;
2418 default:
2419 break;
2420 }
2421 }
2422
continuePreviousGitCommand(const QString & workingDirectory,const QString & msgBoxTitle,QString msgBoxText,const QString & buttonName,const QString & gitCommand,ContinueCommandMode continueMode)2423 void GitClient::continuePreviousGitCommand(const QString &workingDirectory,
2424 const QString &msgBoxTitle, QString msgBoxText,
2425 const QString &buttonName, const QString &gitCommand,
2426 ContinueCommandMode continueMode)
2427 {
2428 bool isRebase = gitCommand == "rebase";
2429 bool hasChanges = false;
2430 switch (continueMode) {
2431 case ContinueOnly:
2432 hasChanges = true;
2433 break;
2434 case SkipIfNoChanges:
2435 hasChanges = gitStatus(workingDirectory, StatusMode(NoUntracked | NoSubmodules))
2436 == GitClient::StatusChanged;
2437 if (!hasChanges)
2438 msgBoxText.prepend(tr("No changes found.") + ' ');
2439 break;
2440 case SkipOnly:
2441 hasChanges = false;
2442 break;
2443 }
2444
2445 QMessageBox msgBox(QMessageBox::Question, msgBoxTitle, msgBoxText,
2446 QMessageBox::NoButton, ICore::dialogParent());
2447 if (hasChanges || isRebase)
2448 msgBox.addButton(hasChanges ? buttonName : tr("Skip"), QMessageBox::AcceptRole);
2449 msgBox.addButton(QMessageBox::Abort);
2450 msgBox.addButton(QMessageBox::Ignore);
2451 switch (msgBox.exec()) {
2452 case QMessageBox::Ignore:
2453 break;
2454 case QMessageBox::Abort:
2455 synchronousAbortCommand(workingDirectory, gitCommand);
2456 break;
2457 default: // Continue/Skip
2458 if (isRebase)
2459 rebase(workingDirectory, QLatin1String(hasChanges ? "--continue" : "--skip"));
2460 else
2461 GitPlugin::startCommit();
2462 }
2463 }
2464
extendedShowDescription(const QString & workingDirectory,const QString & text) const2465 QString GitClient::extendedShowDescription(const QString &workingDirectory, const QString &text) const
2466 {
2467 if (!text.startsWith("commit "))
2468 return text;
2469 QString modText = text;
2470 QString precedes, follows;
2471 int lastHeaderLine = modText.indexOf("\n\n") + 1;
2472 const QString commit = modText.mid(7, 8);
2473 synchronousTagsForCommit(workingDirectory, commit, precedes, follows);
2474 if (!precedes.isEmpty())
2475 modText.insert(lastHeaderLine, "Precedes: " + precedes + '\n');
2476 if (!follows.isEmpty())
2477 modText.insert(lastHeaderLine, "Follows: " + follows + '\n');
2478
2479 // Empty line before headers and commit message
2480 const int emptyLine = modText.indexOf("\n\n");
2481 if (emptyLine != -1)
2482 modText.insert(emptyLine, QString('\n') + Constants::EXPAND_BRANCHES);
2483
2484 return modText;
2485 }
2486
2487 // Quietly retrieve branch list of remote repository URL
2488 //
2489 // The branch HEAD is pointing to is always returned first.
synchronousRepositoryBranches(const QString & repositoryURL,const QString & workingDirectory) const2490 QStringList GitClient::synchronousRepositoryBranches(const QString &repositoryURL,
2491 const QString &workingDirectory) const
2492 {
2493 const unsigned flags = VcsCommand::SshPasswordPrompt
2494 | VcsCommand::SuppressStdErr
2495 | VcsCommand::SuppressFailMessage;
2496 QtcProcess proc;
2497 vcsSynchronousExec(proc,
2498 workingDirectory, {"ls-remote", repositoryURL, HEAD, "refs/heads/*"}, flags);
2499 QStringList branches;
2500 branches << tr("<Detached HEAD>");
2501 QString headSha;
2502 // split "82bfad2f51d34e98b18982211c82220b8db049b<tab>refs/heads/master"
2503 bool headFound = false;
2504 bool branchFound = false;
2505 const QStringList lines = proc.stdOut().split('\n');
2506 for (const QString &line : lines) {
2507 if (line.endsWith("\tHEAD")) {
2508 QTC_CHECK(headSha.isNull());
2509 headSha = line.left(line.indexOf('\t'));
2510 continue;
2511 }
2512
2513 const QString pattern = "\trefs/heads/";
2514 const int pos = line.lastIndexOf(pattern);
2515 if (pos != -1) {
2516 branchFound = true;
2517 const QString branchName = line.mid(pos + pattern.count());
2518 if (!headFound && line.startsWith(headSha)) {
2519 branches[0] = branchName;
2520 headFound = true;
2521 } else {
2522 branches.push_back(branchName);
2523 }
2524 }
2525 }
2526 if (!branchFound)
2527 branches.clear();
2528 return branches;
2529 }
2530
launchGitK(const QString & workingDirectory,const QString & fileName) const2531 void GitClient::launchGitK(const QString &workingDirectory, const QString &fileName) const
2532 {
2533 const QFileInfo binaryInfo = vcsBinary().toFileInfo();
2534 QDir foundBinDir(binaryInfo.dir());
2535 const bool foundBinDirIsBinDir = foundBinDir.dirName() == "bin";
2536 Environment env = processEnvironment();
2537 if (tryLauchingGitK(env, workingDirectory, fileName, foundBinDir.path()))
2538 return;
2539
2540 QString gitkPath = foundBinDir.path() + "/gitk";
2541 VcsOutputWindow::appendSilently(msgCannotLaunch(gitkPath));
2542
2543 if (foundBinDirIsBinDir) {
2544 foundBinDir.cdUp();
2545 const QString binDirName = foundBinDir.dirName();
2546 if (binDirName == "usr" || binDirName.startsWith("mingw"))
2547 foundBinDir.cdUp();
2548 if (tryLauchingGitK(env, workingDirectory, fileName,
2549 foundBinDir.path() + "/cmd")) {
2550 return;
2551 }
2552 gitkPath = foundBinDir.path() + "/cmd/gitk";
2553 VcsOutputWindow::appendSilently(msgCannotLaunch(gitkPath));
2554 }
2555
2556 Environment sysEnv = Environment::systemEnvironment();
2557 const FilePath exec = sysEnv.searchInPath("gitk");
2558
2559 if (!exec.isEmpty() && tryLauchingGitK(env, workingDirectory, fileName,
2560 exec.parentDir().toString())) {
2561 return;
2562 }
2563
2564 VcsOutputWindow::appendError(msgCannotLaunch("gitk"));
2565 }
2566
launchRepositoryBrowser(const QString & workingDirectory) const2567 void GitClient::launchRepositoryBrowser(const QString &workingDirectory) const
2568 {
2569 const QString repBrowserBinary = settings().repositoryBrowserCmd.value();
2570 if (!repBrowserBinary.isEmpty())
2571 QProcess::startDetached(repBrowserBinary, {workingDirectory}, workingDirectory);
2572 }
2573
tryLauchingGitK(const Environment & env,const QString & workingDirectory,const QString & fileName,const QString & gitBinDirectory) const2574 bool GitClient::tryLauchingGitK(const Environment &env,
2575 const QString &workingDirectory,
2576 const QString &fileName,
2577 const QString &gitBinDirectory) const
2578 {
2579 QString binary = gitBinDirectory + "/gitk";
2580 QStringList arguments;
2581 if (HostOsInfo::isWindowsHost()) {
2582 // If git/bin is in path, use 'wish' shell to run. Otherwise (git/cmd), directly run gitk
2583 QString wish = gitBinDirectory + "/wish";
2584 if (QFileInfo::exists(wish + ".exe")) {
2585 arguments << binary;
2586 binary = wish;
2587 }
2588 }
2589 const QString gitkOpts = settings().gitkOptions.value();
2590 if (!gitkOpts.isEmpty())
2591 arguments.append(ProcessArgs::splitArgs(gitkOpts, HostOsInfo::hostOs()));
2592 if (!fileName.isEmpty())
2593 arguments << "--" << fileName;
2594 VcsOutputWindow::appendCommand(workingDirectory, {binary, arguments});
2595 // This should always use QProcess::startDetached (as not to kill
2596 // the child), but that does not have an environment parameter.
2597 bool success = false;
2598 if (!settings().path.value().isEmpty()) {
2599 auto process = new QProcess;
2600 process->setWorkingDirectory(workingDirectory);
2601 process->setProcessEnvironment(env.toProcessEnvironment());
2602 process->start(binary, arguments);
2603 success = process->waitForStarted();
2604 if (success)
2605 connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
2606 process, &QProcess::deleteLater);
2607 else
2608 delete process;
2609 } else {
2610 success = QProcess::startDetached(binary, arguments, workingDirectory);
2611 }
2612
2613 return success;
2614 }
2615
launchGitGui(const QString & workingDirectory)2616 bool GitClient::launchGitGui(const QString &workingDirectory) {
2617 bool success = true;
2618 FilePath gitBinary = vcsBinary();
2619 if (gitBinary.isEmpty()) {
2620 success = false;
2621 } else {
2622 success = QProcess::startDetached(gitBinary.toString(), {"gui"},
2623 workingDirectory);
2624 }
2625
2626 if (!success)
2627 VcsOutputWindow::appendError(msgCannotLaunch("git gui"));
2628
2629 return success;
2630 }
2631
gitBinDirectory() const2632 FilePath GitClient::gitBinDirectory() const
2633 {
2634 const QString git = vcsBinary().toString();
2635 if (git.isEmpty())
2636 return FilePath();
2637
2638 // Is 'git\cmd' in the path (folder containing .bats)?
2639 QString path = QFileInfo(git).absolutePath();
2640 // Git for Windows has git and gitk redirect executables in {setup dir}/cmd
2641 // and the real binaries are in {setup dir}/bin. If cmd is configured in PATH
2642 // or in Git settings, return bin instead.
2643 if (HostOsInfo::isWindowsHost()) {
2644 if (path.endsWith("/cmd", Qt::CaseInsensitive))
2645 path.replace(path.size() - 3, 3, "bin");
2646 if (path.endsWith("/bin", Qt::CaseInsensitive)
2647 && !path.endsWith("/usr/bin", Qt::CaseInsensitive)) {
2648 // Legacy msysGit used Git/bin for additional tools.
2649 // Git for Windows uses Git/usr/bin. Prefer that if it exists.
2650 QString usrBinPath = path;
2651 usrBinPath.replace(usrBinPath.size() - 3, 3, "usr/bin");
2652 if (QFile::exists(usrBinPath))
2653 path = usrBinPath;
2654 }
2655 }
2656 return FilePath::fromString(path);
2657 }
2658
launchGitBash(const QString & workingDirectory)2659 bool GitClient::launchGitBash(const QString &workingDirectory)
2660 {
2661 bool success = true;
2662 const QString git = vcsBinary().toString();
2663
2664 if (git.isEmpty()) {
2665 success = false;
2666 } else {
2667 const QString gitBash = QFileInfo(git).absolutePath() + "/../git-bash.exe";
2668 success = QProcess::startDetached(gitBash, {}, workingDirectory);
2669 }
2670
2671 if (!success)
2672 VcsOutputWindow::appendError(msgCannotLaunch("git-bash"));
2673
2674 return success;
2675 }
2676
vcsBinary() const2677 FilePath GitClient::vcsBinary() const
2678 {
2679 bool ok;
2680 Utils::FilePath binary = static_cast<GitSettings &>(settings()).gitExecutable(&ok);
2681 if (!ok)
2682 return Utils::FilePath();
2683 return binary;
2684 }
2685
encoding(const QString & workingDirectory,const QString & configVar) const2686 QTextCodec *GitClient::encoding(const QString &workingDirectory, const QString &configVar) const
2687 {
2688 QString codecName = readConfigValue(workingDirectory, configVar).trimmed();
2689 // Set default commit encoding to 'UTF-8', when it's not set,
2690 // to solve displaying error of commit log with non-latin characters.
2691 if (codecName.isEmpty())
2692 return QTextCodec::codecForName("UTF-8");
2693 return QTextCodec::codecForName(codecName.toUtf8());
2694 }
2695
2696 // returns first line from log and removes it
shiftLogLine(QByteArray & logText)2697 static QByteArray shiftLogLine(QByteArray &logText)
2698 {
2699 const int index = logText.indexOf('\n');
2700 const QByteArray res = logText.left(index);
2701 logText.remove(0, index + 1);
2702 return res;
2703 }
2704
readDataFromCommit(const QString & repoDirectory,const QString & commit,CommitData & commitData,QString * errorMessage,QString * commitTemplate)2705 bool GitClient::readDataFromCommit(const QString &repoDirectory, const QString &commit,
2706 CommitData &commitData, QString *errorMessage,
2707 QString *commitTemplate)
2708 {
2709 // Get commit data as "SHA1<lf>author<lf>email<lf>message".
2710 const QStringList arguments = {"log", "--max-count=1", "--pretty=format:%h\n%an\n%ae\n%B", commit};
2711 QtcProcess proc;
2712 vcsFullySynchronousExec(proc, repoDirectory, arguments, silentFlags);
2713
2714 if (proc.result() != QtcProcess::FinishedWithSuccess) {
2715 if (errorMessage) {
2716 *errorMessage = tr("Cannot retrieve last commit data of repository \"%1\".")
2717 .arg(QDir::toNativeSeparators(repoDirectory));
2718 }
2719 return false;
2720 }
2721
2722 QTextCodec *authorCodec = HostOsInfo::isWindowsHost()
2723 ? QTextCodec::codecForName("UTF-8")
2724 : commitData.commitEncoding;
2725 QByteArray stdOut = proc.rawStdOut();
2726 commitData.amendSHA1 = QLatin1String(shiftLogLine(stdOut));
2727 commitData.panelData.author = authorCodec->toUnicode(shiftLogLine(stdOut));
2728 commitData.panelData.email = authorCodec->toUnicode(shiftLogLine(stdOut));
2729 if (commitTemplate)
2730 *commitTemplate = commitData.commitEncoding->toUnicode(stdOut);
2731 return true;
2732 }
2733
getCommitData(const QString & workingDirectory,QString * commitTemplate,CommitData & commitData,QString * errorMessage)2734 bool GitClient::getCommitData(const QString &workingDirectory,
2735 QString *commitTemplate,
2736 CommitData &commitData,
2737 QString *errorMessage)
2738 {
2739 commitData.clear();
2740
2741 // Find repo
2742 const QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
2743 if (repoDirectory.isEmpty()) {
2744 *errorMessage = msgRepositoryNotFound(workingDirectory);
2745 return false;
2746 }
2747
2748 commitData.panelInfo.repository = repoDirectory;
2749
2750 QString gitDir = findGitDirForRepository(repoDirectory);
2751 if (gitDir.isEmpty()) {
2752 *errorMessage = tr("The repository \"%1\" is not initialized.").arg(repoDirectory);
2753 return false;
2754 }
2755
2756 // Run status. Note that it has exitcode 1 if there are no added files.
2757 QString output;
2758 if (commitData.commitType == FixupCommit) {
2759 synchronousLog(repoDirectory, {HEAD, "--not", "--remotes", "-n1"}, &output, errorMessage,
2760 VcsCommand::SuppressCommandLogging);
2761 if (output.isEmpty()) {
2762 *errorMessage = msgNoCommits(false);
2763 return false;
2764 }
2765 }
2766 const StatusResult status = gitStatus(repoDirectory, ShowAll, &output, errorMessage);
2767 switch (status) {
2768 case StatusChanged:
2769 break;
2770 case StatusUnchanged:
2771 if (commitData.commitType == AmendCommit) // amend might be run just for the commit message
2772 break;
2773 *errorMessage = msgNoChangedFiles();
2774 return false;
2775 case StatusFailed:
2776 return false;
2777 }
2778
2779 // Output looks like:
2780 // ## branch_name
2781 // MM filename
2782 // A new_unstaged_file
2783 // R old -> new
2784 // D deleted_file
2785 // ?? untracked_file
2786 if (status != StatusUnchanged) {
2787 if (!commitData.parseFilesFromStatus(output)) {
2788 *errorMessage = msgParseFilesFailed();
2789 return false;
2790 }
2791
2792 // Filter out untracked files that are not part of the project
2793 QStringList untrackedFiles = commitData.filterFiles(UntrackedFile);
2794
2795 VcsBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &untrackedFiles);
2796 QList<CommitData::StateFilePair> filteredFiles;
2797 QList<CommitData::StateFilePair>::const_iterator it = commitData.files.constBegin();
2798 for ( ; it != commitData.files.constEnd(); ++it) {
2799 if (it->first == UntrackedFile && !untrackedFiles.contains(it->second))
2800 continue;
2801 filteredFiles.append(*it);
2802 }
2803 commitData.files = filteredFiles;
2804
2805 if (commitData.files.isEmpty() && commitData.commitType != AmendCommit) {
2806 *errorMessage = msgNoChangedFiles();
2807 return false;
2808 }
2809 }
2810
2811 commitData.commitEncoding = encoding(workingDirectory, "i18n.commitEncoding");
2812
2813 // Get the commit template or the last commit message
2814 switch (commitData.commitType) {
2815 case AmendCommit: {
2816 if (!readDataFromCommit(repoDirectory, HEAD, commitData, errorMessage, commitTemplate))
2817 return false;
2818 break;
2819 }
2820 case SimpleCommit: {
2821 bool authorFromCherryPick = false;
2822 QDir gitDirectory(gitDir);
2823 // For cherry-picked commit, read author data from the commit (but template from MERGE_MSG)
2824 if (gitDirectory.exists(CHERRY_PICK_HEAD)) {
2825 authorFromCherryPick = readDataFromCommit(repoDirectory, CHERRY_PICK_HEAD, commitData);
2826 commitData.amendSHA1.clear();
2827 }
2828 if (!authorFromCherryPick) {
2829 // the format is:
2830 // Joe Developer <joedev@example.com> unixtimestamp +HHMM
2831 QString author_info = readGitVar(workingDirectory, "GIT_AUTHOR_IDENT");
2832 int lt = author_info.lastIndexOf('<');
2833 int gt = author_info.lastIndexOf('>');
2834 if (gt == -1 || uint(lt) > uint(gt)) {
2835 // shouldn't happen!
2836 commitData.panelData.author.clear();
2837 commitData.panelData.email.clear();
2838 } else {
2839 commitData.panelData.author = author_info.left(lt - 1);
2840 commitData.panelData.email = author_info.mid(lt + 1, gt - lt - 1);
2841 }
2842 }
2843 // Commit: Get the commit template
2844 QString templateFilename = gitDirectory.absoluteFilePath("MERGE_MSG");
2845 if (!QFile::exists(templateFilename))
2846 templateFilename = gitDirectory.absoluteFilePath("SQUASH_MSG");
2847 if (!QFile::exists(templateFilename)) {
2848 FilePath templateName = FilePath::fromUserInput(
2849 readConfigValue(workingDirectory, "commit.template"));
2850 templateFilename = templateName.toString();
2851 }
2852 if (!templateFilename.isEmpty()) {
2853 // Make relative to repository
2854 const QFileInfo templateFileInfo(templateFilename);
2855 if (templateFileInfo.isRelative())
2856 templateFilename = repoDirectory + '/' + templateFilename;
2857 FileReader reader;
2858 if (!reader.fetch(Utils::FilePath::fromString(templateFilename), QIODevice::Text, errorMessage))
2859 return false;
2860 *commitTemplate = QString::fromLocal8Bit(reader.data());
2861 }
2862 break;
2863 }
2864 case FixupCommit:
2865 break;
2866 }
2867
2868 commitData.enablePush = !synchronousRemotesList(repoDirectory).isEmpty();
2869 if (commitData.enablePush) {
2870 CommandInProgress commandInProgress = checkCommandInProgress(repoDirectory);
2871 if (commandInProgress == Rebase || commandInProgress == RebaseMerge)
2872 commitData.enablePush = false;
2873 }
2874
2875 return true;
2876 }
2877
2878 // Log message for commits/amended commits to go to output window
msgCommitted(const QString & amendSHA1,int fileCount)2879 static inline QString msgCommitted(const QString &amendSHA1, int fileCount)
2880 {
2881 if (amendSHA1.isEmpty())
2882 return GitClient::tr("Committed %n files.", nullptr, fileCount) + '\n';
2883 if (fileCount)
2884 return GitClient::tr("Amended \"%1\" (%n files).", nullptr, fileCount).arg(amendSHA1) + '\n';
2885 return GitClient::tr("Amended \"%1\".").arg(amendSHA1);
2886 }
2887
addAndCommit(const QString & repositoryDirectory,const GitSubmitEditorPanelData & data,CommitType commitType,const QString & amendSHA1,const QString & messageFile,SubmitFileModel * model)2888 bool GitClient::addAndCommit(const QString &repositoryDirectory,
2889 const GitSubmitEditorPanelData &data,
2890 CommitType commitType,
2891 const QString &amendSHA1,
2892 const QString &messageFile,
2893 SubmitFileModel *model)
2894 {
2895 const QString renameSeparator = " -> ";
2896
2897 QStringList filesToAdd;
2898 QStringList filesToRemove;
2899 QStringList filesToReset;
2900
2901 int commitCount = 0;
2902
2903 for (int i = 0; i < model->rowCount(); ++i) {
2904 const FileStates state = static_cast<FileStates>(model->extraData(i).toInt());
2905 QString file = model->file(i);
2906 const bool checked = model->checked(i);
2907
2908 if (checked)
2909 ++commitCount;
2910
2911 if (state == UntrackedFile && checked)
2912 filesToAdd.append(file);
2913
2914 if ((state & StagedFile) && !checked) {
2915 if (state & (ModifiedFile | AddedFile | DeletedFile | TypeChangedFile)) {
2916 filesToReset.append(file);
2917 } else if (state & (RenamedFile | CopiedFile)) {
2918 const QString newFile = file.mid(file.indexOf(renameSeparator) + renameSeparator.count());
2919 filesToReset.append(newFile);
2920 }
2921 } else if (state & UnmergedFile && checked) {
2922 QTC_ASSERT(false, continue); // There should not be unmerged files when committing!
2923 }
2924
2925 if ((state == ModifiedFile || state == TypeChangedFile) && checked) {
2926 filesToReset.removeAll(file);
2927 filesToAdd.append(file);
2928 } else if (state == AddedFile && checked) {
2929 filesToAdd.append(file);
2930 } else if (state == DeletedFile && checked) {
2931 filesToReset.removeAll(file);
2932 filesToRemove.append(file);
2933 } else if (state == RenamedFile && checked) {
2934 QTC_ASSERT(false, continue); // git mv directly stages.
2935 } else if (state == CopiedFile && checked) {
2936 QTC_ASSERT(false, continue); // only is noticed after adding a new file to the index
2937 } else if (state == UnmergedFile && checked) {
2938 QTC_ASSERT(false, continue); // There should not be unmerged files when committing!
2939 }
2940 }
2941
2942 if (!filesToReset.isEmpty() && !synchronousReset(repositoryDirectory, filesToReset))
2943 return false;
2944
2945 if (!filesToRemove.isEmpty() && !synchronousDelete(repositoryDirectory, true, filesToRemove))
2946 return false;
2947
2948 if (!filesToAdd.isEmpty() && !synchronousAdd(repositoryDirectory, filesToAdd))
2949 return false;
2950
2951 // Do the final commit
2952 QStringList arguments = {"commit"};
2953 if (commitType == FixupCommit) {
2954 arguments << "--fixup" << amendSHA1;
2955 } else {
2956 arguments << "-F" << QDir::toNativeSeparators(messageFile);
2957 if (commitType == AmendCommit)
2958 arguments << "--amend";
2959 const QString &authorString = data.authorString();
2960 if (!authorString.isEmpty())
2961 arguments << "--author" << authorString;
2962 if (data.bypassHooks)
2963 arguments << "--no-verify";
2964 if (data.signOff)
2965 arguments << "--signoff";
2966 }
2967
2968 QtcProcess proc;
2969 vcsSynchronousExec(proc, repositoryDirectory, arguments, VcsCommand::NoFullySync);
2970 if (proc.result() == QtcProcess::FinishedWithSuccess) {
2971 VcsOutputWindow::appendMessage(msgCommitted(amendSHA1, commitCount));
2972 GitPlugin::updateCurrentBranch();
2973 return true;
2974 } else {
2975 VcsOutputWindow::appendError(tr("Cannot commit %n files\n", nullptr, commitCount));
2976 return false;
2977 }
2978 }
2979
2980 /* Revert: This function can be called with a file list (to revert single
2981 * files) or a single directory (revert all). Qt Creator currently has only
2982 * 'revert single' in its VCS menus, but the code is prepared to deal with
2983 * reverting a directory pending a sophisticated selection dialog in the
2984 * VcsBase plugin. */
revertI(QStringList files,bool * ptrToIsDirectory,QString * errorMessage,bool revertStaging)2985 GitClient::RevertResult GitClient::revertI(QStringList files,
2986 bool *ptrToIsDirectory,
2987 QString *errorMessage,
2988 bool revertStaging)
2989 {
2990 if (files.empty())
2991 return RevertCanceled;
2992
2993 // Figure out the working directory
2994 const QFileInfo firstFile(files.front());
2995 const bool isDirectory = firstFile.isDir();
2996 if (ptrToIsDirectory)
2997 *ptrToIsDirectory = isDirectory;
2998 const QString workingDirectory = isDirectory ? firstFile.absoluteFilePath() : firstFile.absolutePath();
2999
3000 const QString repoDirectory = VcsManager::findTopLevelForDirectory(workingDirectory);
3001 if (repoDirectory.isEmpty()) {
3002 *errorMessage = msgRepositoryNotFound(workingDirectory);
3003 return RevertFailed;
3004 }
3005
3006 // Check for changes
3007 QString output;
3008 switch (gitStatus(repoDirectory, StatusMode(NoUntracked | NoSubmodules), &output, errorMessage)) {
3009 case StatusChanged:
3010 break;
3011 case StatusUnchanged:
3012 return RevertUnchanged;
3013 case StatusFailed:
3014 return RevertFailed;
3015 }
3016 CommitData data;
3017 if (!data.parseFilesFromStatus(output)) {
3018 *errorMessage = msgParseFilesFailed();
3019 return RevertFailed;
3020 }
3021
3022 // If we are looking at files, make them relative to the repository
3023 // directory to match them in the status output list.
3024 if (!isDirectory) {
3025 const QDir repoDir(repoDirectory);
3026 const QStringList::iterator cend = files.end();
3027 for (QStringList::iterator it = files.begin(); it != cend; ++it)
3028 *it = repoDir.relativeFilePath(*it);
3029 }
3030
3031 // From the status output, determine all modified [un]staged files.
3032 const QStringList allStagedFiles = data.filterFiles(StagedFile | ModifiedFile);
3033 const QStringList allUnstagedFiles = data.filterFiles(ModifiedFile);
3034 // Unless a directory was passed, filter all modified files for the
3035 // argument file list.
3036 QStringList stagedFiles = allStagedFiles;
3037 QStringList unstagedFiles = allUnstagedFiles;
3038 if (!isDirectory) {
3039 const QSet<QString> filesSet = Utils::toSet(files);
3040 stagedFiles = Utils::toList(Utils::toSet(allStagedFiles).intersect(filesSet));
3041 unstagedFiles = Utils::toList(Utils::toSet(allUnstagedFiles).intersect(filesSet));
3042 }
3043 if ((!revertStaging || stagedFiles.empty()) && unstagedFiles.empty())
3044 return RevertUnchanged;
3045
3046 // Ask to revert (to do: Handle lists with a selection dialog)
3047 const QMessageBox::StandardButton answer
3048 = QMessageBox::question(ICore::dialogParent(),
3049 tr("Revert"),
3050 tr("The file has been changed. Do you want to revert it?"),
3051 QMessageBox::Yes | QMessageBox::No,
3052 QMessageBox::No);
3053 if (answer == QMessageBox::No)
3054 return RevertCanceled;
3055
3056 // Unstage the staged files
3057 if (revertStaging && !stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage))
3058 return RevertFailed;
3059 QStringList filesToRevert = unstagedFiles;
3060 if (revertStaging)
3061 filesToRevert += stagedFiles;
3062 // Finally revert!
3063 if (!synchronousCheckoutFiles(repoDirectory, filesToRevert, QString(), errorMessage, revertStaging))
3064 return RevertFailed;
3065 return RevertOk;
3066 }
3067
revert(const QStringList & files,bool revertStaging)3068 void GitClient::revert(const QStringList &files, bool revertStaging)
3069 {
3070 bool isDirectory;
3071 QString errorMessage;
3072 switch (revertI(files, &isDirectory, &errorMessage, revertStaging)) {
3073 case RevertOk:
3074 GitPlugin::emitFilesChanged(files);
3075 break;
3076 case RevertCanceled:
3077 break;
3078 case RevertUnchanged: {
3079 const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
3080 VcsOutputWindow::appendWarning(msg);
3081 }
3082 break;
3083 case RevertFailed:
3084 VcsOutputWindow::appendError(errorMessage);
3085 break;
3086 }
3087 }
3088
fetch(const QString & workingDirectory,const QString & remote)3089 void GitClient::fetch(const QString &workingDirectory, const QString &remote)
3090 {
3091 QStringList const arguments = {"fetch", (remote.isEmpty() ? "--all" : remote)};
3092 VcsCommand *command = vcsExec(workingDirectory, arguments, nullptr, true,
3093 VcsCommand::ShowSuccessMessage);
3094 connect(command, &VcsCommand::success,
3095 this, [workingDirectory] { GitPlugin::updateBranches(workingDirectory); });
3096 }
3097
executeAndHandleConflicts(const QString & workingDirectory,const QStringList & arguments,const QString & abortCommand) const3098 bool GitClient::executeAndHandleConflicts(const QString &workingDirectory,
3099 const QStringList &arguments,
3100 const QString &abortCommand) const
3101 {
3102 // Disable UNIX terminals to suppress SSH prompting.
3103 const unsigned flags = VcsCommand::SshPasswordPrompt
3104 | VcsCommand::ShowStdOut
3105 | VcsCommand::ExpectRepoChanges
3106 | VcsCommand::ShowSuccessMessage;
3107 QtcProcess proc;
3108 vcsSynchronousExec(proc, workingDirectory, arguments, flags);
3109 // Notify about changed files or abort the rebase.
3110 ConflictHandler::handleResponse(proc, workingDirectory, abortCommand);
3111 return proc.result() == QtcProcess::FinishedWithSuccess;
3112 }
3113
pull(const QString & workingDirectory,bool rebase)3114 void GitClient::pull(const QString &workingDirectory, bool rebase)
3115 {
3116 QString abortCommand;
3117 QStringList arguments = {"pull"};
3118 if (rebase) {
3119 arguments << "--rebase";
3120 abortCommand = "rebase";
3121 } else {
3122 abortCommand = "merge";
3123 }
3124
3125 VcsCommand *command = vcsExecAbortable(workingDirectory, arguments, rebase, abortCommand);
3126 connect(command, &VcsCommand::success, this,
3127 [this, workingDirectory] { updateSubmodulesIfNeeded(workingDirectory, true); },
3128 Qt::QueuedConnection);
3129 }
3130
synchronousAbortCommand(const QString & workingDir,const QString & abortCommand)3131 void GitClient::synchronousAbortCommand(const QString &workingDir, const QString &abortCommand)
3132 {
3133 // Abort to clean if something goes wrong
3134 if (abortCommand.isEmpty()) {
3135 // no abort command - checkout index to clean working copy.
3136 synchronousCheckoutFiles(VcsManager::findTopLevelForDirectory(workingDir),
3137 QStringList(), QString(), nullptr, false);
3138 return;
3139 }
3140
3141 QtcProcess proc;
3142 vcsFullySynchronousExec(proc, workingDir, {abortCommand, "--abort"},
3143 VcsCommand::ExpectRepoChanges | VcsCommand::ShowSuccessMessage);
3144 VcsOutputWindow::append(proc.stdOut());
3145 }
3146
synchronousTrackingBranch(const QString & workingDirectory,const QString & branch)3147 QString GitClient::synchronousTrackingBranch(const QString &workingDirectory, const QString &branch)
3148 {
3149 QString remote;
3150 QString localBranch = branch.isEmpty() ? synchronousCurrentLocalBranch(workingDirectory) : branch;
3151 if (localBranch.isEmpty())
3152 return QString();
3153 localBranch.prepend("branch.");
3154 remote = readConfigValue(workingDirectory, localBranch + ".remote");
3155 if (remote.isEmpty())
3156 return QString();
3157 const QString rBranch = readConfigValue(workingDirectory, localBranch + ".merge")
3158 .replace("refs/heads/", QString());
3159 if (rBranch.isEmpty())
3160 return QString();
3161 return remote + '/' + rBranch;
3162 }
3163
synchronousSetTrackingBranch(const QString & workingDirectory,const QString & branch,const QString & tracking)3164 bool GitClient::synchronousSetTrackingBranch(const QString &workingDirectory,
3165 const QString &branch, const QString &tracking)
3166 {
3167 QtcProcess proc;
3168 vcsFullySynchronousExec(proc,
3169 workingDirectory, {"branch", "--set-upstream-to=" + tracking, branch});
3170 return proc.result() == QtcProcess::FinishedWithSuccess;
3171 }
3172
asyncUpstreamStatus(const QString & workingDirectory,const QString & branch,const QString & upstream)3173 VcsBase::VcsCommand *GitClient::asyncUpstreamStatus(const QString &workingDirectory,
3174 const QString &branch,
3175 const QString &upstream)
3176 {
3177 const QStringList args {"rev-list", noColorOption, "--left-right", "--count",
3178 branch + "..." + upstream};
3179 return vcsExec(workingDirectory, args, nullptr, false, silentFlags);
3180 }
3181
handleMergeConflicts(const QString & workingDir,const QString & commit,const QStringList & files,const QString & abortCommand)3182 void GitClient::handleMergeConflicts(const QString &workingDir, const QString &commit,
3183 const QStringList &files, const QString &abortCommand)
3184 {
3185 QString message;
3186 if (!commit.isEmpty()) {
3187 message = tr("Conflicts detected with commit %1.").arg(commit);
3188 } else if (!files.isEmpty()) {
3189 QString fileList;
3190 QStringList partialFiles = files;
3191 while (partialFiles.count() > 20)
3192 partialFiles.removeLast();
3193 fileList = partialFiles.join('\n');
3194 if (partialFiles.count() != files.count())
3195 fileList += "\n...";
3196 message = tr("Conflicts detected with files:\n%1").arg(fileList);
3197 } else {
3198 message = tr("Conflicts detected.");
3199 }
3200 QMessageBox mergeOrAbort(QMessageBox::Question, tr("Conflicts Detected"), message,
3201 QMessageBox::NoButton, ICore::dialogParent());
3202 QPushButton *mergeToolButton = mergeOrAbort.addButton(tr("Run &Merge Tool"),
3203 QMessageBox::AcceptRole);
3204 const QString mergeTool = readConfigValue(workingDir, "merge.tool");
3205 if (mergeTool.isEmpty() || mergeTool.startsWith("vimdiff")) {
3206 mergeToolButton->setEnabled(false);
3207 mergeToolButton->setToolTip(tr("Only graphical merge tools are supported. "
3208 "Please configure merge.tool."));
3209 }
3210 mergeOrAbort.addButton(QMessageBox::Ignore);
3211 if (abortCommand == "rebase")
3212 mergeOrAbort.addButton(tr("&Skip"), QMessageBox::RejectRole);
3213 if (!abortCommand.isEmpty())
3214 mergeOrAbort.addButton(QMessageBox::Abort);
3215 switch (mergeOrAbort.exec()) {
3216 case QMessageBox::Abort:
3217 synchronousAbortCommand(workingDir, abortCommand);
3218 break;
3219 case QMessageBox::Ignore:
3220 break;
3221 default: // Merge or Skip
3222 if (mergeOrAbort.clickedButton() == mergeToolButton)
3223 merge(workingDir);
3224 else if (!abortCommand.isEmpty())
3225 executeAndHandleConflicts(workingDir, {abortCommand, "--skip"}, abortCommand);
3226 }
3227 }
3228
addFuture(const QFuture<void> & future)3229 void GitClient::addFuture(const QFuture<void> &future)
3230 {
3231 m_synchronizer.addFuture(future);
3232 }
3233
3234 // Subversion: git svn
synchronousSubversionFetch(const QString & workingDirectory) const3235 void GitClient::synchronousSubversionFetch(const QString &workingDirectory) const
3236 {
3237 // Disable UNIX terminals to suppress SSH prompting.
3238 const unsigned flags = VcsCommand::SshPasswordPrompt
3239 | VcsCommand::ShowStdOut
3240 | VcsCommand::ShowSuccessMessage;
3241 QtcProcess proc;
3242 vcsSynchronousExec(proc, workingDirectory, {"svn", "fetch"}, flags);
3243 }
3244
subversionLog(const QString & workingDirectory) const3245 void GitClient::subversionLog(const QString &workingDirectory) const
3246 {
3247 QStringList arguments = {"svn", "log"};
3248 int logCount = settings().logCount.value();
3249 if (logCount > 0)
3250 arguments << ("--limit=" + QString::number(logCount));
3251
3252 // Create a command editor, no highlighting or interaction.
3253 const QString title = tr("Git SVN Log");
3254 const Id editorId = Git::Constants::GIT_SVN_LOG_EDITOR_ID;
3255 const QString sourceFile = VcsBaseEditor::getSource(workingDirectory, QStringList());
3256 VcsBaseEditorWidget *editor = createVcsEditor(editorId, title, sourceFile, codecFor(CodecNone),
3257 "svnLog", sourceFile);
3258 editor->setWorkingDirectory(workingDirectory);
3259 vcsExec(workingDirectory, arguments, editor);
3260 }
3261
subversionDeltaCommit(const QString & workingDirectory) const3262 void GitClient::subversionDeltaCommit(const QString &workingDirectory) const
3263 {
3264 vcsExec(workingDirectory, {"svn", "dcommit"}, nullptr, true,
3265 VcsCommand::ShowSuccessMessage);
3266 }
3267
push(const QString & workingDirectory,const QStringList & pushArgs)3268 void GitClient::push(const QString &workingDirectory, const QStringList &pushArgs)
3269 {
3270 VcsCommand *command = vcsExec(
3271 workingDirectory, QStringList({"push"}) + pushArgs, nullptr, true,
3272 VcsCommand::ShowSuccessMessage);
3273 connect(command, &VcsCommand::stdErrText, this, [this, command](const QString &text) {
3274 if (text.contains("non-fast-forward"))
3275 command->setCookie(NonFastForward);
3276 else if (text.contains("has no upstream branch"))
3277 command->setCookie(NoRemoteBranch);
3278
3279 if (command->cookie().toInt() == NoRemoteBranch) {
3280 const QStringList lines = text.split('\n', Qt::SkipEmptyParts);
3281 for (const QString &line : lines) {
3282 /* Extract the suggested command from the git output which
3283 * should be similar to the following:
3284 *
3285 * git push --set-upstream origin add_set_upstream_dialog
3286 */
3287 const QString trimmedLine = line.trimmed();
3288 if (trimmedLine.startsWith("git push")) {
3289 m_pushFallbackCommand = trimmedLine;
3290 break;
3291 }
3292 }
3293 }
3294 });
3295 connect(command, &VcsCommand::finished,
3296 this, [this, command, workingDirectory, pushArgs](bool success) {
3297 if (!success) {
3298 switch (static_cast<PushFailure>(command->cookie().toInt())) {
3299 case Unknown:
3300 break;
3301 case NonFastForward: {
3302 const QColor warnColor = Utils::creatorTheme()->color(Theme::TextColorError);
3303 if (QMessageBox::question(
3304 Core::ICore::dialogParent(), tr("Force Push"),
3305 tr("Push failed. Would you like to force-push <span style=\"color:#%1\">"
3306 "(rewrites remote history)</span>?")
3307 .arg(QString::number(warnColor.rgba(), 16)),
3308 QMessageBox::Yes | QMessageBox::No,
3309 QMessageBox::No) == QMessageBox::Yes) {
3310 VcsCommand *rePushCommand = vcsExec(workingDirectory,
3311 QStringList({"push", "--force-with-lease"}) + pushArgs,
3312 nullptr, true, VcsCommand::ShowSuccessMessage);
3313 connect(rePushCommand, &VcsCommand::success,
3314 this, []() { GitPlugin::updateCurrentBranch(); });
3315 }
3316 break;
3317 }
3318 case NoRemoteBranch:
3319 if (QMessageBox::question(
3320 Core::ICore::dialogParent(), tr("No Upstream Branch"),
3321 tr("Push failed because the local branch \"%1\" "
3322 "does not have an upstream branch on the remote.\n\n"
3323 "Would you like to create the branch \"%1\" on the "
3324 "remote and set it as upstream?")
3325 .arg(synchronousCurrentLocalBranch(workingDirectory)),
3326 QMessageBox::Yes | QMessageBox::No,
3327 QMessageBox::No) == QMessageBox::Yes) {
3328
3329 const QStringList fallbackCommandParts =
3330 m_pushFallbackCommand.split(' ', Qt::SkipEmptyParts);
3331 VcsCommand *rePushCommand = vcsExec(workingDirectory,
3332 fallbackCommandParts.mid(1),
3333 nullptr, true, VcsCommand::ShowSuccessMessage);
3334 connect(rePushCommand, &VcsCommand::success, this, [workingDirectory]() {
3335 GitPlugin::updateBranches(workingDirectory);
3336 });
3337 }
3338 break;
3339 }
3340 } else {
3341 GitPlugin::updateCurrentBranch();
3342 }
3343 });
3344 }
3345
synchronousMerge(const QString & workingDirectory,const QString & branch,bool allowFastForward)3346 bool GitClient::synchronousMerge(const QString &workingDirectory, const QString &branch,
3347 bool allowFastForward)
3348 {
3349 QString command = "merge";
3350 QStringList arguments = {command};
3351 if (!allowFastForward)
3352 arguments << "--no-ff";
3353 arguments << branch;
3354 return executeAndHandleConflicts(workingDirectory, arguments, command);
3355 }
3356
canRebase(const QString & workingDirectory) const3357 bool GitClient::canRebase(const QString &workingDirectory) const
3358 {
3359 const QString gitDir = findGitDirForRepository(workingDirectory);
3360 if (QFileInfo::exists(gitDir + "/rebase-apply")
3361 || QFileInfo::exists(gitDir + "/rebase-merge")) {
3362 VcsOutputWindow::appendError(
3363 tr("Rebase, merge or am is in progress. Finish "
3364 "or abort it and then try again."));
3365 return false;
3366 }
3367 return true;
3368 }
3369
rebase(const QString & workingDirectory,const QString & argument)3370 void GitClient::rebase(const QString &workingDirectory, const QString &argument)
3371 {
3372 vcsExecAbortable(workingDirectory, {"rebase", argument}, true);
3373 }
3374
cherryPick(const QString & workingDirectory,const QString & argument)3375 void GitClient::cherryPick(const QString &workingDirectory, const QString &argument)
3376 {
3377 vcsExecAbortable(workingDirectory, {"cherry-pick", argument});
3378 }
3379
revert(const QString & workingDirectory,const QString & argument)3380 void GitClient::revert(const QString &workingDirectory, const QString &argument)
3381 {
3382 vcsExecAbortable(workingDirectory, {"revert", argument});
3383 }
3384
3385 // Executes a command asynchronously. Work tree is expected to be clean.
3386 // Stashing is handled prior to this call.
vcsExecAbortable(const QString & workingDirectory,const QStringList & arguments,bool isRebase,QString abortCommand)3387 VcsCommand *GitClient::vcsExecAbortable(const QString &workingDirectory,
3388 const QStringList &arguments,
3389 bool isRebase,
3390 QString abortCommand)
3391 {
3392 QTC_ASSERT(!arguments.isEmpty(), return nullptr);
3393
3394 if (abortCommand.isEmpty())
3395 abortCommand = arguments.at(0);
3396 VcsCommand *command = createCommand(workingDirectory, nullptr, VcsWindowOutputBind);
3397 command->setCookie(workingDirectory);
3398 command->addFlags(VcsCommand::SshPasswordPrompt
3399 | VcsCommand::ShowStdOut
3400 | VcsCommand::ShowSuccessMessage);
3401 // For rebase, Git might request an editor (which means the process keeps running until the
3402 // user closes it), so run without timeout.
3403 command->addJob({vcsBinary(), arguments}, isRebase ? 0 : command->defaultTimeoutS());
3404 ConflictHandler::attachToCommand(command, abortCommand);
3405 if (isRebase)
3406 GitProgressParser::attachToCommand(command);
3407 command->execute();
3408
3409 return command;
3410 }
3411
synchronousRevert(const QString & workingDirectory,const QString & commit)3412 bool GitClient::synchronousRevert(const QString &workingDirectory, const QString &commit)
3413 {
3414 const QString command = "revert";
3415 // Do not stash if --continue or --abort is given as the commit
3416 if (!commit.startsWith('-') && !beginStashScope(workingDirectory, command))
3417 return false;
3418 return executeAndHandleConflicts(workingDirectory, {command, "--no-edit", commit}, command);
3419 }
3420
synchronousCherryPick(const QString & workingDirectory,const QString & commit)3421 bool GitClient::synchronousCherryPick(const QString &workingDirectory, const QString &commit)
3422 {
3423 const QString command = "cherry-pick";
3424 // "commit" might be --continue or --abort
3425 const bool isRealCommit = !commit.startsWith('-');
3426 if (isRealCommit && !beginStashScope(workingDirectory, command))
3427 return false;
3428
3429 QStringList arguments = {command};
3430 if (isRealCommit && isRemoteCommit(workingDirectory, commit))
3431 arguments << "-x";
3432 arguments << commit;
3433
3434 return executeAndHandleConflicts(workingDirectory, arguments, command);
3435 }
3436
interactiveRebase(const QString & workingDirectory,const QString & commit,bool fixup)3437 void GitClient::interactiveRebase(const QString &workingDirectory, const QString &commit, bool fixup)
3438 {
3439 QStringList arguments = {"rebase", "-i"};
3440 if (fixup)
3441 arguments << "--autosquash";
3442 arguments << commit + '^';
3443 if (fixup)
3444 m_disableEditor = true;
3445 vcsExecAbortable(workingDirectory, arguments, true);
3446 if (fixup)
3447 m_disableEditor = false;
3448 }
3449
msgNoChangedFiles()3450 QString GitClient::msgNoChangedFiles()
3451 {
3452 return tr("There are no modified files.");
3453 }
3454
msgNoCommits(bool includeRemote)3455 QString GitClient::msgNoCommits(bool includeRemote)
3456 {
3457 return includeRemote ? tr("No commits were found") : tr("No local commits were found");
3458 }
3459
stashPop(const QString & workingDirectory,const QString & stash)3460 void GitClient::stashPop(const QString &workingDirectory, const QString &stash)
3461 {
3462 QStringList arguments = {"stash", "pop"};
3463 if (!stash.isEmpty())
3464 arguments << stash;
3465 VcsCommand *cmd = vcsExec(workingDirectory, arguments, nullptr, true, VcsCommand::ExpectRepoChanges);
3466 ConflictHandler::attachToCommand(cmd);
3467 }
3468
synchronousStashRestore(const QString & workingDirectory,const QString & stash,bool pop,const QString & branch) const3469 bool GitClient::synchronousStashRestore(const QString &workingDirectory,
3470 const QString &stash,
3471 bool pop,
3472 const QString &branch /* = QString()*/) const
3473 {
3474 QStringList arguments = {"stash"};
3475 if (branch.isEmpty())
3476 arguments << QLatin1String(pop ? "pop" : "apply") << stash;
3477 else
3478 arguments << "branch" << branch << stash;
3479 return executeAndHandleConflicts(workingDirectory, arguments);
3480 }
3481
synchronousStashRemove(const QString & workingDirectory,const QString & stash,QString * errorMessage) const3482 bool GitClient::synchronousStashRemove(const QString &workingDirectory, const QString &stash,
3483 QString *errorMessage) const
3484 {
3485 QStringList arguments = {"stash"};
3486 if (stash.isEmpty())
3487 arguments << "clear";
3488 else
3489 arguments << "drop" << stash;
3490
3491 QtcProcess proc;
3492 vcsFullySynchronousExec(proc, workingDirectory, arguments);
3493 if (proc.result() == QtcProcess::FinishedWithSuccess) {
3494 const QString output = proc.stdOut();
3495 if (!output.isEmpty())
3496 VcsOutputWindow::append(output);
3497 return true;
3498 } else {
3499 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
3500 return false;
3501 }
3502 }
3503
synchronousStashList(const QString & workingDirectory,QList<Stash> * stashes,QString * errorMessage) const3504 bool GitClient::synchronousStashList(const QString &workingDirectory, QList<Stash> *stashes,
3505 QString *errorMessage) const
3506 {
3507 stashes->clear();
3508
3509 const QStringList arguments = {"stash", "list", noColorOption};
3510 QtcProcess proc;
3511 vcsFullySynchronousExec(proc, workingDirectory, arguments, VcsCommand::ForceCLocale);
3512 if (proc.result() != QtcProcess::FinishedWithSuccess) {
3513 msgCannotRun(arguments, workingDirectory, proc.stdErr(), errorMessage);
3514 return false;
3515 }
3516 Stash stash;
3517 const QStringList lines = splitLines(proc.stdOut());
3518 for (const QString &line : lines) {
3519 if (stash.parseStashLine(line))
3520 stashes->push_back(stash);
3521 }
3522 return true;
3523 }
3524
3525 // Read a single-line config value, return trimmed
readConfigValue(const QString & workingDirectory,const QString & configVar) const3526 QString GitClient::readConfigValue(const QString &workingDirectory, const QString &configVar) const
3527 {
3528 return readOneLine(workingDirectory, {"config", configVar});
3529 }
3530
setConfigValue(const QString & workingDirectory,const QString & configVar,const QString & value) const3531 void GitClient::setConfigValue(const QString &workingDirectory, const QString &configVar,
3532 const QString &value) const
3533 {
3534 readOneLine(workingDirectory, {"config", configVar, value});
3535 }
3536
readGitVar(const QString & workingDirectory,const QString & configVar) const3537 QString GitClient::readGitVar(const QString &workingDirectory, const QString &configVar) const
3538 {
3539 return readOneLine(workingDirectory, {"var", configVar});
3540 }
3541
readOneLine(const QString & workingDirectory,const QStringList & arguments) const3542 QString GitClient::readOneLine(const QString &workingDirectory, const QStringList &arguments) const
3543 {
3544 // Git for Windows always uses UTF-8 for configuration:
3545 // https://github.com/msysgit/msysgit/wiki/Git-for-Windows-Unicode-Support#convert-config-files
3546 static QTextCodec *codec = HostOsInfo::isWindowsHost()
3547 ? QTextCodec::codecForName("UTF-8")
3548 : QTextCodec::codecForLocale();
3549
3550 QtcProcess proc;
3551 vcsFullySynchronousExec(proc, workingDirectory, arguments, silentFlags, vcsTimeoutS(), codec);
3552 if (proc.result() != QtcProcess::FinishedWithSuccess)
3553 return QString();
3554 return proc.stdOut().trimmed();
3555 }
3556
3557 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
gitVersion(QString * errorMessage) const3558 unsigned GitClient::gitVersion(QString *errorMessage) const
3559 {
3560 const FilePath newGitBinary = vcsBinary();
3561 if (m_gitVersionForBinary != newGitBinary && !newGitBinary.isEmpty()) {
3562 // Do not execute repeatedly if that fails (due to git
3563 // not being installed) until settings are changed.
3564 m_cachedGitVersion = synchronousGitVersion(errorMessage);
3565 m_gitVersionForBinary = newGitBinary;
3566 }
3567 return m_cachedGitVersion;
3568 }
3569
3570 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
synchronousGitVersion(QString * errorMessage) const3571 unsigned GitClient::synchronousGitVersion(QString *errorMessage) const
3572 {
3573 if (vcsBinary().isEmpty())
3574 return 0;
3575
3576 // run git --version
3577 QtcProcess proc;
3578 vcsSynchronousExec(proc, QString(), {"--version"}, silentFlags);
3579 if (proc.result() != QtcProcess::FinishedWithSuccess) {
3580 msgCannotRun(tr("Cannot determine Git version: %1").arg(proc.stdErr()), errorMessage);
3581 return 0;
3582 }
3583
3584 // cut 'git version 1.6.5.1.sha'
3585 // another form: 'git version 1.9.rc1'
3586 const QString output = proc.stdOut();
3587 const QRegularExpression versionPattern("^[^\\d]+(\\d+)\\.(\\d+)\\.(\\d+|rc\\d).*$");
3588 QTC_ASSERT(versionPattern.isValid(), return 0);
3589 const QRegularExpressionMatch match = versionPattern.match(output);
3590 QTC_ASSERT(match.hasMatch(), return 0);
3591 const unsigned majorV = match.captured(1).toUInt(nullptr, 16);
3592 const unsigned minorV = match.captured(2).toUInt(nullptr, 16);
3593 const unsigned patchV = match.captured(3).toUInt(nullptr, 16);
3594 return version(majorV, minorV, patchV);
3595 }
3596
init(const QString & workingDirectory,const QString & command,StashFlag flag,PushAction pushAction)3597 bool GitClient::StashInfo::init(const QString &workingDirectory, const QString &command,
3598 StashFlag flag, PushAction pushAction)
3599 {
3600 m_workingDir = workingDirectory;
3601 m_flags = flag;
3602 m_pushAction = pushAction;
3603 QString errorMessage;
3604 QString statusOutput;
3605 switch (m_instance->gitStatus(m_workingDir, StatusMode(NoUntracked | NoSubmodules),
3606 &statusOutput, &errorMessage)) {
3607 case GitClient::StatusChanged:
3608 if (m_flags & NoPrompt)
3609 executeStash(command, &errorMessage);
3610 else
3611 stashPrompt(command, statusOutput, &errorMessage);
3612 break;
3613 case GitClient::StatusUnchanged:
3614 m_stashResult = StashUnchanged;
3615 break;
3616 case GitClient::StatusFailed:
3617 m_stashResult = StashFailed;
3618 break;
3619 }
3620
3621 if (m_stashResult == StashFailed)
3622 VcsOutputWindow::appendError(errorMessage);
3623 return !stashingFailed();
3624 }
3625
stashPrompt(const QString & command,const QString & statusOutput,QString * errorMessage)3626 void GitClient::StashInfo::stashPrompt(const QString &command, const QString &statusOutput,
3627 QString *errorMessage)
3628 {
3629 QMessageBox msgBox(QMessageBox::Question, tr("Uncommitted Changes Found"),
3630 tr("What would you like to do with local changes in:") + "\n\n\""
3631 + QDir::toNativeSeparators(m_workingDir) + '\"',
3632 QMessageBox::NoButton, ICore::dialogParent());
3633
3634 msgBox.setDetailedText(statusOutput);
3635
3636 QPushButton *stashAndPopButton = msgBox.addButton(tr("Stash && Pop"), QMessageBox::AcceptRole);
3637 stashAndPopButton->setToolTip(tr("Stash local changes and pop when %1 finishes.").arg(command));
3638
3639 QPushButton *stashButton = msgBox.addButton(tr("Stash"), QMessageBox::AcceptRole);
3640 stashButton->setToolTip(tr("Stash local changes and execute %1.").arg(command));
3641
3642 QPushButton *discardButton = msgBox.addButton(tr("Discard"), QMessageBox::AcceptRole);
3643 discardButton->setToolTip(tr("Discard (reset) local changes and execute %1.").arg(command));
3644
3645 QPushButton *ignoreButton = nullptr;
3646 if (m_flags & AllowUnstashed) {
3647 ignoreButton = msgBox.addButton(QMessageBox::Ignore);
3648 ignoreButton->setToolTip(tr("Execute %1 with local changes in working directory.")
3649 .arg(command));
3650 }
3651
3652 QPushButton *cancelButton = msgBox.addButton(QMessageBox::Cancel);
3653 cancelButton->setToolTip(tr("Cancel %1.").arg(command));
3654
3655 msgBox.exec();
3656
3657 if (msgBox.clickedButton() == discardButton) {
3658 m_stashResult = m_instance->synchronousReset(m_workingDir, QStringList(), errorMessage) ?
3659 StashUnchanged : StashFailed;
3660 } else if (msgBox.clickedButton() == ignoreButton) { // At your own risk, so.
3661 m_stashResult = NotStashed;
3662 } else if (msgBox.clickedButton() == cancelButton) {
3663 m_stashResult = StashCanceled;
3664 } else if (msgBox.clickedButton() == stashButton) {
3665 const bool result = m_instance->executeSynchronousStash(
3666 m_workingDir, creatorStashMessage(command), false, errorMessage);
3667 m_stashResult = result ? StashUnchanged : StashFailed;
3668 } else if (msgBox.clickedButton() == stashAndPopButton) {
3669 executeStash(command, errorMessage);
3670 }
3671 }
3672
executeStash(const QString & command,QString * errorMessage)3673 void GitClient::StashInfo::executeStash(const QString &command, QString *errorMessage)
3674 {
3675 m_message = creatorStashMessage(command);
3676 if (!m_instance->executeSynchronousStash(m_workingDir, m_message, false, errorMessage))
3677 m_stashResult = StashFailed;
3678 else
3679 m_stashResult = Stashed;
3680 }
3681
stashingFailed() const3682 bool GitClient::StashInfo::stashingFailed() const
3683 {
3684 switch (m_stashResult) {
3685 case StashCanceled:
3686 case StashFailed:
3687 return true;
3688 case NotStashed:
3689 return !(m_flags & AllowUnstashed);
3690 default:
3691 return false;
3692 }
3693 }
3694
end()3695 void GitClient::StashInfo::end()
3696 {
3697 if (m_stashResult == Stashed) {
3698 QString stashName;
3699 if (m_instance->stashNameFromMessage(m_workingDir, m_message, &stashName))
3700 m_instance->stashPop(m_workingDir, stashName);
3701 }
3702
3703 if (m_pushAction == NormalPush)
3704 m_instance->push(m_workingDir);
3705 else if (m_pushAction == PushToGerrit)
3706 GitPlugin::gerritPush(m_workingDir);
3707
3708 m_pushAction = NoPush;
3709 m_stashResult = NotStashed;
3710 }
3711
GitRemote(const QString & location)3712 GitRemote::GitRemote(const QString &location) : Core::IVersionControl::RepoUrl(location)
3713 {
3714 if (isValid && protocol == "file")
3715 isValid = QDir(path).exists() || QDir(path + ".git").exists();
3716 }
3717
suggestedLocalBranchName(const QString & workingDirectory,const QStringList & localNames,const QString & target,BranchTargetType targetType)3718 QString GitClient::suggestedLocalBranchName(
3719 const QString &workingDirectory,
3720 const QStringList &localNames,
3721 const QString &target,
3722 BranchTargetType targetType)
3723 {
3724 QString initialName;
3725 if (targetType == BranchTargetType::Remote) {
3726 initialName = target.mid(target.lastIndexOf('/') + 1);
3727 } else {
3728 QString subject;
3729 instance()->synchronousLog(workingDirectory, {"-n", "1", "--format=%s", target},
3730 &subject, nullptr, VcsCommand::NoOutput);
3731 initialName = subject.trimmed();
3732 }
3733 QString suggestedName = initialName;
3734 int i = 2;
3735 while (localNames.contains(suggestedName)) {
3736 suggestedName = initialName + QString::number(i);
3737 ++i;
3738 }
3739
3740 return suggestedName;
3741 }
3742
addChangeActions(QMenu * menu,const QString & source,const QString & change)3743 void GitClient::addChangeActions(QMenu *menu, const QString &source, const QString &change)
3744 {
3745 QTC_ASSERT(!change.isEmpty(), return);
3746 const QString &workingDir = fileWorkingDirectory(source);
3747 menu->addAction(tr("Cherr&y-Pick Change %1").arg(change), [workingDir, change] {
3748 m_instance->synchronousCherryPick(workingDir, change);
3749 });
3750 menu->addAction(tr("Re&vert Change %1").arg(change), [workingDir, change] {
3751 m_instance->synchronousRevert(workingDir, change);
3752 });
3753 menu->addAction(tr("C&heckout Change %1").arg(change), [workingDir, change] {
3754 m_instance->checkout(workingDir, change);
3755 });
3756 connect(menu->addAction(tr("&Interactive Rebase from Change %1...").arg(change)),
3757 &QAction::triggered, [workingDir, change] {
3758 GitPlugin::startRebaseFromCommit(workingDir, change);
3759 });
3760 QAction *logAction = menu->addAction(tr("&Log for Change %1").arg(change), [workingDir, change] {
3761 m_instance->log(workingDir, QString(), false, {change});
3762 });
3763 const FilePath filePath = FilePath::fromString(source);
3764 if (!filePath.isDir()) {
3765 menu->addAction(tr("Sh&ow file \"%1\" on revision %2").arg(filePath.fileName()).arg(change),
3766 [workingDir, change, source] {
3767 m_instance->openShowEditor(workingDir, change, source);
3768 });
3769 }
3770 if (change.contains(".."))
3771 menu->setDefaultAction(logAction);
3772 menu->addAction(tr("Add &Tag for Change %1...").arg(change), [workingDir, change] {
3773 QString output;
3774 QString errorMessage;
3775 m_instance->synchronousTagCmd(workingDir, QStringList(),
3776 &output, &errorMessage);
3777
3778 const QStringList tags = output.split('\n');
3779 BranchAddDialog dialog(tags, BranchAddDialog::Type::AddTag, Core::ICore::dialogParent());
3780
3781 if (dialog.exec() == QDialog::Rejected)
3782 return;
3783
3784 m_instance->synchronousTagCmd(workingDir,
3785 {dialog.branchName(), change},
3786 &output, &errorMessage);
3787 VcsOutputWindow::append(output);
3788 if (!errorMessage.isEmpty())
3789 VcsOutputWindow::append(errorMessage, VcsOutputWindow::MessageStyle::Error);
3790 });
3791
3792 auto resetChange = [workingDir, change](const QByteArray &resetType) {
3793 m_instance->reset(
3794 workingDir, QLatin1String("--" + resetType), change);
3795 };
3796 auto resetMenu = new QMenu(tr("&Reset to Change %1").arg(change), menu);
3797 resetMenu->addAction(tr("&Hard"), std::bind(resetChange, "hard"));
3798 resetMenu->addAction(tr("&Mixed"), std::bind(resetChange, "mixed"));
3799 resetMenu->addAction(tr("&Soft"), std::bind(resetChange, "soft"));
3800 menu->addMenu(resetMenu);
3801
3802 menu->addAction(tr("Di&ff Against %1").arg(change),
3803 [workingDir, change] {
3804 m_instance->diffRepository(workingDir, change, {});
3805 });
3806 if (!m_instance->m_diffCommit.isEmpty()) {
3807 menu->addAction(tr("Diff &Against Saved %1").arg(m_instance->m_diffCommit),
3808 [workingDir, change] {
3809 m_instance->diffRepository(workingDir, m_instance->m_diffCommit, change);
3810 m_instance->m_diffCommit.clear();
3811 });
3812 }
3813 menu->addAction(tr("&Save for Diff"), [change] {
3814 m_instance->m_diffCommit = change;
3815 });
3816 }
3817
fileWorkingDirectory(const QString & file)3818 QString GitClient::fileWorkingDirectory(const QString &file)
3819 {
3820 Utils::FilePath path = Utils::FilePath::fromString(file);
3821 if (!path.isEmpty() && !path.isDir())
3822 path = path.parentDir();
3823 while (!path.isEmpty() && !path.exists())
3824 path = path.parentDir();
3825 return path.toString();
3826 }
3827
openShowEditor(const QString & workingDirectory,const QString & ref,const QString & path,ShowEditor showSetting)3828 IEditor *GitClient::openShowEditor(const QString &workingDirectory, const QString &ref,
3829 const QString &path, ShowEditor showSetting)
3830 {
3831 QString topLevel;
3832 VcsManager::findVersionControlForDirectory(workingDirectory, &topLevel);
3833 const QString relativePath = QDir(topLevel).relativeFilePath(path);
3834 const QByteArray content = synchronousShow(topLevel, ref + ":" + relativePath);
3835 if (showSetting == ShowEditor::OnlyIfDifferent) {
3836 if (content.isEmpty())
3837 return nullptr;
3838 QByteArray fileContent;
3839 if (TextFileFormat::readFileUTF8(Utils::FilePath::fromString(path),
3840 nullptr,
3841 &fileContent,
3842 nullptr)
3843 == TextFileFormat::ReadSuccess) {
3844 if (fileContent == content)
3845 return nullptr; // open the file for read/write
3846 }
3847 }
3848
3849 const QString documentId = QLatin1String(Git::Constants::GIT_PLUGIN)
3850 + QLatin1String(".GitShow.") + topLevel
3851 + QLatin1String(".") + relativePath;
3852 QString title = tr("Git Show %1:%2").arg(ref).arg(relativePath);
3853 IEditor *editor = EditorManager::openEditorWithContents(Id(), &title, content, documentId,
3854 EditorManager::DoNotSwitchToDesignMode);
3855 editor->document()->setTemporary(true);
3856 VcsBase::setSource(editor->document(), path);
3857 return editor;
3858 }
3859
3860 } // namespace Internal
3861 } // namespace Git
3862
3863 #include "gitclient.moc"
3864