1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (info@qt.nokia.com)
10 ** GNU Lesser General Public License Usage
12 ** This file may be used under the terms of the GNU Lesser General Public
13 ** License version 2.1 as published by the Free Software Foundation and
14 ** appearing in the file LICENSE.LGPL included in the packaging of this file.
15 ** Please review the following information to ensure the GNU Lesser General
16 ** Public License version 2.1 requirements will be met:
17 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
19 ** In addition, as a special exception, Nokia gives you certain additional
20 ** rights. These rights are described in the Nokia Qt LGPL Exception
21 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
25 ** Alternatively, this file may be used in accordance with the terms and
26 ** conditions contained in a signed written agreement between you and Nokia.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
31 **************************************************************************/
33 #include "gitclient.h"
34 #include "gitcommand.h"
37 #include "commitdata.h"
38 #include "gitconstants.h"
39 #include "gitplugin.h"
40 #include "gitsubmiteditor.h"
41 #include "gitversioncontrol.h"
43 #include <coreplugin/actionmanager/actionmanager.h>
44 #include <coreplugin/coreconstants.h>
45 #include <coreplugin/editormanager/editormanager.h>
46 #include <coreplugin/icore.h>
47 #include <coreplugin/messagemanager.h>
48 #include <coreplugin/progressmanager/progressmanager.h>
49 #include <coreplugin/uniqueidmanager.h>
50 #include <coreplugin/filemanager.h>
51 #include <coreplugin/iversioncontrol.h>
53 #include <texteditor/itexteditor.h>
54 #include <utils/qtcassert.h>
55 #include <utils/qtcprocess.h>
56 #include <utils/synchronousprocess.h>
57 #include <utils/environment.h>
58 #include <vcsbase/vcsbaseeditor.h>
59 #include <vcsbase/vcsbaseoutputwindow.h>
60 #include <vcsbase/vcsbaseplugin.h>
62 #include <QtCore/QRegExp>
63 #include <QtCore/QTemporaryFile>
64 #include <QtCore/QTime>
65 #include <QtCore/QFileInfo>
66 #include <QtCore/QDir>
67 #include <QtCore/QSignalMapper>
69 #include <QtGui/QComboBox>
70 #include <QtGui/QMainWindow> // for msg box parent
71 #include <QtGui/QMessageBox>
72 #include <QtGui/QToolButton>
74 static const char *const kGitDirectoryC = ".git";
75 static const char *const kBranchIndicatorC = "# On branch";
80 BaseGitArgumentsWidget::BaseGitArgumentsWidget(GitSettings *settings,
81 Git::Internal::GitClient *client,
82 const QString &directory,
83 const QStringList &args) :
86 m_workingDirectory(directory),
94 class BaseGitDiffArgumentsWidget : public Git::Internal::BaseGitArgumentsWidget
98 BaseGitDiffArgumentsWidget(Git::Internal::GitSettings *settings,
99 Git::Internal::GitClient *client,
100 const QString &directory,
101 const QStringList &args) :
102 BaseGitArgumentsWidget(settings, client, directory, args),
103 m_patience(new QToolButton),
104 m_ignoreSpaces(new QToolButton)
106 QHBoxLayout *layout = new QHBoxLayout(this);
107 layout->setContentsMargins(3, 0, 3, 0);
108 layout->setSpacing(2);
110 m_patience->setToolTip(tr("Use the patience algorithm for calculating the diff"));
111 m_patience->setText(tr("Patience"));
112 layout->addWidget(m_patience);
113 m_patience->setCheckable(true);
114 m_patience->setChecked(m_settings->diffPatience);
115 connect(m_patience, SIGNAL(toggled(bool)), this, SLOT(testForArgumentsChanged()));
117 m_ignoreSpaces->setToolTip(tr("Ignore whitespace only changes"));
118 m_ignoreSpaces->setText(tr("Ignore Whitespace"));
119 layout->addWidget(m_ignoreSpaces);
120 m_ignoreSpaces->setCheckable(true);
121 m_ignoreSpaces->setChecked(m_settings->ignoreSpaceChangesInDiff);
122 connect(m_ignoreSpaces, SIGNAL(toggled(bool)), this, SLOT(testForArgumentsChanged()));
125 QStringList arguments() const
128 foreach (const QString &arg, m_diffArgs) {
129 if (arg == QLatin1String("--patience")
130 || arg == QLatin1String("--ignore-space-change"))
135 if (m_patience->isChecked() && m_patience->isVisible())
136 args.prepend(QLatin1String("--patience"));
137 if (m_ignoreSpaces->isChecked() && m_ignoreSpaces->isVisible())
138 args.prepend(QLatin1String("--ignore-space-change"));
143 void testForArgumentsChanged() {
144 m_settings->diffPatience = m_patience->isChecked();
145 m_settings->ignoreSpaceChangesInDiff = m_ignoreSpaces->isChecked();
147 QStringList newArguments = arguments();
149 if (newArguments == m_diffArgs)
152 m_diffArgs = newArguments;
157 QToolButton *m_patience;
158 QToolButton *m_ignoreSpaces;
161 class GitCommitDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
165 GitCommitDiffArgumentsWidget(Git::Internal::GitSettings *settings,
166 Git::Internal::GitClient *client, const QString &directory,
167 const QStringList &args, const QStringList &unstaged,
168 const QStringList &staged) :
169 BaseGitDiffArgumentsWidget(settings, client, directory, args),
170 m_unstagedFileNames(unstaged),
171 m_stagedFileNames(staged)
176 m_client->diff(m_workingDirectory, m_diffArgs, m_unstagedFileNames, m_stagedFileNames);
180 const QStringList m_unstagedFileNames;
181 const QStringList m_stagedFileNames;
184 class GitFileDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
188 GitFileDiffArgumentsWidget(Git::Internal::GitSettings *settings,
189 Git::Internal::GitClient *client, const QString &directory,
190 const QStringList &args, const QString &file) :
191 BaseGitDiffArgumentsWidget(settings, client, directory, args),
197 m_client->diff(m_workingDirectory, m_diffArgs, m_fileName);
201 const QString m_fileName;
204 class GitBranchDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
208 GitBranchDiffArgumentsWidget(Git::Internal::GitSettings *settings,
209 Git::Internal::GitClient *client, const QString &directory,
210 const QStringList &args, const QString &branch) :
211 BaseGitDiffArgumentsWidget(settings, client, directory, args),
217 m_client->diffBranch(m_workingDirectory, m_diffArgs, m_branchName);
221 const QString m_branchName;
224 class GitShowArgumentsWidget : public Git::Internal::BaseGitArgumentsWidget
228 GitShowArgumentsWidget(Git::Internal::GitSettings *settings,
229 Git::Internal::GitClient *client,
230 const QString &directory,
231 const QStringList &args,
233 BaseGitArgumentsWidget(settings, client, directory, args),
234 m_prettyFormat(new QComboBox),
237 QHBoxLayout *layout = new QHBoxLayout(this);
238 layout->setContentsMargins(3, 0, 3, 0);
239 layout->setSpacing(2);
241 m_prettyFormat->setToolTip(tr("Select the pretty printing format"));
242 m_prettyFormat->addItem(tr("oneline"), QLatin1String("oneline"));
243 m_prettyFormat->addItem(tr("short"), QLatin1String("short"));
244 m_prettyFormat->addItem(tr("medium"), QLatin1String("medium"));
245 m_prettyFormat->addItem(tr("full"), QLatin1String("full"));
246 m_prettyFormat->addItem(tr("fuller"), QLatin1String("fuller"));
247 m_prettyFormat->addItem(tr("email"), QLatin1String("email"));
248 m_prettyFormat->addItem(tr("raw"), QLatin1String("raw"));
249 layout->addWidget(m_prettyFormat);
250 m_prettyFormat->setCurrentIndex(m_settings->showPrettyFormat);
251 m_prettyFormat->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
252 connect(m_prettyFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(testForArgumentsChanged()));
255 QStringList arguments() const
258 foreach (const QString &arg, m_diffArgs) {
259 if (arg.startsWith(QLatin1String("--pretty=")) || arg.startsWith(QLatin1String("--format=")))
264 args.prepend(QString::fromLatin1("--pretty=")
265 + m_prettyFormat->itemData(m_prettyFormat->currentIndex()).toString());
270 void testForArgumentsChanged() {
271 m_settings->showPrettyFormat = m_prettyFormat->currentIndex();
273 QStringList newArguments = arguments();
275 if (newArguments == m_diffArgs)
278 m_diffArgs = newArguments;
284 m_client->show(m_workingDirectory, m_id, m_diffArgs);
288 QComboBox *m_prettyFormat;
292 class GitBlameArgumentsWidget : public Git::Internal::BaseGitArgumentsWidget
296 GitBlameArgumentsWidget(Git::Internal::GitSettings *settings,
297 Git::Internal::GitClient *client, const QString &directory,
298 const QStringList &args, const QString &revision,
299 const QString &fileName) :
300 Git::Internal::BaseGitArgumentsWidget(settings, client, directory, args),
304 m_revision(revision),
307 QHBoxLayout *layout = new QHBoxLayout(this);
308 layout->setContentsMargins(3, 0, 3, 0);
309 layout->setSpacing(2);
311 m_omitDate = new QToolButton;
312 m_omitDate->setToolTip(tr("Do not show the date a change was made in the output"));
313 m_omitDate->setText(tr("Omit Date"));
314 layout->addWidget(m_omitDate);
315 m_omitDate->setCheckable(true);
316 m_omitDate->setChecked(m_settings->omitAnnotationDate);
317 m_omitDate->setMinimumHeight(16);
318 m_omitDate->setMaximumHeight(16);
319 connect(m_omitDate, SIGNAL(toggled(bool)), this, SLOT(testForArgumentsChanged()));
321 m_ignoreSpaces = new QToolButton;
322 m_ignoreSpaces->setToolTip(tr("Ignore whitespace only changes"));
323 m_ignoreSpaces->setText(tr("Ignore Whitespace"));
324 layout->addWidget(m_ignoreSpaces);
325 m_ignoreSpaces->setCheckable(true);
326 m_ignoreSpaces->setChecked(m_settings->ignoreSpaceChangesInBlame);
327 m_ignoreSpaces->setMinimumHeight(16);
328 m_ignoreSpaces->setMaximumHeight(16);
329 connect(m_ignoreSpaces, SIGNAL(toggled(bool)), this, SLOT(testForArgumentsChanged()));
332 void setEditor(VCSBase::VCSBaseEditorWidget *editor)
338 QStringList arguments() const
340 QStringList args = m_diffArgs;
342 args.removeAll(QLatin1String("-w"));
344 if (m_ignoreSpaces->isChecked())
345 args.prepend(QLatin1String("-w"));
350 void testForArgumentsChanged() {
351 m_settings->omitAnnotationDate = m_omitDate->isChecked();
352 m_settings->ignoreSpaceChangesInBlame = m_ignoreSpaces->isChecked();
354 m_diffArgs = arguments();
355 redoCommand(); // always redo for omit date
360 m_client->blame(m_workingDirectory, m_diffArgs, m_fileName,
361 m_revision, m_editor->lineNumberOfCurrentEditor());
365 QToolButton *m_omitDate;
366 QToolButton *m_ignoreSpaces;
367 VCSBase::VCSBaseEditorWidget *m_editor;
372 inline Core::IEditor* locateEditor(const Core::ICore *core, const char *property, const QString &entry)
374 foreach (Core::IEditor *ed, core->editorManager()->openedEditors())
375 if (ed->file()->property(property).toString() == entry)
380 // Return converted command output, remove '\r' read on Windows
381 static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
383 QString output = QString::fromLocal8Bit(a);
384 output.remove(QLatin1Char('\r'));
388 // Return converted command output split into lines
389 static inline QStringList commandOutputLinesFromLocal8Bit(const QByteArray &a)
391 QString output = commandOutputFromLocal8Bit(a);
392 const QChar newLine = QLatin1Char('\n');
393 if (output.endsWith(newLine))
394 output.truncate(output.size() - 1);
395 if (output.isEmpty())
396 return QStringList();
397 return output.split(newLine);
400 static inline VCSBase::VCSBaseOutputWindow *outputWindow()
402 return VCSBase::VCSBaseOutputWindow::instance();
405 static inline QString msgRepositoryNotFound(const QString &dir)
407 return GitClient::tr("Unable to determine the repository for %1.").arg(dir);
410 static inline QString msgParseFilesFailed()
412 return GitClient::tr("Unable to parse the file output.");
415 // ---------------- GitClient
417 const char *GitClient::stashNamePrefix = "stash@{";
419 GitClient::GitClient(GitPlugin* plugin)
420 : m_msgWait(tr("Waiting for data...")),
422 m_core(Core::ICore::instance()),
423 m_repositoryChangedSignalMapper(0),
424 m_cachedGitVersion(0),
425 m_hasCachedGitVersion(false)
427 if (QSettings *s = m_core->settings()) {
428 m_settings.fromSettings(s);
429 m_binaryPath = m_settings.gitBinaryPath();
433 GitClient::~GitClient()
437 const char *GitClient::noColorOption = "--no-color";
439 QString GitClient::findRepositoryForDirectory(const QString &dir)
441 // Check for ".git/config"
442 const QString checkFile = QLatin1String(kGitDirectoryC) + QLatin1String("/config");
443 return VCSBase::VCSBasePlugin::findRepositoryForDirectory(dir, checkFile);
446 VCSBase::VCSBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
447 const QString &dynamicPropertyValue) const
449 VCSBase::VCSBaseEditorWidget *rc = 0;
450 Core::IEditor *outputEditor = locateEditor(m_core, registerDynamicProperty, dynamicPropertyValue);
455 Core::EditorManager::instance()->activateEditor(outputEditor, Core::EditorManager::ModeSwitch);
456 outputEditor->createNew(m_msgWait);
457 rc = VCSBase::VCSBaseEditorWidget::getVcsBaseEditor(outputEditor);
463 /* Create an editor associated to VCS output of a source file/directory
464 * (using the file's codec). Makes use of a dynamic property to find an
465 * existing instance and to reuse it (in case, say, 'git diff foo' is
467 VCSBase::VCSBaseEditorWidget *GitClient::createVCSEditor(const QString &id,
469 // Source file or directory
470 const QString &source,
472 // Dynamic property and value to identify that editor
473 const char *registerDynamicProperty,
474 const QString &dynamicPropertyValue,
475 QWidget *configWidget) const
477 VCSBase::VCSBaseEditorWidget *rc = 0;
478 Q_ASSERT(!findExistingVCSEditor(registerDynamicProperty, dynamicPropertyValue));
480 // Create new, set wait message, set up with source and codec
481 Core::IEditor *outputEditor = m_core->editorManager()->openEditorWithContents(id, &title, m_msgWait);
482 outputEditor->file()->setProperty(registerDynamicProperty, dynamicPropertyValue);
483 rc = VCSBase::VCSBaseEditorWidget::getVcsBaseEditor(outputEditor);
484 connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,int)),
485 this, SLOT(slotBlameRevisionRequested(QString,QString,int)));
486 QTC_ASSERT(rc, return 0);
487 rc->setSource(source);
489 rc->setCodec(VCSBase::VCSBaseEditorWidget::getCodec(source));
491 m_core->editorManager()->activateEditor(outputEditor, Core::EditorManager::ModeSwitch);
492 rc->setForceReadOnly(true);
495 rc->setConfigurationWidget(configWidget);
500 void GitClient::diff(const QString &workingDirectory,
501 const QStringList &diffArgs,
502 const QStringList &unstagedFileNames,
503 const QStringList &stagedFileNames)
506 if (Git::Constants::debug)
507 qDebug() << "diff" << workingDirectory << unstagedFileNames << stagedFileNames;
509 const QString binary = QLatin1String(Constants::GIT_BINARY);
510 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
511 const QString title = tr("Git Diff");
513 QStringList userDiffArgs = diffArgs;
514 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", workingDirectory);
516 GitCommitDiffArgumentsWidget *argWidget =
517 new GitCommitDiffArgumentsWidget(&m_settings, this, workingDirectory, diffArgs,
518 unstagedFileNames, stagedFileNames);
519 userDiffArgs = argWidget->arguments();
521 editor = createVCSEditor(editorId, title,
522 workingDirectory, true, "originalFileName", workingDirectory, argWidget);
524 editor->setDiffBaseDirectory(workingDirectory);
526 // Create a batch of 2 commands to be run after each other in case
527 // we have a mixture of staged/unstaged files as is the case
528 // when using the submit dialog.
529 GitCommand *command = createCommand(workingDirectory, editor);
533 cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption);
535 if (unstagedFileNames.empty() && stagedFileNames.empty()) {
536 QStringList arguments(cmdArgs);
537 arguments << userDiffArgs;
538 outputWindow()->appendCommand(workingDirectory, binary, arguments);
539 command->addJob(arguments, m_settings.timeoutSeconds);
542 if (!unstagedFileNames.empty()) {
543 QStringList arguments(cmdArgs);
544 arguments << userDiffArgs;
545 arguments << QLatin1String("--") << unstagedFileNames;
546 outputWindow()->appendCommand(workingDirectory, binary, arguments);
547 command->addJob(arguments, m_settings.timeoutSeconds);
549 if (!stagedFileNames.empty()) {
550 QStringList arguments(cmdArgs);
551 arguments << userDiffArgs;
552 arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
553 outputWindow()->appendCommand(workingDirectory, binary, arguments);
554 command->addJob(arguments, m_settings.timeoutSeconds);
560 void GitClient::diff(const QString &workingDirectory,
561 const QStringList &diffArgs,
562 const QString &fileName)
564 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
565 const QString title = tr("Git Diff %1").arg(fileName);
566 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, fileName);
568 QStringList userDiffArgs = diffArgs;
569 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", sourceFile);
571 GitFileDiffArgumentsWidget *argWidget =
572 new GitFileDiffArgumentsWidget(&m_settings, this, workingDirectory,
574 userDiffArgs = argWidget->arguments();
576 editor = createVCSEditor(editorId, title, sourceFile, true, "originalFileName", sourceFile, argWidget);
580 cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
583 if (!fileName.isEmpty())
584 cmdArgs << QLatin1String("--") << fileName;
585 executeGit(workingDirectory, cmdArgs, editor);
588 void GitClient::diffBranch(const QString &workingDirectory,
589 const QStringList &diffArgs,
590 const QString &branchName)
592 if (Git::Constants::debug)
593 qDebug() << "diffBranch" << workingDirectory << branchName;
595 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
596 const QString title = tr("Git Diff Branch %1").arg(branchName);
597 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, QStringList());
599 QStringList userDiffArgs = diffArgs;
600 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("BranchName", branchName);
602 GitBranchDiffArgumentsWidget *argWidget =
603 new GitBranchDiffArgumentsWidget(&m_settings, this, workingDirectory,
604 diffArgs, branchName);
605 userDiffArgs = argWidget->arguments();
607 editor = createVCSEditor(editorId, title, sourceFile, true, "BranchName", branchName, argWidget);
611 cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
612 << diffArgs << branchName;
614 executeGit(workingDirectory, cmdArgs, editor);
617 void GitClient::status(const QString &workingDirectory)
619 // @TODO: Use "--no-color" once it is supported
620 QStringList statusArgs(QLatin1String("status"));
621 statusArgs << QLatin1String("-u");
622 VCSBase::VCSBaseOutputWindow *outwin = outputWindow();
623 outwin->setRepository(workingDirectory);
624 GitCommand *command = executeGit(workingDirectory, statusArgs, 0, true);
625 connect(command, SIGNAL(finished(bool,int,QVariant)), outwin, SLOT(clearRepository()),
626 Qt::QueuedConnection);
629 static const char graphLogFormatC[] = "%h %an %s %ci";
631 // Create a graphical log.
632 void GitClient::graphLog(const QString &workingDirectory, const QString & branch)
634 if (Git::Constants::debug)
635 qDebug() << "log" << workingDirectory;
637 QStringList arguments;
638 arguments << QLatin1String("log") << QLatin1String(noColorOption);
640 if (m_settings.logCount > 0)
641 arguments << QLatin1String("-n") << QString::number(m_settings.logCount);
642 arguments << (QLatin1String("--pretty=format:") + QLatin1String(graphLogFormatC))
643 << QLatin1String("--topo-order") << QLatin1String("--graph");
646 if (branch.isEmpty()) {
647 title = tr("Git Log");
649 title = tr("Git Log %1").arg(branch);
652 const QString editorId = QLatin1String(Git::Constants::GIT_LOG_EDITOR_ID);
653 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, QStringList());
654 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
656 editor = createVCSEditor(editorId, title, sourceFile, false, "logFileName", sourceFile, 0);
657 executeGit(workingDirectory, arguments, editor);
660 void GitClient::log(const QString &workingDirectory, const QStringList &fileNames, bool enableAnnotationContextMenu)
662 if (Git::Constants::debug)
663 qDebug() << "log" << workingDirectory << fileNames;
665 QStringList arguments;
666 arguments << QLatin1String("log") << QLatin1String(noColorOption);
668 if (m_settings.logCount > 0)
669 arguments << QLatin1String("-n") << QString::number(m_settings.logCount);
671 if (!fileNames.isEmpty())
672 arguments.append(fileNames);
674 const QString msgArg = fileNames.empty() ? workingDirectory :
675 fileNames.join(QString(", "));
676 const QString title = tr("Git Log %1").arg(msgArg);
677 const QString editorId = QLatin1String(Git::Constants::GIT_LOG_EDITOR_ID);
678 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, fileNames);
679 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
681 editor = createVCSEditor(editorId, title, sourceFile, false, "logFileName", sourceFile, 0);
682 editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
683 executeGit(workingDirectory, arguments, editor);
686 // Do not show "0000" or "^32ae4"
687 static inline bool canShow(const QString &sha)
689 if (sha.startsWith(QLatin1Char('^')))
691 if (sha.count(QLatin1Char('0')) == sha.size())
696 static inline QString msgCannotShow(const QString &sha)
698 return GitClient::tr("Cannot describe '%1'.").arg(sha);
701 void GitClient::show(const QString &source, const QString &id, const QStringList &args)
703 if (Git::Constants::debug)
704 qDebug() << "show" << source << id;
706 outputWindow()->append(msgCannotShow(id));
710 QStringList userArgs = args;
711 const QString title = tr("Git Show %1").arg(id);
712 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
713 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("show", id);
715 GitShowArgumentsWidget *argWidget =
716 new GitShowArgumentsWidget(&m_settings, this, source,
718 userArgs = argWidget->arguments();
719 editor = createVCSEditor(editorId, title, source, true, "show", id, argWidget);
722 QStringList arguments;
723 arguments << QLatin1String("show") << QLatin1String(noColorOption);
724 arguments.append(userArgs);
727 const QFileInfo sourceFi(source);
728 const QString workDir = sourceFi.isDir() ? sourceFi.absoluteFilePath() : sourceFi.absolutePath();
729 executeGit(workDir, arguments, editor);
732 void GitClient::slotBlameRevisionRequested(const QString &source, QString change, int lineNumber)
734 // This might be invoked with a verbose revision description
735 // "SHA1 author subject" from the annotation context menu. Strip the rest.
736 const int blankPos = change.indexOf(QLatin1Char(' '));
738 change.truncate(blankPos);
739 const QFileInfo fi(source);
740 blame(fi.absolutePath(), QStringList(), fi.fileName(), change, lineNumber);
743 void GitClient::blame(const QString &workingDirectory,
744 const QStringList &args,
745 const QString &fileName,
746 const QString &revision /* = QString() */,
747 int lineNumber /* = -1 */)
749 if (Git::Constants::debug)
750 qDebug() << "blame" << workingDirectory << fileName << lineNumber << args;
752 const QString editorId = QLatin1String(Git::Constants::GIT_BLAME_EDITOR_ID);
753 const QString id = VCSBase::VCSBaseEditorWidget::getTitleId(workingDirectory, QStringList(fileName), revision);
754 const QString title = tr("Git Blame %1").arg(id);
755 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, fileName);
757 QStringList userBlameArgs = args;
758 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("blameFileName", id);
760 GitBlameArgumentsWidget *argWidget =
761 new GitBlameArgumentsWidget(&m_settings, this, workingDirectory, userBlameArgs,
763 editor = createVCSEditor(editorId, title, sourceFile, true, "blameFileName", id, argWidget);
764 argWidget->setEditor(editor);
766 userBlameArgs = argWidget->arguments();
769 QStringList arguments(QLatin1String("blame"));
770 arguments << QLatin1String("--root");
771 arguments.append(userBlameArgs);
772 arguments << QLatin1String("--") << fileName;
773 if (!revision.isEmpty())
774 arguments << revision;
775 executeGit(workingDirectory, arguments, editor, false, GitCommand::NoReport, lineNumber);
778 void GitClient::checkoutBranch(const QString &workingDirectory, const QString &branch)
780 QStringList arguments(QLatin1String("checkout"));
782 GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
783 connectRepositoryChanged(workingDirectory, cmd);
786 bool GitClient::synchronousCheckoutBranch(const QString &workingDirectory,
787 const QString &branch,
788 QString *errorMessage /* = 0 */)
790 QByteArray outputText;
791 QByteArray errorText;
792 QStringList arguments;
793 arguments << QLatin1String("checkout") << branch;
794 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
795 const QString output = commandOutputFromLocal8Bit(outputText);
796 outputWindow()->append(output);
798 const QString stdErr = commandOutputFromLocal8Bit(errorText);
799 //: Meaning of the arguments: %1: Branch, %2: Repository, %3: Error message
800 const QString msg = tr("Unable to checkout %1 of %2: %3").arg(branch, workingDirectory, stdErr);
804 outputWindow()->appendError(msg);
811 void GitClient::checkout(const QString &workingDirectory, const QString &fileName)
813 // Passing an empty argument as the file name is very dangereous, since this makes
814 // git checkout apply to all files. Almost looks like a bug in git.
815 if (fileName.isEmpty())
818 QStringList arguments;
819 arguments << QLatin1String("checkout") << QLatin1String("HEAD") << QLatin1String("--")
822 executeGit(workingDirectory, arguments, 0, true);
825 void GitClient::hardReset(const QString &workingDirectory, const QString &commit)
827 QStringList arguments;
828 arguments << QLatin1String("reset") << QLatin1String("--hard");
829 if (!commit.isEmpty())
832 GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
833 connectRepositoryChanged(workingDirectory, cmd);
836 void GitClient::addFile(const QString &workingDirectory, const QString &fileName)
838 QStringList arguments;
839 arguments << QLatin1String("add") << fileName;
841 executeGit(workingDirectory, arguments, 0, true);
844 // Warning: 'intendToAdd' works only from 1.6.1 onwards
845 bool GitClient::synchronousAdd(const QString &workingDirectory,
847 const QStringList &files)
849 if (Git::Constants::debug)
850 qDebug() << Q_FUNC_INFO << workingDirectory << files;
851 QByteArray outputText;
852 QByteArray errorText;
853 QStringList arguments;
854 arguments << QLatin1String("add");
856 arguments << QLatin1String("--intent-to-add");
857 arguments.append(files);
858 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
860 const QString errorMessage = tr("Unable to add %n file(s) to %1: %2", 0, files.size()).
861 arg(QDir::toNativeSeparators(workingDirectory),
862 commandOutputFromLocal8Bit(errorText));
863 outputWindow()->appendError(errorMessage);
868 bool GitClient::synchronousDelete(const QString &workingDirectory,
870 const QStringList &files)
872 if (Git::Constants::debug)
873 qDebug() << Q_FUNC_INFO << workingDirectory << files;
874 QByteArray outputText;
875 QByteArray errorText;
876 QStringList arguments;
877 arguments << QLatin1String("rm");
879 arguments << QLatin1String("--force");
880 arguments.append(files);
881 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
883 const QString errorMessage = tr("Unable to remove %n file(s) from %1: %2", 0, files.size()).
884 arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
885 outputWindow()->appendError(errorMessage);
890 bool GitClient::synchronousMove(const QString &workingDirectory,
894 if (Git::Constants::debug)
895 qDebug() << Q_FUNC_INFO << workingDirectory << from << to;
896 QByteArray outputText;
897 QByteArray errorText;
898 QStringList arguments;
899 arguments << QLatin1String("mv");
902 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
904 const QString errorMessage = tr("Unable to move from %1 to %2: %3").
905 arg(from, to, commandOutputFromLocal8Bit(errorText));
906 outputWindow()->appendError(errorMessage);
911 bool GitClient::synchronousReset(const QString &workingDirectory,
912 const QStringList &files,
913 QString *errorMessage)
915 if (Git::Constants::debug)
916 qDebug() << Q_FUNC_INFO << workingDirectory << files;
917 QByteArray outputText;
918 QByteArray errorText;
919 QStringList arguments;
920 arguments << QLatin1String("reset");
921 if (files.isEmpty()) {
922 arguments << QLatin1String("--hard");
924 arguments << QLatin1String("HEAD") << QLatin1String("--") << files;
926 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
927 const QString output = commandOutputFromLocal8Bit(outputText);
928 outputWindow()->append(output);
929 // Note that git exits with 1 even if the operation is successful
930 // Assume real failure if the output does not contain "foo.cpp modified"
931 // or "Unstaged changes after reset" (git 1.7.0).
933 (!output.contains(QLatin1String("modified"))
934 && !output.contains(QLatin1String("Unstaged changes after reset")))) {
935 const QString stdErr = commandOutputFromLocal8Bit(errorText);
936 const QString msg = files.isEmpty() ?
937 tr("Unable to reset %1: %2").arg(QDir::toNativeSeparators(workingDirectory), stdErr) :
938 tr("Unable to reset %n file(s) in %1: %2", 0, files.size()).
939 arg(QDir::toNativeSeparators(workingDirectory), stdErr);
943 outputWindow()->appendError(msg);
950 // Initialize repository
951 bool GitClient::synchronousInit(const QString &workingDirectory)
953 if (Git::Constants::debug)
954 qDebug() << Q_FUNC_INFO << workingDirectory;
955 QByteArray outputText;
956 QByteArray errorText;
957 const QStringList arguments(QLatin1String("init"));
958 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
959 // '[Re]Initialized...'
960 outputWindow()->append(commandOutputFromLocal8Bit(outputText));
962 outputWindow()->appendError(commandOutputFromLocal8Bit(errorText));
966 /* Checkout, supports:
967 * git checkout -- <files>
968 * git checkout revision -- <files>
969 * git checkout revision -- . */
970 bool GitClient::synchronousCheckoutFiles(const QString &workingDirectory,
971 QStringList files /* = QStringList() */,
972 QString revision /* = QString() */,
973 QString *errorMessage /* = 0 */,
974 bool revertStaging /* = true */)
976 if (Git::Constants::debug)
977 qDebug() << Q_FUNC_INFO << workingDirectory << files;
978 if (revertStaging && revision.isEmpty())
979 revision = QLatin1String("HEAD");
981 files = QStringList(QString(QLatin1Char('.')));
982 QByteArray outputText;
983 QByteArray errorText;
984 QStringList arguments;
985 arguments << QLatin1String("checkout");
987 arguments << revision;
988 arguments << QLatin1String("--") << files;
989 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
991 const QString fileArg = files.join(QLatin1String(", "));
992 //: Meaning of the arguments: %1: revision, %2: files, %3: repository,
993 //: %4: Error message
994 const QString msg = tr("Unable to checkout %1 of %2 in %3: %4").
995 arg(revision, fileArg, workingDirectory, commandOutputFromLocal8Bit(errorText));
999 outputWindow()->appendError(msg);
1006 static inline QString msgParentRevisionFailed(const QString &workingDirectory,
1007 const QString &revision,
1010 //: Failed to find parent revisions of a SHA1 for "annotate previous"
1011 return GitClient::tr("Unable to find parent revisions of %1 in %2: %3").arg(revision, workingDirectory, why);
1014 static inline QString msgInvalidRevision()
1016 return GitClient::tr("Invalid revision");
1019 // Split a line of "<commit> <parent1> ..." to obtain parents from "rev-list" or "log".
1020 static inline bool splitCommitParents(const QString &line,
1021 QString *commit = 0,
1022 QStringList *parents = 0)
1028 QStringList tokens = line.trimmed().split(QLatin1Char(' '));
1029 if (tokens.size() < 2)
1032 *commit = tokens.front();
1039 // Find out the immediate parent revisions of a revision of the repository.
1040 // Might be several in case of merges.
1041 bool GitClient::synchronousParentRevisions(const QString &workingDirectory,
1042 const QStringList &files /* = QStringList() */,
1043 const QString &revision,
1044 QStringList *parents,
1045 QString *errorMessage)
1047 if (Git::Constants::debug)
1048 qDebug() << Q_FUNC_INFO << workingDirectory << revision;
1049 QByteArray outputTextData;
1050 QByteArray errorText;
1051 QStringList arguments;
1052 arguments << QLatin1String("rev-list") << QLatin1String(GitClient::noColorOption)
1053 << QLatin1String("--parents") << QLatin1String("--max-count=1") << revision;
1054 if (!files.isEmpty()) {
1055 arguments.append(QLatin1String("--"));
1056 arguments.append(files);
1058 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
1060 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, commandOutputFromLocal8Bit(errorText));
1063 // Should result in one line of blank-delimited revisions, specifying current first
1064 // unless it is top.
1065 QString outputText = commandOutputFromLocal8Bit(outputTextData);
1066 outputText.remove(QLatin1Char('\n'));
1067 if (!splitCommitParents(outputText, 0, parents)) {
1068 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, msgInvalidRevision());
1071 if (Git::Constants::debug)
1072 qDebug() << workingDirectory << files << revision << "->" << *parents;
1076 // Short SHA1, author, subject
1077 static const char defaultShortLogFormatC[] = "%h (%an \"%s\")";
1079 bool GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision,
1080 QString *description, QString *errorMessage)
1082 // Short SHA 1, author, subject
1083 return synchronousShortDescription(workingDirectory, revision,
1084 QLatin1String(defaultShortLogFormatC),
1085 description, errorMessage);
1088 // Convenience working on a list of revisions
1089 bool GitClient::synchronousShortDescriptions(const QString &workingDirectory, const QStringList &revisions,
1090 QStringList *descriptions, QString *errorMessage)
1092 descriptions->clear();
1093 foreach (const QString &revision, revisions) {
1094 QString description;
1095 if (!synchronousShortDescription(workingDirectory, revision, &description, errorMessage)) {
1096 descriptions->clear();
1099 descriptions->push_back(description);
1104 static inline QString msgCannotDetermineBranch(const QString &workingDirectory, const QString &why)
1106 return GitClient::tr("Unable to retrieve branch of %1: %2").arg(QDir::toNativeSeparators(workingDirectory), why);
1109 // Retrieve head revision/branch
1110 bool GitClient::synchronousTopRevision(const QString &workingDirectory,
1111 QString *revision /* = 0 */,
1112 QString *branch /* = 0 */,
1113 QString *errorMessageIn /* = 0 */)
1115 if (Git::Constants::debug)
1116 qDebug() << Q_FUNC_INFO << workingDirectory;
1117 QByteArray outputTextData;
1118 QByteArray errorText;
1119 QStringList arguments;
1120 QString errorMessage;
1125 arguments << QLatin1String("log") << QLatin1String(noColorOption)
1126 << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:%H");
1127 if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
1128 errorMessage = tr("Unable to retrieve top revision of %1: %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1131 *revision = commandOutputFromLocal8Bit(outputTextData);
1132 revision->remove(QLatin1Char('\n'));
1133 } // revision desired
1138 arguments << QLatin1String("branch") << QLatin1String(noColorOption);
1139 if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
1140 errorMessage = msgCannotDetermineBranch(workingDirectory, commandOutputFromLocal8Bit(errorText));
1143 /* parse output for current branch: \code
1147 const QString branchPrefix = QLatin1String("* ");
1148 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputTextData)) {
1149 if (line.startsWith(branchPrefix)) {
1151 branch->remove(0, branchPrefix.size());
1155 if (branch->isEmpty()) {
1156 errorMessage = msgCannotDetermineBranch(workingDirectory,
1157 QString::fromLatin1("Internal error: Failed to parse output: %1").arg(commandOutputFromLocal8Bit(outputTextData)));
1162 const bool failed = (revision && revision->isEmpty()) || (branch && branch->isEmpty());
1163 if (failed && !errorMessage.isEmpty()) {
1164 if (errorMessageIn) {
1165 *errorMessageIn = errorMessage;
1167 outputWindow()->appendError(errorMessage);
1173 // Format an entry in a one-liner for selection list using git log.
1174 bool GitClient::synchronousShortDescription(const QString &workingDirectory,
1175 const QString &revision,
1176 const QString &format,
1177 QString *description,
1178 QString *errorMessage)
1180 if (Git::Constants::debug)
1181 qDebug() << Q_FUNC_INFO << workingDirectory << revision;
1182 QByteArray outputTextData;
1183 QByteArray errorText;
1184 QStringList arguments;
1185 arguments << QLatin1String("log") << QLatin1String(GitClient::noColorOption)
1186 << (QLatin1String("--pretty=format:") + format)
1187 << QLatin1String("--max-count=1") << revision;
1188 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
1190 *errorMessage = tr("Unable to describe revision %1 in %2: %3").arg(revision, workingDirectory, commandOutputFromLocal8Bit(errorText));
1193 *description = commandOutputFromLocal8Bit(outputTextData);
1194 if (description->endsWith(QLatin1Char('\n')))
1195 description->truncate(description->size() - 1);
1199 // Create a default message to be used for describing stashes
1200 static inline QString creatorStashMessage(const QString &keyword = QString())
1202 QString rc = QCoreApplication::applicationName();
1203 rc += QLatin1Char(' ');
1204 if (!keyword.isEmpty()) {
1206 rc += QLatin1Char(' ');
1208 rc += QDateTime::currentDateTime().toString(Qt::ISODate);
1212 /* Do a stash and return the message as identifier. Note that stash names (stash{n})
1213 * shift as they are pushed, so, enforce the use of messages to identify them. Flags:
1214 * StashPromptDescription: Prompt the user for a description message.
1215 * StashImmediateRestore: Immediately re-apply this stash (used for snapshots), user keeps on working
1216 * StashIgnoreUnchanged: Be quiet about unchanged repositories (used for IVersionControl's snapshots). */
1218 QString GitClient::synchronousStash(const QString &workingDirectory,
1219 const QString &messageKeyword /* = QString() */,
1221 bool *unchanged /* =0 */)
1226 bool success = false;
1227 // Check for changes and stash
1228 QString errorMessage;
1229 switch (gitStatus(workingDirectory, false, 0, &errorMessage)) {
1230 case StatusChanged: {
1231 message = creatorStashMessage(messageKeyword);
1233 if ((flags & StashPromptDescription)) {
1234 if (!inputText(Core::ICore::instance()->mainWindow(),
1235 tr("Stash Description"), tr("Description:"), &message))
1238 if (!executeSynchronousStash(workingDirectory, message))
1240 if ((flags & StashImmediateRestore)
1241 && !synchronousStashRestore(workingDirectory, QLatin1String("stash@{0}")))
1247 case StatusUnchanged:
1250 if (!(flags & StashIgnoreUnchanged))
1251 outputWindow()->append(msgNoChangedFiles());
1254 outputWindow()->append(errorMessage);
1259 if (Git::Constants::debug)
1260 qDebug() << Q_FUNC_INFO << '\n' << workingDirectory << messageKeyword << "returns" << message;
1264 bool GitClient::executeSynchronousStash(const QString &workingDirectory,
1265 const QString &message,
1266 QString *errorMessage /* = 0*/)
1268 if (Git::Constants::debug)
1269 qDebug() << Q_FUNC_INFO << workingDirectory;
1270 QByteArray outputText;
1271 QByteArray errorText;
1272 QStringList arguments;
1273 arguments << QLatin1String("stash");
1274 if (!message.isEmpty())
1275 arguments << QLatin1String("save") << message;
1276 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
1278 const QString msg = tr("Unable stash in %1: %2").
1279 arg(QDir::toNativeSeparators(workingDirectory),
1280 commandOutputFromLocal8Bit(errorText));
1282 *errorMessage = msg;
1284 outputWindow()->append(msg);
1291 // Resolve a stash name from message
1292 bool GitClient::stashNameFromMessage(const QString &workingDirectory,
1293 const QString &message, QString *name,
1294 QString *errorMessage /* = 0 */)
1297 if (message.startsWith(QLatin1String(stashNamePrefix))) {
1301 // Retrieve list and find via message
1302 QList<Stash> stashes;
1303 if (!synchronousStashList(workingDirectory, &stashes, errorMessage))
1305 foreach (const Stash &s, stashes) {
1306 if (s.message == message) {
1311 //: Look-up of a stash via its descriptive message failed.
1312 const QString msg = tr("Unable to resolve stash message '%1' in %2").arg(message, workingDirectory);
1314 *errorMessage = msg;
1316 outputWindow()->append(msg);
1321 bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs,
1322 QString *output, QString *errorMessage)
1324 if (Git::Constants::debug)
1325 qDebug() << Q_FUNC_INFO << workingDirectory << branchArgs;
1326 branchArgs.push_front(QLatin1String("branch"));
1327 QByteArray outputText;
1328 QByteArray errorText;
1329 const bool rc = fullySynchronousGit(workingDirectory, branchArgs, &outputText, &errorText);
1331 *errorMessage = tr("Unable to run a 'git branch' command in %1: %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1334 *output = commandOutputFromLocal8Bit(outputText);
1338 bool GitClient::synchronousShow(const QString &workingDirectory, const QString &id,
1339 QString *output, QString *errorMessage)
1341 if (Git::Constants::debug)
1342 qDebug() << Q_FUNC_INFO << workingDirectory << id;
1344 *errorMessage = msgCannotShow(id);
1347 QStringList args(QLatin1String("show"));
1348 args << QLatin1String(noColorOption) << id;
1349 QByteArray outputText;
1350 QByteArray errorText;
1351 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1353 *errorMessage = tr("Unable to run 'git show' in %1: %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1356 *output = commandOutputFromLocal8Bit(outputText);
1360 // Retrieve list of files to be cleaned
1361 bool GitClient::synchronousCleanList(const QString &workingDirectory,
1362 QStringList *files, QString *errorMessage)
1364 if (Git::Constants::debug)
1365 qDebug() << Q_FUNC_INFO << workingDirectory;
1368 args << QLatin1String("clean") << QLatin1String("--dry-run") << QLatin1String("-dxf");
1369 QByteArray outputText;
1370 QByteArray errorText;
1371 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1373 *errorMessage = tr("Unable to run 'git clean' in %1: %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1376 // Filter files that git would remove
1377 const QString prefix = QLatin1String("Would remove ");
1378 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText))
1379 if (line.startsWith(prefix))
1380 files->push_back(line.mid(prefix.size()));
1384 bool GitClient::synchronousApplyPatch(const QString &workingDirectory,
1385 const QString &file, QString *errorMessage)
1387 if (Git::Constants::debug)
1388 qDebug() << Q_FUNC_INFO << workingDirectory;
1390 args << QLatin1String("apply") << QLatin1String("--whitespace=fix") << file;
1391 QByteArray outputText;
1392 QByteArray errorText;
1393 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1395 if (!errorText.isEmpty())
1396 *errorMessage = tr("There were warnings while applying %1 to %2:\n%3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
1398 *errorMessage = tr("Unable apply patch %1 to %2: %3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
1404 // Factory function to create an asynchronous command
1405 GitCommand *GitClient::createCommand(const QString &workingDirectory,
1406 VCSBase::VCSBaseEditorWidget* editor,
1407 bool outputToWindow,
1408 int editorLineNumber)
1410 if (Git::Constants::debug)
1411 qDebug() << Q_FUNC_INFO << workingDirectory << editor;
1413 VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();
1414 GitCommand* command = new GitCommand(binary(), workingDirectory, processEnvironment(), QVariant(editorLineNumber));
1416 connect(command, SIGNAL(finished(bool,int,QVariant)), editor, SLOT(commandFinishedGotoLine(bool,int,QVariant)));
1417 if (outputToWindow) {
1418 if (editor) { // assume that the commands output is the important thing
1419 connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendDataSilently(QByteArray)));
1421 connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendData(QByteArray)));
1424 QTC_ASSERT(editor, /**/);
1425 connect(command, SIGNAL(outputData(QByteArray)), editor, SLOT(setPlainTextDataFiltered(QByteArray)));
1429 connect(command, SIGNAL(errorText(QString)), outputWindow, SLOT(appendError(QString)));
1433 // Execute a single command
1434 GitCommand *GitClient::executeGit(const QString &workingDirectory,
1435 const QStringList &arguments,
1436 VCSBase::VCSBaseEditorWidget* editor,
1437 bool outputToWindow,
1438 GitCommand::TerminationReportMode tm,
1439 int editorLineNumber,
1440 bool unixTerminalDisabled)
1442 outputWindow()->appendCommand(workingDirectory, QLatin1String(Constants::GIT_BINARY), arguments);
1443 GitCommand *command = createCommand(workingDirectory, editor, outputToWindow, editorLineNumber);
1444 command->addJob(arguments, m_settings.timeoutSeconds);
1445 command->setTerminationReportMode(tm);
1446 command->setUnixTerminalDisabled(unixTerminalDisabled);
1451 // Return fixed arguments required to run
1452 QString GitClient::binary() const
1454 return m_binaryPath;
1457 // Determine a value for the HOME variable on Windows
1458 // working around MSys git not finding it when run outside git bash
1459 // (then looking for the SSH keys under "\program files\git").
1461 QString GitClient::fakeWinHome(const QProcessEnvironment &e)
1463 const QString homeDrive = e.value("HOMEDRIVE");
1464 const QString homePath = e.value("HOMEPATH");
1465 QTC_ASSERT(!homeDrive.isEmpty() && !homePath.isEmpty(), return QString())
1466 return homeDrive + homePath;
1469 QProcessEnvironment GitClient::processEnvironment() const
1472 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
1473 if (m_settings.adoptPath)
1474 environment.insert(QLatin1String("PATH"), m_settings.path);
1476 if (m_settings.winSetHomeEnvironment) {
1477 const QString home = fakeWinHome(environment);
1478 if (Constants::debug)
1479 qDebug("Setting home '%s'", qPrintable(home));
1480 environment.insert(QLatin1String("HOME"), home);
1483 // Set up SSH and C locale (required by git using perl).
1484 VCSBase::VCSBasePlugin::setProcessEnvironment(&environment, false);
1488 // Synchronous git execution using Utils::SynchronousProcess, with
1489 // log windows updating.
1490 Utils::SynchronousProcessResponse
1491 GitClient::synchronousGit(const QString &workingDirectory,
1492 const QStringList &gitArguments,
1494 QTextCodec *stdOutCodec)
1496 if (Git::Constants::debug)
1497 qDebug() << "synchronousGit" << workingDirectory << gitArguments;
1498 return VCSBase::VCSBasePlugin::runVCS(workingDirectory, binary(), gitArguments,
1499 m_settings.timeoutSeconds * 1000,
1500 processEnvironment(),
1501 flags, stdOutCodec);
1504 bool GitClient::fullySynchronousGit(const QString &workingDirectory,
1505 const QStringList &gitArguments,
1506 QByteArray* outputText,
1507 QByteArray* errorText,
1508 bool logCommandToWindow)
1510 if (Git::Constants::debug)
1511 qDebug() << "fullySynchronousGit" << workingDirectory << gitArguments;
1513 if (logCommandToWindow)
1514 outputWindow()->appendCommand(workingDirectory, m_binaryPath, gitArguments);
1517 process.setWorkingDirectory(workingDirectory);
1518 process.setProcessEnvironment(processEnvironment());
1520 process.start(binary(), gitArguments);
1521 process.closeWriteChannel();
1522 if (!process.waitForStarted()) {
1524 const QString msg = QString::fromLatin1("Unable to execute '%1': %2:")
1525 .arg(binary(), process.errorString());
1526 *errorText = msg.toLocal8Bit();
1531 if (!Utils::SynchronousProcess::readDataFromProcess(process, m_settings.timeoutSeconds * 1000,
1532 outputText, errorText, true)) {
1533 errorText->append(GitCommand::msgTimeout(m_settings.timeoutSeconds).toLocal8Bit());
1534 Utils::SynchronousProcess::stopProcess(process);
1538 if (Git::Constants::debug)
1539 qDebug() << "synchronousGit ex=" << process.exitStatus() << process.exitCode();
1540 return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
1544 askWithDetailedText(QWidget *parent,
1545 const QString &title, const QString &msg,
1547 QMessageBox::StandardButton defaultButton,
1548 QMessageBox::StandardButtons buttons = QMessageBox::Yes|QMessageBox::No)
1550 QMessageBox msgBox(QMessageBox::Question, title, msg, buttons, parent);
1551 msgBox.setDetailedText(inf);
1552 msgBox.setDefaultButton(defaultButton);
1553 return msgBox.exec();
1556 // Convenience that pops up an msg box.
1557 GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory)
1559 QString errorMessage;
1560 const StashResult sr = ensureStash(workingDirectory, &errorMessage);
1561 if (sr == StashFailed)
1562 outputWindow()->appendError(errorMessage);
1566 // Ensure that changed files are stashed before a pull or similar
1567 GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory, QString *errorMessage)
1569 QString statusOutput;
1570 switch (gitStatus(workingDirectory, false, &statusOutput, errorMessage)) {
1573 case StatusUnchanged:
1574 return StashUnchanged;
1579 const int answer = askWithDetailedText(m_core->mainWindow(), tr("Changes"),
1580 tr("You have modified files. Would you like to stash your changes?"),
1581 statusOutput, QMessageBox::Yes, QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel);
1583 case QMessageBox::Cancel:
1584 return StashCanceled;
1585 case QMessageBox::Yes:
1586 if (!executeSynchronousStash(workingDirectory, creatorStashMessage(QLatin1String("push")), errorMessage))
1589 case QMessageBox::No: // At your own risk, so.
1596 // Trim a git status file spec: "modified: foo .cpp" -> "modified: foo .cpp"
1597 static inline QString trimFileSpecification(QString fileSpec)
1599 const int colonIndex = fileSpec.indexOf(QLatin1Char(':'));
1600 if (colonIndex != -1) {
1601 // Collapse the sequence of spaces
1602 const int filePos = colonIndex + 2;
1603 int nonBlankPos = filePos;
1604 for ( ; fileSpec.at(nonBlankPos).isSpace(); nonBlankPos++) ;
1605 if (nonBlankPos > filePos)
1606 fileSpec.remove(filePos, nonBlankPos - filePos);
1611 GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory,
1614 QString *errorMessage,
1617 // Run 'status'. Note that git returns exitcode 1 if there are no added files.
1618 QByteArray outputText;
1619 QByteArray errorText;
1620 // @TODO: Use "--no-color" once it is supported
1621 QStringList statusArgs(QLatin1String("status"));
1623 statusArgs << QLatin1String("-u");
1624 const bool statusRc = fullySynchronousGit(workingDirectory, statusArgs, &outputText, &errorText);
1625 GitCommand::removeColorCodes(&outputText);
1627 *output = commandOutputFromLocal8Bit(outputText);
1628 const bool branchKnown = outputText.contains(kBranchIndicatorC);
1630 *onBranch = branchKnown;
1631 // Is it something really fatal?
1632 if (!statusRc && !branchKnown && !outputText.contains("# Not currently on any branch.")) {
1634 const QString error = commandOutputFromLocal8Bit(errorText);
1635 *errorMessage = tr("Unable to obtain the status: %1").arg(error);
1637 return StatusFailed;
1639 // Unchanged (output text depending on whether -u was passed)
1640 if (outputText.contains("nothing to commit"))
1641 return StatusUnchanged;
1642 if (outputText.contains("nothing added to commit but untracked files present"))
1643 return untracked ? StatusChanged : StatusUnchanged;
1644 return StatusChanged;
1647 // Quietly retrieve branch list of remote repository URL
1649 // The branch HEAD is pointing to is always returned first.
1650 QStringList GitClient::synchronousRepositoryBranches(const QString &repositoryURL)
1652 QStringList arguments(QLatin1String("ls-remote"));
1653 arguments << repositoryURL << QLatin1String("HEAD") << QLatin1String("refs/heads/*");
1654 const unsigned flags =
1655 VCSBase::VCSBasePlugin::SshPasswordPrompt|
1656 VCSBase::VCSBasePlugin::SuppressStdErrInLogWindow|
1657 VCSBase::VCSBasePlugin::SuppressFailMessageInLogWindow;
1658 const Utils::SynchronousProcessResponse resp = synchronousGit(QString(), arguments, flags);
1659 QStringList branches;
1660 branches << "<detached HEAD>";
1662 if (resp.result == Utils::SynchronousProcessResponse::Finished) {
1663 // split "82bfad2f51d34e98b18982211c82220b8db049b<tab>refs/heads/master"
1664 foreach(const QString &line, resp.stdOut.split(QLatin1Char('\n'))) {
1665 if (line.endsWith("\tHEAD")) {
1666 Q_ASSERT(headSha.isNull());
1667 headSha = line.left(line.indexOf(QChar('\t')));
1671 const int slashPos = line.lastIndexOf(QLatin1Char('/'));
1672 const QString branchName = line.mid(slashPos + 1);
1673 if (slashPos != -1) {
1674 if (line.startsWith(headSha))
1675 branches[0] = branchName;
1677 branches.push_back(branchName);
1684 void GitClient::launchGitK(const QString &workingDirectory)
1686 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
1687 // Locate git in (potentially) custom path. m_binaryPath can be absolute,
1688 // which will be handled correctly.
1689 QTC_ASSERT(!m_binaryPath.isEmpty(), return);
1690 const QString gitBinary = QLatin1String(Constants::GIT_BINARY);
1691 const QProcessEnvironment env = processEnvironment();
1692 const QString path = env.value(QLatin1String("PATH"));
1693 const QString fullGitBinary = Utils::SynchronousProcess::locateBinary(path, m_binaryPath);
1694 if (fullGitBinary.isEmpty()) {
1695 outwin->appendError(tr("Cannot locate %1.").arg(gitBinary));
1698 const QString gitBinDirectory = QFileInfo(fullGitBinary).absolutePath();
1699 QDir foundBinDir = gitBinDirectory;
1700 const bool foundBinDirIsCmdDir = foundBinDir.dirName() == "cmd";
1701 if (!tryLauchingGitK(env, workingDirectory, gitBinDirectory, foundBinDirIsCmdDir)) {
1702 if (foundBinDirIsCmdDir) {
1704 tryLauchingGitK(env, workingDirectory, foundBinDir.path() + "/bin", false);
1709 bool GitClient::tryLauchingGitK(const QProcessEnvironment &env,
1710 const QString &workingDirectory,
1711 const QString &gitBinDirectory,
1715 // Launch 'wish' shell from git binary directory with the gitk located there
1716 const QString binary = gitBinDirectory + QLatin1String("/wish");
1717 QStringList arguments(gitBinDirectory + QLatin1String("/gitk"));
1719 // Simple: Run gitk from binary path
1720 const QString binary = gitBinDirectory + QLatin1String("/gitk");
1721 QStringList arguments;
1723 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
1724 if (!m_settings.gitkOptions.isEmpty())
1725 arguments.append(Utils::QtcProcess::splitArgs(m_settings.gitkOptions));
1726 outwin->appendCommand(workingDirectory, binary, arguments);
1727 // This should always use QProcess::startDetached (as not to kill
1728 // the child), but that does not have an environment parameter.
1729 bool success = false;
1730 if (m_settings.adoptPath) {
1731 QProcess *process = new QProcess(this);
1732 process->setWorkingDirectory(workingDirectory);
1733 process->setProcessEnvironment(env);
1734 process->start(binary, arguments);
1735 success = process->waitForStarted();
1737 connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
1742 success = QProcess::startDetached(binary, arguments, workingDirectory);
1745 const QString error = tr("Unable to launch %1.").arg(binary);
1747 outwin->appendSilently(error);
1749 outwin->appendError(error);
1754 bool GitClient::getCommitData(const QString &workingDirectory,
1756 QString *commitTemplate,
1757 CommitData *commitData,
1758 QString *errorMessage)
1760 if (Git::Constants::debug)
1761 qDebug() << Q_FUNC_INFO << workingDirectory;
1763 commitData->clear();
1766 const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
1767 if (repoDirectory.isEmpty()) {
1768 *errorMessage = msgRepositoryNotFound(workingDirectory);
1772 commitData->panelInfo.repository = repoDirectory;
1774 QDir gitDir(repoDirectory);
1775 if (!gitDir.cd(QLatin1String(kGitDirectoryC))) {
1776 *errorMessage = tr("The repository %1 is not initialized yet.").arg(repoDirectory);
1781 const QString descriptionFile = gitDir.absoluteFilePath(QLatin1String("description"));
1782 if (QFileInfo(descriptionFile).isFile()) {
1783 QFile file(descriptionFile);
1784 if (file.open(QIODevice::ReadOnly|QIODevice::Text))
1785 commitData->panelInfo.description = commandOutputFromLocal8Bit(file.readAll()).trimmed();
1788 // Run status. Note that it has exitcode 1 if there are no added files.
1791 const StatusResult status = gitStatus(repoDirectory, true, &output, errorMessage, &onBranch);
1795 *errorMessage = tr("You did not checkout a branch.");
1799 case StatusUnchanged:
1802 *errorMessage = msgNoChangedFiles();
1808 // Output looks like:
1809 // # On branch [branchname]
1810 // # Changes to be committed:
1811 // # (use "git reset HEAD <file>..." to unstage)
1813 // # modified: somefile.cpp
1814 // # new File: somenew.h
1816 // # Changed but not updated:
1817 // # (use "git add <file>..." to update what will be committed)
1819 // # modified: someother.cpp
1820 // # modified: submodule (modified content)
1821 // # modified: submodule2 (new commit)
1823 // # Untracked files:
1824 // # (use "git add <file>..." to include in what will be committed)
1826 // # list of files...
1828 if (status != StatusUnchanged) {
1829 if (!commitData->parseFilesFromStatus(output)) {
1830 *errorMessage = msgParseFilesFailed();
1833 // Filter out untracked files that are not part of the project
1834 VCSBase::VCSBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &commitData->untrackedFiles);
1835 if (commitData->filesEmpty()) {
1836 *errorMessage = msgNoChangedFiles();
1841 commitData->panelData.author = readConfigValue(workingDirectory, QLatin1String("user.name"));
1842 commitData->panelData.email = readConfigValue(workingDirectory, QLatin1String("user.email"));
1844 // Get the commit template or the last commit message
1846 // Amend: get last commit data as "SHA1@message". TODO: Figure out codec.
1847 QStringList args(QLatin1String("log"));
1848 const QString format = synchronousGitVersion(true) > 0x010701 ? "%h@%B" : "%h@%s%n%n%b";
1849 args << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:") + format;
1850 const Utils::SynchronousProcessResponse sp = synchronousGit(repoDirectory, args);
1851 if (sp.result != Utils::SynchronousProcessResponse::Finished) {
1852 *errorMessage = tr("Unable to retrieve the last commit data of the repository %1.").arg(repoDirectory);
1855 const int separatorPos = sp.stdOut.indexOf(QLatin1Char('@'));
1856 QTC_ASSERT(separatorPos != -1, return false)
1857 commitData->amendSHA1= sp.stdOut.left(separatorPos);
1858 *commitTemplate = sp.stdOut.mid(separatorPos + 1);
1860 // Commit: Get the commit template
1861 QString templateFilename = readConfigValue(workingDirectory, QLatin1String("commit.template"));
1862 if (!templateFilename.isEmpty()) {
1863 // Make relative to repository
1864 const QFileInfo templateFileInfo(templateFilename);
1865 if (templateFileInfo.isRelative())
1866 templateFilename = repoDirectory + QLatin1Char('/') + templateFilename;
1867 QFile templateFile(templateFilename);
1868 if (templateFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
1869 *commitTemplate = QString::fromLocal8Bit(templateFile.readAll());
1871 qWarning("Unable to read commit template %s: %s",
1872 qPrintable(templateFilename),
1873 qPrintable(templateFile.errorString()));
1880 // Log message for commits/amended commits to go to output window
1881 static inline QString msgCommitted(const QString &amendSHA1, int fileCount)
1883 if (amendSHA1.isEmpty())
1884 return GitClient::tr("Committed %n file(s).\n", 0, fileCount);
1886 return GitClient::tr("Amended %1 (%n file(s)).\n", 0, fileCount).arg(amendSHA1);
1887 return GitClient::tr("Amended %1.").arg(amendSHA1);
1891 bool GitClient::addAndCommit(const QString &repositoryDirectory,
1892 const GitSubmitEditorPanelData &data,
1893 const QString &amendSHA1,
1894 const QString &messageFile,
1895 const QStringList &checkedFiles,
1896 const QStringList &origCommitFiles,
1897 const QStringList &origDeletedFiles)
1899 if (Git::Constants::debug)
1900 qDebug() << "GitClient::addAndCommit:" << repositoryDirectory << checkedFiles << origCommitFiles;
1901 const QString renamedSeparator = QLatin1String(" -> ");
1902 const bool amend = !amendSHA1.isEmpty();
1904 // Do we need to reset any files that had been added before
1905 // (did the user uncheck any previously added files)
1906 // Split up renamed files ('foo.cpp -> foo2.cpp').
1907 QStringList resetFiles = origCommitFiles.toSet().subtract(checkedFiles.toSet()).toList();
1908 for (QStringList::iterator it = resetFiles.begin(); it != resetFiles.end(); ++it) {
1909 const int renamedPos = it->indexOf(renamedSeparator);
1910 if (renamedPos != -1) {
1911 const QString newFile = it->mid(renamedPos + renamedSeparator.size());
1912 it->truncate(renamedPos);
1913 it = resetFiles.insert(++it, newFile);
1917 if (!resetFiles.isEmpty())
1918 if (!synchronousReset(repositoryDirectory, resetFiles))
1921 // Re-add all to make sure we have the latest changes, but only add those that aren't marked
1922 // for deletion. Purge out renamed files ('foo.cpp -> foo2.cpp').
1923 QStringList addFiles = checkedFiles.toSet().subtract(origDeletedFiles.toSet()).toList();
1924 for (QStringList::iterator it = addFiles.begin(); it != addFiles.end(); ) {
1925 if (it->contains(renamedSeparator)) {
1926 it = addFiles.erase(it);
1931 if (!addFiles.isEmpty())
1932 if (!synchronousAdd(repositoryDirectory, false, addFiles))
1935 // Do the final commit
1937 args << QLatin1String("commit")
1938 << QLatin1String("-F") << QDir::toNativeSeparators(messageFile);
1940 args << QLatin1String("--amend");
1941 const QString &authorString = data.authorString();
1942 if (!authorString.isEmpty())
1943 args << QLatin1String("--author") << authorString;
1945 QByteArray outputText;
1946 QByteArray errorText;
1947 const bool rc = fullySynchronousGit(repositoryDirectory, args, &outputText, &errorText);
1949 outputWindow()->append(msgCommitted(amendSHA1, checkedFiles.size()));
1951 outputWindow()->appendError(tr("Unable to commit %n file(s): %1\n", 0, checkedFiles.size()).arg(commandOutputFromLocal8Bit(errorText)));
1956 /* Revert: This function can be called with a file list (to revert single
1957 * files) or a single directory (revert all). Qt Creator currently has only
1958 * 'revert single' in its VCS menus, but the code is prepared to deal with
1959 * reverting a directory pending a sophisticated selection dialog in the
1960 * VCSBase plugin. */
1962 GitClient::RevertResult GitClient::revertI(QStringList files,
1963 bool *ptrToIsDirectory,
1964 QString *errorMessage,
1967 if (Git::Constants::debug)
1968 qDebug() << Q_FUNC_INFO << files;
1971 return RevertCanceled;
1973 // Figure out the working directory
1974 const QFileInfo firstFile(files.front());
1975 const bool isDirectory = firstFile.isDir();
1976 if (ptrToIsDirectory)
1977 *ptrToIsDirectory = isDirectory;
1978 const QString workingDirectory = isDirectory ? firstFile.absoluteFilePath() : firstFile.absolutePath();
1980 const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
1981 if (repoDirectory.isEmpty()) {
1982 *errorMessage = msgRepositoryNotFound(workingDirectory);
1983 return RevertFailed;
1986 // Check for changes
1988 switch (gitStatus(repoDirectory, false, &output, errorMessage)) {
1991 case StatusUnchanged:
1992 return RevertUnchanged;
1994 return RevertFailed;
1997 if (!data.parseFilesFromStatus(output)) {
1998 *errorMessage = msgParseFilesFailed();
1999 return RevertFailed;
2002 // If we are looking at files, make them relative to the repository
2003 // directory to match them in the status output list.
2005 const QDir repoDir(repoDirectory);
2006 const QStringList::iterator cend = files.end();
2007 for (QStringList::iterator it = files.begin(); it != cend; ++it)
2008 *it = repoDir.relativeFilePath(*it);
2011 // From the status output, determine all modified [un]staged files.
2012 const QString modifiedState = QLatin1String("modified");
2013 const QStringList allStagedFiles = data.stagedFileNames(modifiedState);
2014 const QStringList allUnstagedFiles = data.unstagedFileNames(modifiedState);
2015 // Unless a directory was passed, filter all modified files for the
2016 // argument file list.
2017 QStringList stagedFiles = allStagedFiles;
2018 QStringList unstagedFiles = allUnstagedFiles;
2020 const QSet<QString> filesSet = files.toSet();
2021 stagedFiles = allStagedFiles.toSet().intersect(filesSet).toList();
2022 unstagedFiles = allUnstagedFiles.toSet().intersect(filesSet).toList();
2024 if (Git::Constants::debug)
2025 qDebug() << Q_FUNC_INFO << data.stagedFiles << data.unstagedFiles << allStagedFiles << allUnstagedFiles << stagedFiles << unstagedFiles;
2027 if ((!revertStaging || stagedFiles.empty()) && unstagedFiles.empty())
2028 return RevertUnchanged;
2030 // Ask to revert (to do: Handle lists with a selection dialog)
2031 const QMessageBox::StandardButton answer
2032 = QMessageBox::question(m_core->mainWindow(),
2034 tr("The file has been changed. Do you want to revert it?"),
2035 QMessageBox::Yes|QMessageBox::No,
2037 if (answer == QMessageBox::No)
2038 return RevertCanceled;
2040 // Unstage the staged files
2041 if (revertStaging && !stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage))
2042 return RevertFailed;
2043 QStringList filesToRevert = unstagedFiles;
2045 filesToRevert += stagedFiles;
2047 if (!synchronousCheckoutFiles(repoDirectory, filesToRevert, QString(), errorMessage, revertStaging))
2048 return RevertFailed;
2052 void GitClient::revert(const QStringList &files, bool revertStaging)
2055 QString errorMessage;
2056 switch (revertI(files, &isDirectory, &errorMessage, revertStaging)) {
2058 m_plugin->gitVersionControl()->emitFilesChanged(files);
2060 case RevertCanceled:
2062 case RevertUnchanged: {
2063 const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
2064 outputWindow()->append(msg);
2068 outputWindow()->append(errorMessage);
2073 bool GitClient::synchronousFetch(const QString &workingDirectory)
2075 QStringList arguments(QLatin1String("fetch"));
2076 // Disable UNIX terminals to suppress SSH prompting.
2077 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
2078 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
2079 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, arguments, flags);
2080 return resp.result == Utils::SynchronousProcessResponse::Finished;
2083 bool GitClient::synchronousPull(const QString &workingDirectory)
2085 return synchronousPull(workingDirectory, m_settings.pullRebase);
2088 bool GitClient::synchronousPull(const QString &workingDirectory, bool rebase)
2090 QStringList arguments(QLatin1String("pull"));
2092 arguments << QLatin1String("--rebase");
2093 // Disable UNIX terminals to suppress SSH prompting.
2094 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow;
2095 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, arguments, flags);
2096 // Notify about changed files or abort the rebase.
2097 const bool ok = resp.result == Utils::SynchronousProcessResponse::Finished;
2099 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
2102 syncAbortPullRebase(workingDirectory);
2107 void GitClient::syncAbortPullRebase(const QString &workingDir)
2109 // Abort rebase to clean if something goes wrong
2110 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
2111 outwin->appendError(tr("The command 'git pull --rebase' failed, aborting rebase."));
2112 QStringList arguments;
2113 arguments << QLatin1String("rebase") << QLatin1String("--abort");
2116 const bool rc = fullySynchronousGit(workingDir, arguments, &stdOut, &stdErr, true);
2117 outwin->append(commandOutputFromLocal8Bit(stdOut));
2119 outwin->appendError(commandOutputFromLocal8Bit(stdErr));
2122 // Subversion: git svn
2123 void GitClient::synchronousSubversionFetch(const QString &workingDirectory)
2126 args << QLatin1String("svn") << QLatin1String("fetch");
2127 // Disable UNIX terminals to suppress SSH prompting.
2128 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
2129 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
2130 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, args, flags);
2131 // Notify about changes.
2132 if (resp.result == Utils::SynchronousProcessResponse::Finished)
2133 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
2136 void GitClient::subversionLog(const QString &workingDirectory)
2138 if (Git::Constants::debug)
2139 qDebug() << "subversionLog" << workingDirectory;
2141 QStringList arguments;
2142 arguments << QLatin1String("svn") << QLatin1String("log");
2143 if (m_settings.logCount > 0)
2144 arguments << (QLatin1String("--limit=") + QString::number(m_settings.logCount));
2146 // Create a command editor, no highlighting or interaction.
2147 const QString title = tr("Git SVN Log");
2148 const QString editorId = QLatin1String(Git::Constants::C_GIT_COMMAND_LOG_EDITOR);
2149 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, QStringList());
2150 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("svnLog", sourceFile);
2152 editor = createVCSEditor(editorId, title, sourceFile, false, "svnLog", sourceFile, 0);
2153 executeGit(workingDirectory, arguments, editor);
2156 bool GitClient::synchronousPush(const QString &workingDirectory)
2158 // Disable UNIX terminals to suppress SSH prompting.
2159 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
2160 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
2161 const Utils::SynchronousProcessResponse resp =
2162 synchronousGit(workingDirectory, QStringList(QLatin1String("push")), flags);
2163 return resp.result == Utils::SynchronousProcessResponse::Finished;
2166 QString GitClient::msgNoChangedFiles()
2168 return tr("There are no modified files.");
2171 void GitClient::stashPop(const QString &workingDirectory)
2173 QStringList arguments(QLatin1String("stash"));
2174 arguments << QLatin1String("pop");
2175 GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
2176 connectRepositoryChanged(workingDirectory, cmd);
2179 bool GitClient::synchronousStashRestore(const QString &workingDirectory,
2180 const QString &stash,
2181 const QString &branch /* = QString()*/,
2182 QString *errorMessage)
2184 QStringList arguments(QLatin1String("stash"));
2185 if (branch.isEmpty()) {
2186 arguments << QLatin1String("apply") << stash;
2188 arguments << QLatin1String("branch") << branch << stash;
2190 QByteArray outputText;
2191 QByteArray errorText;
2192 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
2194 const QString stdErr = commandOutputFromLocal8Bit(errorText);
2195 const QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory);
2196 const QString msg = branch.isEmpty() ?
2197 tr("Unable to restore stash %1: %2").
2198 arg(nativeWorkingDir, stdErr) :
2199 tr("Unable to restore stash %1 to branch %2: %3").
2200 arg(nativeWorkingDir, branch, stdErr);
2202 *errorMessage = msg;
2204 outputWindow()->append(msg);
2208 QString output = commandOutputFromLocal8Bit(outputText);
2209 if (!output.isEmpty())
2210 outputWindow()->append(output);
2211 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
2215 bool GitClient::synchronousStashRemove(const QString &workingDirectory,
2216 const QString &stash /* = QString() */,
2217 QString *errorMessage /* = 0 */)
2219 QStringList arguments(QLatin1String("stash"));
2220 if (stash.isEmpty()) {
2221 arguments << QLatin1String("clear");
2223 arguments << QLatin1String("drop") << stash;
2225 QByteArray outputText;
2226 QByteArray errorText;
2227 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
2229 const QString stdErr = commandOutputFromLocal8Bit(errorText);
2230 const QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory);
2231 const QString msg = stash.isEmpty() ?
2232 tr("Unable to remove stashes of %1: %2").
2233 arg(nativeWorkingDir, stdErr) :
2234 tr("Unable to remove stash %1 of %2: %3").
2235 arg(stash, nativeWorkingDir, stdErr);
2237 *errorMessage = msg;
2239 outputWindow()->append(msg);
2243 QString output = commandOutputFromLocal8Bit(outputText);
2244 if (!output.isEmpty())
2245 outputWindow()->append(output);
2249 void GitClient::branchList(const QString &workingDirectory)
2251 QStringList arguments(QLatin1String("branch"));
2252 arguments << QLatin1String("-r") << QLatin1String(noColorOption);
2253 executeGit(workingDirectory, arguments, 0, true);
2256 void GitClient::stashList(const QString &workingDirectory)
2258 QStringList arguments(QLatin1String("stash"));
2259 arguments << QLatin1String("list") << QLatin1String(noColorOption);
2260 executeGit(workingDirectory, arguments, 0, true);
2263 bool GitClient::synchronousStashList(const QString &workingDirectory,
2264 QList<Stash> *stashes,
2265 QString *errorMessage /* = 0 */)
2268 QStringList arguments(QLatin1String("stash"));
2269 arguments << QLatin1String("list") << QLatin1String(noColorOption);
2270 QByteArray outputText;
2271 QByteArray errorText;
2272 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
2274 const QString msg = tr("Unable retrieve stash list of %1: %2").
2275 arg(QDir::toNativeSeparators(workingDirectory),
2276 commandOutputFromLocal8Bit(errorText));
2278 *errorMessage = msg;
2280 outputWindow()->append(msg);
2285 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText))
2286 if (stash.parseStashLine(line))
2287 stashes->push_back(stash);
2288 if (Git::Constants::debug)
2289 qDebug() << Q_FUNC_INFO << *stashes;
2293 QString GitClient::readConfig(const QString &workingDirectory, const QStringList &configVar)
2295 QStringList arguments;
2296 arguments << QLatin1String("config") << configVar;
2298 QByteArray outputText;
2299 QByteArray errorText;
2300 if (fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText, false))
2301 return commandOutputFromLocal8Bit(outputText);
2305 // Read a single-line config value, return trimmed
2306 QString GitClient::readConfigValue(const QString &workingDirectory, const QString &configVar)
2308 return readConfig(workingDirectory, QStringList(configVar)).remove(QLatin1Char('\n'));
2311 bool GitClient::cloneRepository(const QString &directory,const QByteArray &url)
2313 QDir workingDirectory(directory);
2314 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt |
2315 VCSBase::VCSBasePlugin::ShowStdOutInLogWindow|
2316 VCSBase::VCSBasePlugin::ShowSuccessMessage;
2318 if (workingDirectory.exists()) {
2319 if (!synchronousInit(workingDirectory.path()))
2322 QStringList arguments(QLatin1String("remote"));
2323 arguments << QLatin1String("add") << QLatin1String("origin") << url;
2324 if (!fullySynchronousGit(workingDirectory.path(), arguments, 0, 0, true))
2328 arguments << QLatin1String("fetch");
2329 const Utils::SynchronousProcessResponse resp =
2330 synchronousGit(workingDirectory.path(), arguments, flags);
2331 if (resp.result != Utils::SynchronousProcessResponse::Finished)
2335 arguments << QLatin1String("config")
2336 << QLatin1String("branch.master.remote")
2337 << QLatin1String("origin");
2338 if (!fullySynchronousGit(workingDirectory.path(), arguments, 0, 0, true))
2342 arguments << QLatin1String("config")
2343 << QLatin1String("branch.master.merge")
2344 << QLatin1String("refs/heads/master");
2345 if (!fullySynchronousGit(workingDirectory.path(), arguments, 0, 0, true))
2350 QStringList arguments(QLatin1String("clone"));
2351 arguments << url << workingDirectory.dirName();
2352 workingDirectory.cdUp();
2353 const Utils::SynchronousProcessResponse resp =
2354 synchronousGit(workingDirectory.path(), arguments, flags);
2355 return resp.result == Utils::SynchronousProcessResponse::Finished;
2359 QString GitClient::vcsGetRepositoryURL(const QString &directory)
2361 QStringList arguments(QLatin1String("config"));
2362 QByteArray outputText;
2364 arguments << QLatin1String("remote.origin.url");
2366 if (fullySynchronousGit(directory, arguments, &outputText, 0, false)) {
2367 return commandOutputFromLocal8Bit(outputText);
2372 GitSettings GitClient::settings() const
2377 void GitClient::setSettings(const GitSettings &s)
2379 if (s != m_settings) {
2381 if (QSettings *coreSettings = m_core->settings())
2382 m_settings.toSettings(coreSettings);
2383 m_binaryPath = m_settings.gitBinaryPath();
2384 m_cachedGitVersion = 0u;
2385 m_hasCachedGitVersion = false;
2389 void GitClient::connectRepositoryChanged(const QString & repository, GitCommand *cmd)
2391 // Bind command success termination with repository to changed signal
2392 if (!m_repositoryChangedSignalMapper) {
2393 m_repositoryChangedSignalMapper = new QSignalMapper(this);
2394 connect(m_repositoryChangedSignalMapper, SIGNAL(mapped(QString)),
2395 m_plugin->gitVersionControl(), SIGNAL(repositoryChanged(QString)));
2397 m_repositoryChangedSignalMapper->setMapping(cmd, repository);
2398 connect(cmd, SIGNAL(success()), m_repositoryChangedSignalMapper, SLOT(map()),
2399 Qt::QueuedConnection);
2402 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
2403 unsigned GitClient::gitVersion(bool silent, QString *errorMessage /* = 0 */)
2405 if (!m_hasCachedGitVersion) {
2406 // Do not execute repeatedly if that fails (due to git
2407 // not being installed) until settings are changed.
2408 m_cachedGitVersion = synchronousGitVersion(silent, errorMessage);
2409 m_hasCachedGitVersion = true;
2411 return m_cachedGitVersion;
2414 QString GitClient::gitVersionString(bool silent, QString *errorMessage)
2416 if (const unsigned version = gitVersion(silent, errorMessage)) {
2418 QTextStream(&rc) << (version >> 16) << '.'
2419 << (0xFF & (version >> 8)) << '.'
2420 << (version & 0xFF);
2426 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
2427 unsigned GitClient::synchronousGitVersion(bool silent, QString *errorMessage /* = 0 */)
2429 // run git --version
2430 QByteArray outputText;
2431 QByteArray errorText;
2432 const bool rc = fullySynchronousGit(QString(), QStringList("--version"), &outputText, &errorText);
2434 const QString msg = tr("Unable to determine git version: %1").arg(commandOutputFromLocal8Bit(errorText));
2436 *errorMessage = msg;
2439 outputWindow()->append(msg);
2441 outputWindow()->appendError(msg);
2446 // cut 'git version 1.6.5.1.sha'
2447 const QString output = commandOutputFromLocal8Bit(outputText);
2448 const QRegExp versionPattern(QLatin1String("^[^\\d]+([\\d])\\.([\\d])\\.([\\d]).*$"));
2449 QTC_ASSERT(versionPattern.isValid(), return 0);
2450 QTC_ASSERT(versionPattern.exactMatch(output), return 0);
2451 const unsigned major = versionPattern.cap(1).toUInt();
2452 const unsigned minor = versionPattern.cap(2).toUInt();
2453 const unsigned patch = versionPattern.cap(3).toUInt();
2454 return version(major, minor, patch);
2457 } // namespace Internal
2460 #include "gitclient.moc"