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