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 info@qt.nokia.com.
31 **************************************************************************/
33 #include "gitclient.h"
36 #include "commitdata.h"
37 #include "gitconstants.h"
38 #include "gitplugin.h"
39 #include "gitsubmiteditor.h"
40 #include "gitversioncontrol.h"
42 #include <coreplugin/actionmanager/actionmanager.h>
43 #include <coreplugin/coreconstants.h>
44 #include <coreplugin/editormanager/editormanager.h>
45 #include <coreplugin/icore.h>
46 #include <coreplugin/messagemanager.h>
47 #include <coreplugin/progressmanager/progressmanager.h>
48 #include <coreplugin/vcsmanager.h>
49 #include <coreplugin/id.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 <utils/fileutils.h>
59 #include <vcsbase/command.h>
60 #include <vcsbase/vcsbaseeditor.h>
61 #include <vcsbase/vcsbaseeditorparameterwidget.h>
62 #include <vcsbase/vcsbaseoutputwindow.h>
63 #include <vcsbase/vcsbaseplugin.h>
65 #include <QtCore/QRegExp>
66 #include <QtCore/QTemporaryFile>
67 #include <QtCore/QTime>
68 #include <QtCore/QFileInfo>
69 #include <QtCore/QDir>
70 #include <QtCore/QSignalMapper>
72 #include <QtGui/QComboBox>
73 #include <QtGui/QMainWindow> // for msg box parent
74 #include <QtGui/QMessageBox>
75 #include <QtGui/QToolButton>
77 static const char kGitDirectoryC[] = ".git";
78 static const char kBranchIndicatorC[] = "# On branch";
83 class BaseGitDiffArgumentsWidget : public VCSBase::VCSBaseEditorParameterWidget
88 BaseGitDiffArgumentsWidget(GitClient *client, const QString &directory,
89 const QStringList &args) :
90 m_workingDirectory(directory),
94 Q_ASSERT(!directory.isEmpty());
97 mapSetting(addToggleButton(QLatin1String("--patience"), tr("Patience"),
98 tr("Use the patience algorithm for calculating the differences.")),
99 client->settings()->boolPointer(GitSettings::diffPatienceKey));
100 mapSetting(addToggleButton("--ignore-space-change", tr("Ignore Whitespace"),
101 tr("Ignore whitespace only changes.")),
102 m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInDiffKey));
106 QString m_workingDirectory;
111 class GitCommitDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
116 GitCommitDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
117 const QStringList &args, const QStringList &unstaged,
118 const QStringList &staged) :
119 BaseGitDiffArgumentsWidget(client, directory, args),
120 m_unstagedFileNames(unstaged),
121 m_stagedFileNames(staged)
124 void executeCommand()
126 m_client->diff(m_workingDirectory, m_args, m_unstagedFileNames, m_stagedFileNames);
130 const QStringList m_unstagedFileNames;
131 const QStringList m_stagedFileNames;
134 class GitFileDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
138 GitFileDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
139 const QStringList &args, const QString &file) :
140 BaseGitDiffArgumentsWidget(client, directory, args),
144 void executeCommand()
146 m_client->diff(m_workingDirectory, m_args, m_fileName);
150 const QString m_fileName;
153 class GitBranchDiffArgumentsWidget : public BaseGitDiffArgumentsWidget
157 GitBranchDiffArgumentsWidget(Git::Internal::GitClient *client, const QString &directory,
158 const QStringList &args, const QString &branch) :
159 BaseGitDiffArgumentsWidget(client, directory, args),
165 m_client->diffBranch(m_workingDirectory, m_args, m_branchName);
169 const QString m_branchName;
172 class GitShowArgumentsWidget : public VCSBase::VCSBaseEditorParameterWidget
177 GitShowArgumentsWidget(Git::Internal::GitClient *client,
178 const QString &directory,
179 const QStringList &args,
182 m_workingDirectory(directory),
186 QList<ComboBoxItem> prettyChoices;
187 prettyChoices << ComboBoxItem(tr("oneline"), QLatin1String("oneline"))
188 << ComboBoxItem(tr("short"), QLatin1String("short"))
189 << ComboBoxItem(tr("medium"), QLatin1String("medium"))
190 << ComboBoxItem(tr("full"), QLatin1String("full"))
191 << ComboBoxItem(tr("fuller"), QLatin1String("fuller"))
192 << ComboBoxItem(tr("email"), QLatin1String("email"))
193 << ComboBoxItem(tr("raw"), QLatin1String("raw"));
194 mapSetting(addComboBox(QLatin1String("--pretty"), prettyChoices),
195 m_client->settings()->intPointer(GitSettings::showPrettyFormatKey));
198 void executeCommand()
200 m_client->show(m_workingDirectory, m_id, m_args);
205 QString m_workingDirectory;
210 class GitBlameArgumentsWidget : public VCSBase::VCSBaseEditorParameterWidget
215 GitBlameArgumentsWidget(Git::Internal::GitClient *client,
216 const QString &directory,
217 const QStringList &args,
218 const QString &revision, const QString &fileName) :
221 m_workingDirectory(directory),
223 m_revision(revision),
226 mapSetting(addToggleButton(QString(), tr("Omit Date"),
227 tr("Hide the date of a change from the output.")),
228 m_client->settings()->boolPointer(GitSettings::omitAnnotationDateKey));
229 mapSetting(addToggleButton(QString("-w"), tr("Ignore Whitespace"),
230 tr("Ignore whitespace only changes.")),
231 m_client->settings()->boolPointer(GitSettings::ignoreSpaceChangesInBlameKey));
234 void setEditor(VCSBase::VCSBaseEditorWidget *editor)
240 void executeCommand()
244 line = m_editor->lineNumberOfCurrentEditor();
245 m_client->blame(m_workingDirectory, m_args, m_fileName, m_revision, line);
249 VCSBase::VCSBaseEditorWidget *m_editor;
251 QString m_workingDirectory;
257 inline Core::IEditor* locateEditor(const Core::ICore *core, const char *property, const QString &entry)
259 foreach (Core::IEditor *ed, core->editorManager()->openedEditors())
260 if (ed->file()->property(property).toString() == entry)
265 // Return converted command output, remove '\r' read on Windows
266 static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
268 QString output = QString::fromLocal8Bit(a);
269 output.remove(QLatin1Char('\r'));
273 // Return converted command output split into lines
274 static inline QStringList commandOutputLinesFromLocal8Bit(const QByteArray &a)
276 QString output = commandOutputFromLocal8Bit(a);
277 const QChar newLine = QLatin1Char('\n');
278 if (output.endsWith(newLine))
279 output.truncate(output.size() - 1);
280 if (output.isEmpty())
281 return QStringList();
282 return output.split(newLine);
285 static inline VCSBase::VCSBaseOutputWindow *outputWindow()
287 return VCSBase::VCSBaseOutputWindow::instance();
290 static inline QString msgRepositoryNotFound(const QString &dir)
292 return GitClient::tr("Cannot determine the repository for \"%1\".").arg(dir);
295 static inline QString msgParseFilesFailed()
297 return GitClient::tr("Cannot parse the file output.");
300 // ---------------- GitClient
302 const char *GitClient::stashNamePrefix = "stash@{";
304 GitClient::GitClient(GitSettings *settings) :
305 m_cachedGitVersion(0),
306 m_msgWait(tr("Waiting for data...")),
307 m_core(Core::ICore::instance()),
308 m_repositoryChangedSignalMapper(0),
312 connect(m_core, SIGNAL(saveSettingsRequested()), this, SLOT(saveSettings()));
315 GitClient::~GitClient()
319 const char *GitClient::noColorOption = "--no-color";
320 const char *GitClient::decorateOption = "--decorate";
322 QString GitClient::findRepositoryForDirectory(const QString &dir)
324 // Check for ".git/config"
325 const QString checkFile = QLatin1String(kGitDirectoryC) + QLatin1String("/config");
326 return VCSBase::VCSBasePlugin::findRepositoryForDirectory(dir, checkFile);
329 VCSBase::VCSBaseEditorWidget *GitClient::findExistingVCSEditor(const char *registerDynamicProperty,
330 const QString &dynamicPropertyValue) const
332 VCSBase::VCSBaseEditorWidget *rc = 0;
333 Core::IEditor *outputEditor = locateEditor(m_core, registerDynamicProperty, dynamicPropertyValue);
338 Core::EditorManager::instance()->activateEditor(outputEditor, Core::EditorManager::ModeSwitch);
339 outputEditor->createNew(m_msgWait);
340 rc = VCSBase::VCSBaseEditorWidget::getVcsBaseEditor(outputEditor);
345 /* Create an editor associated to VCS output of a source file/directory
346 * (using the file's codec). Makes use of a dynamic property to find an
347 * existing instance and to reuse it (in case, say, 'git diff foo' is
349 VCSBase::VCSBaseEditorWidget *GitClient::createVCSEditor(const QString &id,
351 // Source file or directory
352 const QString &source,
354 // Dynamic property and value to identify that editor
355 const char *registerDynamicProperty,
356 const QString &dynamicPropertyValue,
357 QWidget *configWidget) const
359 VCSBase::VCSBaseEditorWidget *rc = 0;
360 Q_ASSERT(!findExistingVCSEditor(registerDynamicProperty, dynamicPropertyValue));
362 // Create new, set wait message, set up with source and codec
363 Core::IEditor *outputEditor = m_core->editorManager()->openEditorWithContents(id, &title, m_msgWait);
364 outputEditor->file()->setProperty(registerDynamicProperty, dynamicPropertyValue);
365 rc = VCSBase::VCSBaseEditorWidget::getVcsBaseEditor(outputEditor);
366 connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,int)),
367 this, SLOT(slotBlameRevisionRequested(QString,QString,int)));
368 QTC_ASSERT(rc, return 0);
369 rc->setSource(source);
371 rc->setCodec(VCSBase::VCSBaseEditorWidget::getCodec(source));
373 rc->setForceReadOnly(true);
374 m_core->editorManager()->activateEditor(outputEditor, Core::EditorManager::ModeSwitch);
377 rc->setConfigurationWidget(configWidget);
382 void GitClient::diff(const QString &workingDirectory,
383 const QStringList &diffArgs,
384 const QStringList &unstagedFileNames,
385 const QStringList &stagedFileNames)
387 const QString binary = settings()->stringValue(GitSettings::binaryPathKey);
388 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
389 const QString title = tr("Git Diff");
391 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", workingDirectory);
393 GitCommitDiffArgumentsWidget *argWidget =
394 new GitCommitDiffArgumentsWidget(this, workingDirectory, diffArgs,
395 unstagedFileNames, stagedFileNames);
397 editor = createVCSEditor(editorId, title,
398 workingDirectory, true, "originalFileName", workingDirectory, argWidget);
399 connect(editor, SIGNAL(diffChunkReverted(VCSBase::DiffChunk)), argWidget, SLOT(executeCommand()));
400 editor->setRevertDiffChunkEnabled(true);
403 GitCommitDiffArgumentsWidget *argWidget = qobject_cast<GitCommitDiffArgumentsWidget *>(editor->configurationWidget());
404 QStringList userDiffArgs = argWidget->arguments();
405 editor->setDiffBaseDirectory(workingDirectory);
407 // Create a batch of 2 commands to be run after each other in case
408 // we have a mixture of staged/unstaged files as is the case
409 // when using the submit dialog.
410 VCSBase::Command *command = createCommand(workingDirectory, editor);
414 cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption);
416 int timeout = settings()->intValue(GitSettings::timeoutKey);
418 if (unstagedFileNames.empty() && stagedFileNames.empty()) {
419 QStringList arguments(cmdArgs);
420 arguments << userDiffArgs;
421 outputWindow()->appendCommand(workingDirectory, binary, arguments);
422 command->addJob(arguments, timeout);
425 if (!unstagedFileNames.empty()) {
426 QStringList arguments(cmdArgs);
427 arguments << userDiffArgs;
428 arguments << QLatin1String("--") << unstagedFileNames;
429 outputWindow()->appendCommand(workingDirectory, binary, arguments);
430 command->addJob(arguments, timeout);
432 if (!stagedFileNames.empty()) {
433 QStringList arguments(cmdArgs);
434 arguments << userDiffArgs;
435 arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
436 outputWindow()->appendCommand(workingDirectory, binary, arguments);
437 command->addJob(arguments, timeout);
443 void GitClient::diff(const QString &workingDirectory,
444 const QStringList &diffArgs,
445 const QString &fileName)
447 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
448 const QString title = tr("Git Diff \"%1\"").arg(fileName);
449 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, fileName);
451 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("originalFileName", sourceFile);
453 GitFileDiffArgumentsWidget *argWidget =
454 new GitFileDiffArgumentsWidget(this, workingDirectory, diffArgs, fileName);
456 editor = createVCSEditor(editorId, title, sourceFile, true, "originalFileName", sourceFile, argWidget);
457 connect(editor, SIGNAL(diffChunkReverted(VCSBase::DiffChunk)), argWidget, SLOT(executeCommand()));
458 editor->setRevertDiffChunkEnabled(true);
461 GitFileDiffArgumentsWidget *argWidget = qobject_cast<GitFileDiffArgumentsWidget *>(editor->configurationWidget());
462 QStringList userDiffArgs = argWidget->arguments();
465 cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
468 if (!fileName.isEmpty())
469 cmdArgs << QLatin1String("--") << fileName;
470 executeGit(workingDirectory, cmdArgs, editor);
473 void GitClient::diffBranch(const QString &workingDirectory,
474 const QStringList &diffArgs,
475 const QString &branchName)
477 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
478 const QString title = tr("Git Diff Branch \"%1\"").arg(branchName);
479 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, QStringList());
481 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("BranchName", branchName);
483 editor = createVCSEditor(editorId, title, sourceFile, true, "BranchName", branchName,
484 new GitBranchDiffArgumentsWidget(this, workingDirectory,
485 diffArgs, branchName));
487 GitBranchDiffArgumentsWidget *argWidget = qobject_cast<GitBranchDiffArgumentsWidget *>(editor->configurationWidget());
488 QStringList userDiffArgs = argWidget->arguments();
491 cmdArgs << QLatin1String("diff") << QLatin1String(noColorOption)
492 << userDiffArgs << branchName;
494 executeGit(workingDirectory, cmdArgs, editor);
497 void GitClient::status(const QString &workingDirectory)
499 // @TODO: Use "--no-color" once it is supported
500 QStringList statusArgs(QLatin1String("status"));
501 statusArgs << QLatin1String("-u");
502 VCSBase::VCSBaseOutputWindow *outwin = outputWindow();
503 outwin->setRepository(workingDirectory);
504 VCSBase::Command *command = executeGit(workingDirectory, statusArgs, 0, true);
505 connect(command, SIGNAL(finished(bool,int,QVariant)), outwin, SLOT(clearRepository()),
506 Qt::QueuedConnection);
509 static const char graphLogFormatC[] = "%h %d %an %s %ci";
511 // Create a graphical log.
512 void GitClient::graphLog(const QString &workingDirectory, const QString & branch)
514 QStringList arguments;
515 arguments << QLatin1String("log") << QLatin1String(noColorOption);
517 int logCount = settings()->intValue(GitSettings::logCountKey);
519 arguments << QLatin1String("-n") << QString::number(logCount);
520 arguments << (QLatin1String("--pretty=format:") + QLatin1String(graphLogFormatC))
521 << QLatin1String("--topo-order") << QLatin1String("--graph");
524 if (branch.isEmpty()) {
525 title = tr("Git Log");
527 title = tr("Git Log \"%1\"").arg(branch);
530 const QString editorId = QLatin1String(Git::Constants::GIT_LOG_EDITOR_ID);
531 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, QStringList());
532 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
534 editor = createVCSEditor(editorId, title, sourceFile, false, "logFileName", sourceFile, 0);
535 executeGit(workingDirectory, arguments, editor);
538 void GitClient::log(const QString &workingDirectory, const QStringList &fileNames,
539 bool enableAnnotationContextMenu)
541 QStringList arguments;
542 arguments << QLatin1String("log") << QLatin1String(noColorOption)
543 << QLatin1String(decorateOption);
545 int logCount = settings()->intValue(GitSettings::logCountKey);
547 arguments << QLatin1String("-n") << QString::number(logCount);
549 if (!fileNames.isEmpty())
550 arguments.append(fileNames);
552 const QString msgArg = fileNames.empty() ? workingDirectory :
553 fileNames.join(QString(", "));
554 const QString title = tr("Git Log \"%1\"").arg(msgArg);
555 const QString editorId = QLatin1String(Git::Constants::GIT_LOG_EDITOR_ID);
556 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, fileNames);
557 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
559 editor = createVCSEditor(editorId, title, sourceFile, false, "logFileName", sourceFile, 0);
560 editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
561 executeGit(workingDirectory, arguments, editor);
564 // Do not show "0000" or "^32ae4"
565 static inline bool canShow(const QString &sha)
567 if (sha.startsWith(QLatin1Char('^')))
569 if (sha.count(QLatin1Char('0')) == sha.size())
574 static inline QString msgCannotShow(const QString &sha)
576 return GitClient::tr("Cannot describe \"%1\".").arg(sha);
579 void GitClient::show(const QString &source, const QString &id, const QStringList &args)
582 outputWindow()->append(msgCannotShow(id));
586 const QString title = tr("Git Show \"%1\"").arg(id);
587 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
588 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("show", id);
590 editor = createVCSEditor(editorId, title, source, true, "show", id,
591 new GitShowArgumentsWidget(this, source, args, id));
593 GitShowArgumentsWidget *argWidget = qobject_cast<GitShowArgumentsWidget *>(editor->configurationWidget());
594 QStringList userArgs = argWidget->arguments();
596 QStringList arguments;
597 arguments << QLatin1String("show") << QLatin1String(noColorOption);
598 arguments << QLatin1String(decorateOption);
599 arguments.append(userArgs);
602 const QFileInfo sourceFi(source);
603 const QString workDir = sourceFi.isDir() ? sourceFi.absoluteFilePath() : sourceFi.absolutePath();
604 executeGit(workDir, arguments, editor);
607 void GitClient::saveSettings()
609 settings()->writeSettings(m_core->settings());
612 void GitClient::slotBlameRevisionRequested(const QString &source, QString change, int lineNumber)
614 // This might be invoked with a verbose revision description
615 // "SHA1 author subject" from the annotation context menu. Strip the rest.
616 const int blankPos = change.indexOf(QLatin1Char(' '));
618 change.truncate(blankPos);
619 const QFileInfo fi(source);
620 blame(fi.absolutePath(), QStringList(), fi.fileName(), change, lineNumber);
623 void GitClient::blame(const QString &workingDirectory,
624 const QStringList &args,
625 const QString &fileName,
626 const QString &revision,
629 const QString editorId = QLatin1String(Git::Constants::GIT_BLAME_EDITOR_ID);
630 const QString id = VCSBase::VCSBaseEditorWidget::getTitleId(workingDirectory, QStringList(fileName), revision);
631 const QString title = tr("Git Blame \"%1\"").arg(id);
632 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, fileName);
634 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("blameFileName", id);
636 GitBlameArgumentsWidget *argWidget =
637 new GitBlameArgumentsWidget(this, workingDirectory, args,
639 editor = createVCSEditor(editorId, title, sourceFile, true, "blameFileName", id, argWidget);
640 argWidget->setEditor(editor);
643 GitBlameArgumentsWidget *argWidget = qobject_cast<GitBlameArgumentsWidget *>(editor->configurationWidget());
644 QStringList userBlameArgs = argWidget->arguments();
646 QStringList arguments(QLatin1String("blame"));
647 arguments << QLatin1String("--root");
648 arguments.append(userBlameArgs);
649 arguments << QLatin1String("--") << fileName;
650 if (!revision.isEmpty())
651 arguments << revision;
652 executeGit(workingDirectory, arguments, editor, false, VCSBase::Command::NoReport, lineNumber);
655 void GitClient::checkoutBranch(const QString &workingDirectory, const QString &branch)
657 QStringList arguments(QLatin1String("checkout"));
659 VCSBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true);
660 connectRepositoryChanged(workingDirectory, cmd);
663 bool GitClient::synchronousCheckoutBranch(const QString &workingDirectory,
664 const QString &branch,
665 QString *errorMessage /* = 0 */)
667 QByteArray outputText;
668 QByteArray errorText;
669 QStringList arguments;
670 arguments << QLatin1String("checkout") << branch;
671 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
672 const QString output = commandOutputFromLocal8Bit(outputText);
673 outputWindow()->append(output);
675 const QString stdErr = commandOutputFromLocal8Bit(errorText);
676 //: Meaning of the arguments: %1: Branch, %2: Repository, %3: Error message
677 const QString msg = tr("Cannot checkout \"%1\" of \"%2\": %3").arg(branch, workingDirectory, stdErr);
681 outputWindow()->appendError(msg);
688 void GitClient::checkout(const QString &workingDirectory, const QString &fileName)
690 // Passing an empty argument as the file name is very dangereous, since this makes
691 // git checkout apply to all files. Almost looks like a bug in git.
692 if (fileName.isEmpty())
695 QStringList arguments;
696 arguments << QLatin1String("checkout") << QLatin1String("HEAD") << QLatin1String("--")
699 executeGit(workingDirectory, arguments, 0, true);
702 void GitClient::hardReset(const QString &workingDirectory, const QString &commit)
704 QStringList arguments;
705 arguments << QLatin1String("reset") << QLatin1String("--hard");
706 if (!commit.isEmpty())
709 VCSBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true);
710 connectRepositoryChanged(workingDirectory, cmd);
713 void GitClient::addFile(const QString &workingDirectory, const QString &fileName)
715 QStringList arguments;
716 arguments << QLatin1String("add") << fileName;
718 executeGit(workingDirectory, arguments, 0, true);
721 // Warning: 'intendToAdd' works only from 1.6.1 onwards
722 bool GitClient::synchronousAdd(const QString &workingDirectory,
724 const QStringList &files)
726 QByteArray outputText;
727 QByteArray errorText;
728 QStringList arguments;
729 arguments << QLatin1String("add");
731 arguments << QLatin1String("--intent-to-add");
732 arguments.append(files);
733 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
735 const QString errorMessage = tr("Cannot add %n file(s) to \"%1\": %2", 0, files.size()).
736 arg(QDir::toNativeSeparators(workingDirectory),
737 commandOutputFromLocal8Bit(errorText));
738 outputWindow()->appendError(errorMessage);
743 bool GitClient::synchronousDelete(const QString &workingDirectory,
745 const QStringList &files)
747 QByteArray outputText;
748 QByteArray errorText;
749 QStringList arguments;
750 arguments << QLatin1String("rm");
752 arguments << QLatin1String("--force");
753 arguments.append(files);
754 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
756 const QString errorMessage = tr("Cannot remove %n file(s) from \"%1\": %2", 0, files.size()).
757 arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
758 outputWindow()->appendError(errorMessage);
763 bool GitClient::synchronousMove(const QString &workingDirectory,
767 QByteArray outputText;
768 QByteArray errorText;
769 QStringList arguments;
770 arguments << QLatin1String("mv");
773 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
775 const QString errorMessage = tr("Cannot move from \"%1\" to \"%2\": %3").
776 arg(from, to, commandOutputFromLocal8Bit(errorText));
777 outputWindow()->appendError(errorMessage);
782 bool GitClient::synchronousReset(const QString &workingDirectory,
783 const QStringList &files,
784 QString *errorMessage)
786 QByteArray outputText;
787 QByteArray errorText;
788 QStringList arguments;
789 arguments << QLatin1String("reset");
791 arguments << QLatin1String("--hard");
793 arguments << QLatin1String("HEAD") << QLatin1String("--") << files;
794 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
795 const QString output = commandOutputFromLocal8Bit(outputText);
796 outputWindow()->append(output);
797 // Note that git exits with 1 even if the operation is successful
798 // Assume real failure if the output does not contain "foo.cpp modified"
799 // or "Unstaged changes after reset" (git 1.7.0).
800 if (!rc && (!output.contains(QLatin1String("modified"))
801 && !output.contains(QLatin1String("Unstaged changes after reset")))) {
802 const QString stdErr = commandOutputFromLocal8Bit(errorText);
803 const QString msg = files.isEmpty() ?
804 tr("Cannot reset \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), stdErr) :
805 tr("Cannot reset %n file(s) in \"%1\": %2", 0, files.size()).
806 arg(QDir::toNativeSeparators(workingDirectory), stdErr);
810 outputWindow()->appendError(msg);
817 // Initialize repository
818 bool GitClient::synchronousInit(const QString &workingDirectory)
820 QByteArray outputText;
821 QByteArray errorText;
822 const QStringList arguments(QLatin1String("init"));
823 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
824 // '[Re]Initialized...'
825 outputWindow()->append(commandOutputFromLocal8Bit(outputText));
827 outputWindow()->appendError(commandOutputFromLocal8Bit(errorText));
829 // TODO: Turn this into a VCSBaseClient and use resetCachedVcsInfo(...)
830 Core::VcsManager *vcsManager = m_core->vcsManager();
831 vcsManager->resetVersionControlForDirectory(workingDirectory);
836 /* Checkout, supports:
837 * git checkout -- <files>
838 * git checkout revision -- <files>
839 * git checkout revision -- . */
840 bool GitClient::synchronousCheckoutFiles(const QString &workingDirectory,
841 QStringList files /* = QStringList() */,
842 QString revision /* = QString() */,
843 QString *errorMessage /* = 0 */,
844 bool revertStaging /* = true */)
846 if (revertStaging && revision.isEmpty())
847 revision = QLatin1String("HEAD");
849 files = QStringList(QString(QLatin1Char('.')));
850 QByteArray outputText;
851 QByteArray errorText;
852 QStringList arguments;
853 arguments << QLatin1String("checkout");
855 arguments << revision;
856 arguments << QLatin1String("--") << files;
857 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
859 const QString fileArg = files.join(QLatin1String(", "));
860 //: Meaning of the arguments: %1: revision, %2: files, %3: repository,
861 //: %4: Error message
862 const QString msg = tr("Cannot checkout \"%1\" of %2 in \"%3\": %4").
863 arg(revision, fileArg, workingDirectory, commandOutputFromLocal8Bit(errorText));
867 outputWindow()->appendError(msg);
874 static inline QString msgParentRevisionFailed(const QString &workingDirectory,
875 const QString &revision,
878 //: Failed to find parent revisions of a SHA1 for "annotate previous"
879 return GitClient::tr("Cannot find parent revisions of \"%1\" in \"%2\": %3").arg(revision, workingDirectory, why);
882 static inline QString msgInvalidRevision()
884 return GitClient::tr("Invalid revision");
887 // Split a line of "<commit> <parent1> ..." to obtain parents from "rev-list" or "log".
888 static inline bool splitCommitParents(const QString &line,
890 QStringList *parents = 0)
896 QStringList tokens = line.trimmed().split(QLatin1Char(' '));
897 if (tokens.size() < 2)
900 *commit = tokens.front();
907 // Find out the immediate parent revisions of a revision of the repository.
908 // Might be several in case of merges.
909 bool GitClient::synchronousParentRevisions(const QString &workingDirectory,
910 const QStringList &files /* = QStringList() */,
911 const QString &revision,
912 QStringList *parents,
913 QString *errorMessage)
915 QByteArray outputTextData;
916 QByteArray errorText;
917 QStringList arguments;
918 arguments << QLatin1String("rev-list") << QLatin1String(GitClient::noColorOption)
919 << QLatin1String("--parents") << QLatin1String("--max-count=1") << revision;
920 if (!files.isEmpty()) {
921 arguments.append(QLatin1String("--"));
922 arguments.append(files);
924 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
926 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, commandOutputFromLocal8Bit(errorText));
929 // Should result in one line of blank-delimited revisions, specifying current first
931 QString outputText = commandOutputFromLocal8Bit(outputTextData);
932 outputText.remove(QLatin1Char('\n'));
933 if (!splitCommitParents(outputText, 0, parents)) {
934 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, msgInvalidRevision());
940 // Short SHA1, author, subject
941 static const char defaultShortLogFormatC[] = "%h (%an \"%s\")";
943 bool GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision,
944 QString *description, QString *errorMessage)
946 // Short SHA 1, author, subject
947 return synchronousShortDescription(workingDirectory, revision,
948 QLatin1String(defaultShortLogFormatC),
949 description, errorMessage);
952 // Convenience working on a list of revisions
953 bool GitClient::synchronousShortDescriptions(const QString &workingDirectory, const QStringList &revisions,
954 QStringList *descriptions, QString *errorMessage)
956 descriptions->clear();
957 foreach (const QString &revision, revisions) {
959 if (!synchronousShortDescription(workingDirectory, revision, &description, errorMessage)) {
960 descriptions->clear();
963 descriptions->push_back(description);
968 static inline QString msgCannotDetermineBranch(const QString &workingDirectory, const QString &why)
970 return GitClient::tr("Cannot retrieve branch of \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), why);
973 // Retrieve head revision/branch
974 bool GitClient::synchronousTopRevision(const QString &workingDirectory, QString *revision,
975 QString *branch, QString *errorMessageIn)
977 QByteArray outputTextData;
978 QByteArray errorText;
979 QStringList arguments;
980 QString errorMessage;
985 arguments << QLatin1String("log") << QLatin1String(noColorOption)
986 << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:%H");
987 if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
988 errorMessage = tr("Cannot retrieve top revision of \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
991 *revision = commandOutputFromLocal8Bit(outputTextData);
992 revision->remove(QLatin1Char('\n'));
993 } // revision desired
998 arguments << QLatin1String("branch") << QLatin1String(noColorOption);
999 if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
1000 errorMessage = msgCannotDetermineBranch(workingDirectory, commandOutputFromLocal8Bit(errorText));
1003 /* parse output for current branch: \code
1007 const QString branchPrefix = QLatin1String("* ");
1008 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputTextData)) {
1009 if (line.startsWith(branchPrefix)) {
1011 branch->remove(0, branchPrefix.size());
1015 if (branch->isEmpty()) {
1016 errorMessage = msgCannotDetermineBranch(workingDirectory,
1017 QString::fromLatin1("Internal error: Failed to parse output: %1").arg(commandOutputFromLocal8Bit(outputTextData)));
1022 const bool failed = (revision && revision->isEmpty()) || (branch && branch->isEmpty());
1023 if (failed && !errorMessage.isEmpty()) {
1024 if (errorMessageIn) {
1025 *errorMessageIn = errorMessage;
1027 outputWindow()->appendError(errorMessage);
1033 // Format an entry in a one-liner for selection list using git log.
1034 bool GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision,
1035 const QString &format, QString *description,
1036 QString *errorMessage)
1038 QByteArray outputTextData;
1039 QByteArray errorText;
1040 QStringList arguments;
1041 arguments << QLatin1String("log") << QLatin1String(GitClient::noColorOption)
1042 << (QLatin1String("--pretty=format:") + format)
1043 << QLatin1String("--max-count=1") << revision;
1044 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
1046 *errorMessage = tr("Cannot describe revision \"%1\" in \"%2\": %3").arg(revision, workingDirectory, commandOutputFromLocal8Bit(errorText));
1049 *description = commandOutputFromLocal8Bit(outputTextData);
1050 if (description->endsWith(QLatin1Char('\n')))
1051 description->truncate(description->size() - 1);
1055 // Create a default message to be used for describing stashes
1056 static inline QString creatorStashMessage(const QString &keyword = QString())
1058 QString rc = QCoreApplication::applicationName();
1059 rc += QLatin1Char(' ');
1060 if (!keyword.isEmpty()) {
1062 rc += QLatin1Char(' ');
1064 rc += QDateTime::currentDateTime().toString(Qt::ISODate);
1068 /* Do a stash and return the message as identifier. Note that stash names (stash{n})
1069 * shift as they are pushed, so, enforce the use of messages to identify them. Flags:
1070 * StashPromptDescription: Prompt the user for a description message.
1071 * StashImmediateRestore: Immediately re-apply this stash (used for snapshots), user keeps on working
1072 * StashIgnoreUnchanged: Be quiet about unchanged repositories (used for IVersionControl's snapshots). */
1074 QString GitClient::synchronousStash(const QString &workingDirectory, const QString &messageKeyword,
1075 unsigned flags, bool *unchanged)
1080 bool success = false;
1081 // Check for changes and stash
1082 QString errorMessage;
1083 switch (gitStatus(workingDirectory, false, 0, &errorMessage)) {
1084 case StatusChanged: {
1085 message = creatorStashMessage(messageKeyword);
1087 if ((flags & StashPromptDescription)) {
1088 if (!inputText(Core::ICore::instance()->mainWindow(),
1089 tr("Stash Description"), tr("Description:"), &message))
1092 if (!executeSynchronousStash(workingDirectory, message))
1094 if ((flags & StashImmediateRestore)
1095 && !synchronousStashRestore(workingDirectory, QLatin1String("stash@{0}")))
1101 case StatusUnchanged:
1104 if (!(flags & StashIgnoreUnchanged))
1105 outputWindow()->append(msgNoChangedFiles());
1108 outputWindow()->append(errorMessage);
1116 bool GitClient::executeSynchronousStash(const QString &workingDirectory,
1117 const QString &message,
1118 QString *errorMessage)
1120 QByteArray outputText;
1121 QByteArray errorText;
1122 QStringList arguments;
1123 arguments << QLatin1String("stash");
1124 if (!message.isEmpty())
1125 arguments << QLatin1String("save") << message;
1126 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
1128 const QString msg = tr("Cannot stash in \"%1\": %2").
1129 arg(QDir::toNativeSeparators(workingDirectory),
1130 commandOutputFromLocal8Bit(errorText));
1132 *errorMessage = msg;
1134 outputWindow()->append(msg);
1141 // Resolve a stash name from message
1142 bool GitClient::stashNameFromMessage(const QString &workingDirectory,
1143 const QString &message, QString *name,
1144 QString *errorMessage)
1147 if (message.startsWith(QLatin1String(stashNamePrefix))) {
1151 // Retrieve list and find via message
1152 QList<Stash> stashes;
1153 if (!synchronousStashList(workingDirectory, &stashes, errorMessage))
1155 foreach (const Stash &s, stashes) {
1156 if (s.message == message) {
1161 //: Look-up of a stash via its descriptive message failed.
1162 const QString msg = tr("Cannot resolve stash message \"%1\" in \"%2\".").arg(message, workingDirectory);
1164 *errorMessage = msg;
1166 outputWindow()->append(msg);
1171 bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs,
1172 QString *output, QString *errorMessage)
1174 branchArgs.push_front(QLatin1String("branch"));
1175 QByteArray outputText;
1176 QByteArray errorText;
1177 const bool rc = fullySynchronousGit(workingDirectory, branchArgs, &outputText, &errorText);
1179 *errorMessage = tr("Cannot run \"git branch\" in \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1182 *output = commandOutputFromLocal8Bit(outputText);
1186 bool GitClient::synchronousRemoteCmd(const QString &workingDirectory, QStringList remoteArgs,
1187 QString *output, QString *errorMessage)
1189 remoteArgs.push_front(QLatin1String("remote"));
1190 QByteArray outputText;
1191 QByteArray errorText;
1192 const bool rc = fullySynchronousGit(workingDirectory, remoteArgs, &outputText, &errorText);
1194 *errorMessage = tr("Cannot run \"git remote\" in \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1197 *output = commandOutputFromLocal8Bit(outputText);
1201 bool GitClient::synchronousShow(const QString &workingDirectory, const QString &id,
1202 QString *output, QString *errorMessage)
1205 *errorMessage = msgCannotShow(id);
1208 QStringList args(QLatin1String("show"));
1209 args << QLatin1String(decorateOption) << QLatin1String(noColorOption) << id;
1210 QByteArray outputText;
1211 QByteArray errorText;
1212 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1214 *errorMessage = tr("Cannot run \"git show\" in \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1217 *output = commandOutputFromLocal8Bit(outputText);
1221 // Retrieve list of files to be cleaned
1222 bool GitClient::synchronousCleanList(const QString &workingDirectory,
1223 QStringList *files, QString *errorMessage)
1227 args << QLatin1String("clean") << QLatin1String("--dry-run") << QLatin1String("-dxf");
1228 QByteArray outputText;
1229 QByteArray errorText;
1230 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1232 *errorMessage = tr("Cannot run \"git clean\" in \"%1\": %2").arg(QDir::toNativeSeparators(workingDirectory), commandOutputFromLocal8Bit(errorText));
1235 // Filter files that git would remove
1236 const QString prefix = QLatin1String("Would remove ");
1237 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText))
1238 if (line.startsWith(prefix))
1239 files->push_back(line.mid(prefix.size()));
1243 bool GitClient::synchronousApplyPatch(const QString &workingDirectory,
1244 const QString &file, QString *errorMessage)
1247 args << QLatin1String("apply") << QLatin1String("--whitespace=fix") << file;
1248 QByteArray outputText;
1249 QByteArray errorText;
1250 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1252 if (!errorText.isEmpty())
1253 *errorMessage = tr("There were warnings while applying \"%1\" to \"%2\":\n%3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
1255 *errorMessage = tr("Cannot apply patch \"%1\" to \"%2\": %3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
1261 // Factory function to create an asynchronous command
1262 VCSBase::Command *GitClient::createCommand(const QString &workingDirectory,
1263 VCSBase::VCSBaseEditorWidget* editor,
1264 bool useOutputToWindow,
1265 int editorLineNumber)
1267 VCSBase::Command *command = new VCSBase::Command(gitBinaryPath(), workingDirectory, processEnvironment());
1268 command->setCookie(QVariant(editorLineNumber));
1270 connect(command, SIGNAL(finished(bool,int,QVariant)), editor, SLOT(commandFinishedGotoLine(bool,int,QVariant)));
1271 if (useOutputToWindow) {
1272 if (editor) // assume that the commands output is the important thing
1273 connect(command, SIGNAL(outputData(QByteArray)), outputWindow(), SLOT(appendDataSilently(QByteArray)));
1275 connect(command, SIGNAL(outputData(QByteArray)), outputWindow(), SLOT(appendData(QByteArray)));
1278 connect(command, SIGNAL(outputData(QByteArray)), editor, SLOT(setPlainTextDataFiltered(QByteArray)));
1282 connect(command, SIGNAL(errorText(QString)), outputWindow(), SLOT(appendError(QString)));
1286 // Execute a single command
1287 VCSBase::Command *GitClient::executeGit(const QString &workingDirectory,
1288 const QStringList &arguments,
1289 VCSBase::VCSBaseEditorWidget* editor,
1290 bool useOutputToWindow,
1291 VCSBase::Command::TerminationReportMode tm,
1292 int editorLineNumber,
1293 bool unixTerminalDisabled)
1295 outputWindow()->appendCommand(workingDirectory, settings()->stringValue(GitSettings::binaryPathKey), arguments);
1296 VCSBase::Command *command = createCommand(workingDirectory, editor, useOutputToWindow, editorLineNumber);
1297 command->addJob(arguments, settings()->intValue(GitSettings::timeoutKey));
1298 command->setTerminationReportMode(tm);
1299 command->setUnixTerminalDisabled(unixTerminalDisabled);
1304 QProcessEnvironment GitClient::processEnvironment() const
1307 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
1308 if (settings()->boolValue(GitSettings::adoptPathKey))
1309 environment.insert(QLatin1String("PATH"), settings()->stringValue(GitSettings::pathKey));
1311 if (settings()->boolValue(GitSettings::winSetHomeEnvironmentKey))
1312 environment.insert(QLatin1String("HOME"), QDir::toNativeSeparators(QDir::homePath()));
1314 // Set up SSH and C locale (required by git using perl).
1315 VCSBase::VCSBasePlugin::setProcessEnvironment(&environment, false);
1319 // Synchronous git execution using Utils::SynchronousProcess, with
1320 // log windows updating.
1321 Utils::SynchronousProcessResponse GitClient::synchronousGit(const QString &workingDirectory,
1322 const QStringList &gitArguments,
1324 QTextCodec *stdOutCodec)
1326 return VCSBase::VCSBasePlugin::runVCS(workingDirectory, gitBinaryPath(), gitArguments,
1327 settings()->intValue(GitSettings::timeoutKey) * 1000,
1328 processEnvironment(),
1329 flags, stdOutCodec);
1332 bool GitClient::fullySynchronousGit(const QString &workingDirectory,
1333 const QStringList &gitArguments,
1334 QByteArray* outputText,
1335 QByteArray* errorText,
1336 bool logCommandToWindow) const
1338 return VCSBase::VCSBasePlugin::runFullySynchronous(workingDirectory, gitBinaryPath(), gitArguments,
1339 processEnvironment(), outputText, errorText,
1340 settings()->intValue(GitSettings::timeoutKey) * 1000,
1341 logCommandToWindow);
1344 static inline int askWithDetailedText(QWidget *parent,
1345 const QString &title, const QString &msg,
1347 QMessageBox::StandardButton defaultButton,
1348 QMessageBox::StandardButtons buttons = QMessageBox::Yes|QMessageBox::No)
1350 QMessageBox msgBox(QMessageBox::Question, title, msg, buttons, parent);
1351 msgBox.setDetailedText(inf);
1352 msgBox.setDefaultButton(defaultButton);
1353 return msgBox.exec();
1356 // Convenience that pops up an msg box.
1357 GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory)
1359 QString errorMessage;
1360 const StashResult sr = ensureStash(workingDirectory, &errorMessage);
1361 if (sr == StashFailed)
1362 outputWindow()->appendError(errorMessage);
1366 // Ensure that changed files are stashed before a pull or similar
1367 GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory, QString *errorMessage)
1369 QString statusOutput;
1370 switch (gitStatus(workingDirectory, false, &statusOutput, errorMessage)) {
1373 case StatusUnchanged:
1374 return StashUnchanged;
1379 const int answer = askWithDetailedText(m_core->mainWindow(), tr("Changes"),
1380 tr("Would you like to stash your changes?"),
1381 statusOutput, QMessageBox::Yes, QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel);
1383 case QMessageBox::Cancel:
1384 return StashCanceled;
1385 case QMessageBox::Yes:
1386 if (!executeSynchronousStash(workingDirectory, creatorStashMessage(QLatin1String("push")), errorMessage))
1389 case QMessageBox::No: // At your own risk, so.
1396 // Trim a git status file spec: "modified: foo .cpp" -> "modified: foo .cpp"
1397 static inline QString trimFileSpecification(QString fileSpec)
1399 const int colonIndex = fileSpec.indexOf(QLatin1Char(':'));
1400 if (colonIndex != -1) {
1401 // Collapse the sequence of spaces
1402 const int filePos = colonIndex + 2;
1403 int nonBlankPos = filePos;
1404 for ( ; fileSpec.at(nonBlankPos).isSpace(); nonBlankPos++) ;
1405 if (nonBlankPos > filePos)
1406 fileSpec.remove(filePos, nonBlankPos - filePos);
1411 GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory,
1414 QString *errorMessage,
1417 // Run 'status'. Note that git returns exitcode 1 if there are no added files.
1418 QByteArray outputText;
1419 QByteArray errorText;
1420 // @TODO: Use "--no-color" once it is supported
1421 QStringList statusArgs(QLatin1String("status"));
1423 statusArgs << QLatin1String("-u");
1424 const bool statusRc = fullySynchronousGit(workingDirectory, statusArgs, &outputText, &errorText);
1425 VCSBase::Command::removeColorCodes(&outputText);
1427 *output = commandOutputFromLocal8Bit(outputText);
1428 const bool branchKnown = outputText.contains(kBranchIndicatorC);
1430 *onBranch = branchKnown;
1431 // Is it something really fatal?
1432 if (!statusRc && !branchKnown && !outputText.contains("# Not currently on any branch.")) {
1434 const QString error = commandOutputFromLocal8Bit(errorText);
1435 *errorMessage = tr("Cannot obtain status: %1").arg(error);
1437 return StatusFailed;
1439 // Unchanged (output text depending on whether -u was passed)
1440 if (outputText.contains("nothing to commit"))
1441 return StatusUnchanged;
1442 if (outputText.contains("nothing added to commit but untracked files present"))
1443 return untracked ? StatusChanged : StatusUnchanged;
1444 return StatusChanged;
1447 // Quietly retrieve branch list of remote repository URL
1449 // The branch HEAD is pointing to is always returned first.
1450 QStringList GitClient::synchronousRepositoryBranches(const QString &repositoryURL)
1452 QStringList arguments(QLatin1String("ls-remote"));
1453 arguments << repositoryURL << QLatin1String("HEAD") << QLatin1String("refs/heads/*");
1454 const unsigned flags =
1455 VCSBase::VCSBasePlugin::SshPasswordPrompt|
1456 VCSBase::VCSBasePlugin::SuppressStdErrInLogWindow|
1457 VCSBase::VCSBasePlugin::SuppressFailMessageInLogWindow;
1458 const Utils::SynchronousProcessResponse resp = synchronousGit(QString(), arguments, flags);
1459 QStringList branches;
1460 branches << "<detached HEAD>";
1462 if (resp.result == Utils::SynchronousProcessResponse::Finished) {
1463 // split "82bfad2f51d34e98b18982211c82220b8db049b<tab>refs/heads/master"
1464 foreach(const QString &line, resp.stdOut.split(QLatin1Char('\n'))) {
1465 if (line.endsWith("\tHEAD")) {
1466 Q_ASSERT(headSha.isNull());
1467 headSha = line.left(line.indexOf(QChar('\t')));
1471 const int slashPos = line.lastIndexOf(QLatin1Char('/'));
1472 const QString branchName = line.mid(slashPos + 1);
1473 if (slashPos != -1) {
1474 if (line.startsWith(headSha))
1475 branches[0] = branchName;
1477 branches.push_back(branchName);
1484 void GitClient::launchGitK(const QString &workingDirectory)
1486 const QFileInfo binaryInfo(gitBinaryPath());
1487 QDir foundBinDir(binaryInfo.dir());
1488 const bool foundBinDirIsCmdDir = foundBinDir.dirName() == "cmd";
1489 QProcessEnvironment env = processEnvironment();
1490 if (tryLauchingGitK(env, workingDirectory, foundBinDir.path(), foundBinDirIsCmdDir))
1492 if (!foundBinDirIsCmdDir)
1495 tryLauchingGitK(env, workingDirectory, foundBinDir.path() + "/bin", false);
1498 bool GitClient::tryLauchingGitK(const QProcessEnvironment &env,
1499 const QString &workingDirectory,
1500 const QString &gitBinDirectory,
1504 // Launch 'wish' shell from git binary directory with the gitk located there
1505 const QString binary = gitBinDirectory + QLatin1String("/wish");
1506 QStringList arguments(gitBinDirectory + QLatin1String("/gitk"));
1508 // Simple: Run gitk from binary path
1509 const QString binary = gitBinDirectory + QLatin1String("/gitk");
1510 QStringList arguments;
1512 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
1513 const QString gitkOpts = settings()->stringValue(GitSettings::gitkOptionsKey);
1514 if (!gitkOpts.isEmpty())
1515 arguments.append(Utils::QtcProcess::splitArgs(gitkOpts));
1516 outwin->appendCommand(workingDirectory, binary, arguments);
1517 // This should always use QProcess::startDetached (as not to kill
1518 // the child), but that does not have an environment parameter.
1519 bool success = false;
1520 if (settings()->boolValue(GitSettings::adoptPathKey)) {
1521 QProcess *process = new QProcess(this);
1522 process->setWorkingDirectory(workingDirectory);
1523 process->setProcessEnvironment(env);
1524 process->start(binary, arguments);
1525 success = process->waitForStarted();
1527 connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
1531 success = QProcess::startDetached(binary, arguments, workingDirectory);
1534 const QString error = tr("Cannot launch \"%1\".").arg(binary);
1536 outwin->appendSilently(error);
1538 outwin->appendError(error);
1543 QString GitClient::gitBinaryPath(bool *ok, QString *errorMessage) const
1545 return settings()->gitBinaryPath(ok, errorMessage);
1548 bool GitClient::getCommitData(const QString &workingDirectory,
1550 QString *commitTemplate,
1551 CommitData *commitData,
1552 QString *errorMessage)
1554 commitData->clear();
1557 const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
1558 if (repoDirectory.isEmpty()) {
1559 *errorMessage = msgRepositoryNotFound(workingDirectory);
1563 commitData->panelInfo.repository = repoDirectory;
1565 QDir gitDir(repoDirectory);
1566 if (!gitDir.cd(QLatin1String(kGitDirectoryC))) {
1567 *errorMessage = tr("The repository \"%1\" is not initialized.").arg(repoDirectory);
1572 const QString descriptionFile = gitDir.absoluteFilePath(QLatin1String("description"));
1573 if (QFileInfo(descriptionFile).isFile()) {
1574 Utils::FileReader reader;
1575 if (!reader.fetch(descriptionFile, QIODevice::Text, errorMessage))
1577 commitData->panelInfo.description = commandOutputFromLocal8Bit(reader.data()).trimmed();
1580 // Run status. Note that it has exitcode 1 if there are no added files.
1583 const StatusResult status = gitStatus(repoDirectory, true, &output, errorMessage, &onBranch);
1587 *errorMessage = tr("You did not checkout a branch.");
1591 case StatusUnchanged:
1594 *errorMessage = msgNoChangedFiles();
1600 // Output looks like:
1601 // # On branch [branchname]
1602 // # Changes to be committed:
1603 // # (use "git reset HEAD <file>..." to unstage)
1605 // # modified: somefile.cpp
1606 // # new File: somenew.h
1608 // # Changed but not updated:
1609 // # (use "git add <file>..." to update what will be committed)
1611 // # modified: someother.cpp
1612 // # modified: submodule (modified content)
1613 // # modified: submodule2 (new commit)
1615 // # Untracked files:
1616 // # (use "git add <file>..." to include in what will be committed)
1618 // # list of files...
1620 if (status != StatusUnchanged) {
1621 if (!commitData->parseFilesFromStatus(output)) {
1622 *errorMessage = msgParseFilesFailed();
1625 // Filter out untracked files that are not part of the project
1626 VCSBase::VCSBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &commitData->untrackedFiles);
1627 if (commitData->filesEmpty()) {
1628 *errorMessage = msgNoChangedFiles();
1633 commitData->panelData.author = readConfigValue(workingDirectory, QLatin1String("user.name"));
1634 commitData->panelData.email = readConfigValue(workingDirectory, QLatin1String("user.email"));
1636 // Get the commit template or the last commit message
1638 // Amend: get last commit data as "SHA1@message". TODO: Figure out codec.
1639 QStringList args(QLatin1String("log"));
1640 const QString format = synchronousGitVersion(true) > 0x010701 ? "%h@%B" : "%h@%s%n%n%b";
1641 args << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:") + format;
1642 const Utils::SynchronousProcessResponse sp = synchronousGit(repoDirectory, args);
1643 if (sp.result != Utils::SynchronousProcessResponse::Finished) {
1644 *errorMessage = tr("Cannot retrieve last commit data of repository \"%1\".").arg(repoDirectory);
1647 const int separatorPos = sp.stdOut.indexOf(QLatin1Char('@'));
1648 QTC_ASSERT(separatorPos != -1, return false)
1649 commitData->amendSHA1= sp.stdOut.left(separatorPos);
1650 *commitTemplate = sp.stdOut.mid(separatorPos + 1);
1652 // Commit: Get the commit template
1653 QString templateFilename = readConfigValue(workingDirectory, QLatin1String("commit.template"));
1654 if (!templateFilename.isEmpty()) {
1655 // Make relative to repository
1656 const QFileInfo templateFileInfo(templateFilename);
1657 if (templateFileInfo.isRelative())
1658 templateFilename = repoDirectory + QLatin1Char('/') + templateFilename;
1659 Utils::FileReader reader;
1660 if (!reader.fetch(templateFilename, QIODevice::Text, errorMessage))
1662 *commitTemplate = QString::fromLocal8Bit(reader.data());
1668 // Log message for commits/amended commits to go to output window
1669 static inline QString msgCommitted(const QString &amendSHA1, int fileCount)
1671 if (amendSHA1.isEmpty())
1672 return GitClient::tr("Committed %n file(s).\n", 0, fileCount);
1674 return GitClient::tr("Amended \"%1\" (%n file(s)).\n", 0, fileCount).arg(amendSHA1);
1675 return GitClient::tr("Amended \"%1\".").arg(amendSHA1);
1679 bool GitClient::addAndCommit(const QString &repositoryDirectory,
1680 const GitSubmitEditorPanelData &data,
1681 const QString &amendSHA1,
1682 const QString &messageFile,
1683 const QStringList &checkedFiles,
1684 const QStringList &origCommitFiles,
1685 const QStringList &origDeletedFiles)
1687 const QString renamedSeparator = QLatin1String(" -> ");
1688 const bool amend = !amendSHA1.isEmpty();
1690 // Do we need to reset any files that had been added before
1691 // (did the user uncheck any previously added files)
1692 // Split up renamed files ('foo.cpp -> foo2.cpp').
1693 QStringList resetFiles = origCommitFiles.toSet().subtract(checkedFiles.toSet()).toList();
1694 for (QStringList::iterator it = resetFiles.begin(); it != resetFiles.end(); ++it) {
1695 const int renamedPos = it->indexOf(renamedSeparator);
1696 if (renamedPos != -1) {
1697 const QString newFile = it->mid(renamedPos + renamedSeparator.size());
1698 it->truncate(renamedPos);
1699 it = resetFiles.insert(++it, newFile);
1703 if (!resetFiles.isEmpty())
1704 if (!synchronousReset(repositoryDirectory, resetFiles))
1707 // Re-add all to make sure we have the latest changes, but only add those that aren't marked
1708 // for deletion. Purge out renamed files ('foo.cpp -> foo2.cpp').
1709 QStringList addFiles = checkedFiles.toSet().subtract(origDeletedFiles.toSet()).toList();
1710 for (QStringList::iterator it = addFiles.begin(); it != addFiles.end(); ) {
1711 if (it->contains(renamedSeparator))
1712 it = addFiles.erase(it);
1716 if (!addFiles.isEmpty() && !synchronousAdd(repositoryDirectory, false, addFiles))
1719 // Do the final commit
1721 args << QLatin1String("commit")
1722 << QLatin1String("-F") << QDir::toNativeSeparators(messageFile);
1724 args << QLatin1String("--amend");
1725 const QString &authorString = data.authorString();
1726 if (!authorString.isEmpty())
1727 args << QLatin1String("--author") << authorString;
1729 QByteArray outputText;
1730 QByteArray errorText;
1731 const bool rc = fullySynchronousGit(repositoryDirectory, args, &outputText, &errorText);
1733 outputWindow()->append(msgCommitted(amendSHA1, checkedFiles.size()));
1735 outputWindow()->appendError(tr("Cannot commit %n file(s): %1\n", 0, checkedFiles.size()).arg(commandOutputFromLocal8Bit(errorText)));
1739 /* Revert: This function can be called with a file list (to revert single
1740 * files) or a single directory (revert all). Qt Creator currently has only
1741 * 'revert single' in its VCS menus, but the code is prepared to deal with
1742 * reverting a directory pending a sophisticated selection dialog in the
1743 * VCSBase plugin. */
1744 GitClient::RevertResult GitClient::revertI(QStringList files,
1745 bool *ptrToIsDirectory,
1746 QString *errorMessage,
1750 return RevertCanceled;
1752 // Figure out the working directory
1753 const QFileInfo firstFile(files.front());
1754 const bool isDirectory = firstFile.isDir();
1755 if (ptrToIsDirectory)
1756 *ptrToIsDirectory = isDirectory;
1757 const QString workingDirectory = isDirectory ? firstFile.absoluteFilePath() : firstFile.absolutePath();
1759 const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
1760 if (repoDirectory.isEmpty()) {
1761 *errorMessage = msgRepositoryNotFound(workingDirectory);
1762 return RevertFailed;
1765 // Check for changes
1767 switch (gitStatus(repoDirectory, false, &output, errorMessage)) {
1770 case StatusUnchanged:
1771 return RevertUnchanged;
1773 return RevertFailed;
1776 if (!data.parseFilesFromStatus(output)) {
1777 *errorMessage = msgParseFilesFailed();
1778 return RevertFailed;
1781 // If we are looking at files, make them relative to the repository
1782 // directory to match them in the status output list.
1784 const QDir repoDir(repoDirectory);
1785 const QStringList::iterator cend = files.end();
1786 for (QStringList::iterator it = files.begin(); it != cend; ++it)
1787 *it = repoDir.relativeFilePath(*it);
1790 // From the status output, determine all modified [un]staged files.
1791 const QString modifiedState = QLatin1String("modified");
1792 const QStringList allStagedFiles = data.stagedFileNames(modifiedState);
1793 const QStringList allUnstagedFiles = data.unstagedFileNames(modifiedState);
1794 // Unless a directory was passed, filter all modified files for the
1795 // argument file list.
1796 QStringList stagedFiles = allStagedFiles;
1797 QStringList unstagedFiles = allUnstagedFiles;
1799 const QSet<QString> filesSet = files.toSet();
1800 stagedFiles = allStagedFiles.toSet().intersect(filesSet).toList();
1801 unstagedFiles = allUnstagedFiles.toSet().intersect(filesSet).toList();
1803 if ((!revertStaging || stagedFiles.empty()) && unstagedFiles.empty())
1804 return RevertUnchanged;
1806 // Ask to revert (to do: Handle lists with a selection dialog)
1807 const QMessageBox::StandardButton answer
1808 = QMessageBox::question(m_core->mainWindow(),
1810 tr("The file has been changed. Do you want to revert it?"),
1811 QMessageBox::Yes|QMessageBox::No,
1813 if (answer == QMessageBox::No)
1814 return RevertCanceled;
1816 // Unstage the staged files
1817 if (revertStaging && !stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage))
1818 return RevertFailed;
1819 QStringList filesToRevert = unstagedFiles;
1821 filesToRevert += stagedFiles;
1823 if (!synchronousCheckoutFiles(repoDirectory, filesToRevert, QString(), errorMessage, revertStaging))
1824 return RevertFailed;
1828 void GitClient::revert(const QStringList &files, bool revertStaging)
1831 QString errorMessage;
1832 switch (revertI(files, &isDirectory, &errorMessage, revertStaging)) {
1834 GitPlugin::instance()->gitVersionControl()->emitFilesChanged(files);
1836 case RevertCanceled:
1838 case RevertUnchanged: {
1839 const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
1840 outputWindow()->append(msg);
1844 outputWindow()->append(errorMessage);
1849 bool GitClient::synchronousFetch(const QString &workingDirectory, const QString &remote)
1851 QStringList arguments(QLatin1String("fetch"));
1852 if (!remote.isEmpty())
1853 arguments << remote;
1854 // Disable UNIX terminals to suppress SSH prompting.
1855 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
1856 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
1857 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, arguments, flags);
1858 return resp.result == Utils::SynchronousProcessResponse::Finished;
1861 bool GitClient::synchronousPull(const QString &workingDirectory)
1863 return synchronousPull(workingDirectory, settings()->boolValue(GitSettings::pullRebaseKey));
1866 bool GitClient::synchronousPull(const QString &workingDirectory, bool rebase)
1868 QStringList arguments(QLatin1String("pull"));
1870 arguments << QLatin1String("--rebase");
1871 // Disable UNIX terminals to suppress SSH prompting.
1872 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow;
1873 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, arguments, flags);
1874 // Notify about changed files or abort the rebase.
1875 const bool ok = resp.result == Utils::SynchronousProcessResponse::Finished;
1877 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
1880 syncAbortPullRebase(workingDirectory);
1885 void GitClient::syncAbortPullRebase(const QString &workingDir)
1887 // Abort rebase to clean if something goes wrong
1888 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
1889 outwin->appendError(tr("The command 'git pull --rebase' failed, aborting rebase."));
1890 QStringList arguments;
1891 arguments << QLatin1String("rebase") << QLatin1String("--abort");
1894 const bool rc = fullySynchronousGit(workingDir, arguments, &stdOut, &stdErr, true);
1895 outwin->append(commandOutputFromLocal8Bit(stdOut));
1897 outwin->appendError(commandOutputFromLocal8Bit(stdErr));
1900 // Subversion: git svn
1901 void GitClient::synchronousSubversionFetch(const QString &workingDirectory)
1904 args << QLatin1String("svn") << QLatin1String("fetch");
1905 // Disable UNIX terminals to suppress SSH prompting.
1906 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
1907 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
1908 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, args, flags);
1909 // Notify about changes.
1910 if (resp.result == Utils::SynchronousProcessResponse::Finished)
1911 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
1914 void GitClient::subversionLog(const QString &workingDirectory)
1916 QStringList arguments;
1917 arguments << QLatin1String("svn") << QLatin1String("log");
1918 int logCount = settings()->intValue(GitSettings::logCountKey);
1920 arguments << (QLatin1String("--limit=") + QString::number(logCount));
1922 // Create a command editor, no highlighting or interaction.
1923 const QString title = tr("Git SVN Log");
1924 const QString editorId = QLatin1String(Git::Constants::C_GIT_COMMAND_LOG_EDITOR);
1925 const QString sourceFile = VCSBase::VCSBaseEditorWidget::getSource(workingDirectory, QStringList());
1926 VCSBase::VCSBaseEditorWidget *editor = findExistingVCSEditor("svnLog", sourceFile);
1928 editor = createVCSEditor(editorId, title, sourceFile, false, "svnLog", sourceFile, 0);
1929 executeGit(workingDirectory, arguments, editor);
1932 bool GitClient::synchronousPush(const QString &workingDirectory)
1934 // Disable UNIX terminals to suppress SSH prompting.
1935 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
1936 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
1937 const Utils::SynchronousProcessResponse resp =
1938 synchronousGit(workingDirectory, QStringList(QLatin1String("push")), flags);
1939 return resp.result == Utils::SynchronousProcessResponse::Finished;
1942 QString GitClient::msgNoChangedFiles()
1944 return tr("There are no modified files.");
1947 void GitClient::stashPop(const QString &workingDirectory)
1949 QStringList arguments(QLatin1String("stash"));
1950 arguments << QLatin1String("pop");
1951 VCSBase::Command *cmd = executeGit(workingDirectory, arguments, 0, true);
1952 connectRepositoryChanged(workingDirectory, cmd);
1955 bool GitClient::synchronousStashRestore(const QString &workingDirectory,
1956 const QString &stash,
1957 const QString &branch /* = QString()*/,
1958 QString *errorMessage)
1960 QStringList arguments(QLatin1String("stash"));
1961 if (branch.isEmpty())
1962 arguments << QLatin1String("apply") << stash;
1964 arguments << QLatin1String("branch") << branch << stash;
1965 QByteArray outputText;
1966 QByteArray errorText;
1967 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
1969 const QString stdErr = commandOutputFromLocal8Bit(errorText);
1970 const QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory);
1971 const QString msg = branch.isEmpty() ?
1972 tr("Cannot restore stash \"%1\": %2").
1973 arg(nativeWorkingDir, stdErr) :
1974 tr("Cannot restore stash \"%1\" to branch \"%2\": %3").
1975 arg(nativeWorkingDir, branch, stdErr);
1977 *errorMessage = msg;
1979 outputWindow()->append(msg);
1982 QString output = commandOutputFromLocal8Bit(outputText);
1983 if (!output.isEmpty())
1984 outputWindow()->append(output);
1985 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
1989 bool GitClient::synchronousStashRemove(const QString &workingDirectory,
1990 const QString &stash /* = QString() */,
1991 QString *errorMessage /* = 0 */)
1993 QStringList arguments(QLatin1String("stash"));
1994 if (stash.isEmpty()) {
1995 arguments << QLatin1String("clear");
1997 arguments << QLatin1String("drop") << stash;
1999 QByteArray outputText;
2000 QByteArray errorText;
2001 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
2003 const QString stdErr = commandOutputFromLocal8Bit(errorText);
2004 const QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory);
2005 const QString msg = stash.isEmpty() ?
2006 tr("Cannot remove stashes of \"%1\": %2").
2007 arg(nativeWorkingDir, stdErr) :
2008 tr("Cannot remove stash \"%1\" of \"%2\": %3").
2009 arg(stash, nativeWorkingDir, stdErr);
2011 *errorMessage = msg;
2013 outputWindow()->append(msg);
2016 QString output = commandOutputFromLocal8Bit(outputText);
2017 if (!output.isEmpty())
2018 outputWindow()->append(output);
2022 void GitClient::branchList(const QString &workingDirectory)
2024 QStringList arguments(QLatin1String("branch"));
2025 arguments << QLatin1String("-r") << QLatin1String(noColorOption);
2026 executeGit(workingDirectory, arguments, 0, true);
2029 void GitClient::stashList(const QString &workingDirectory)
2031 QStringList arguments(QLatin1String("stash"));
2032 arguments << QLatin1String("list") << QLatin1String(noColorOption);
2033 executeGit(workingDirectory, arguments, 0, true);
2036 bool GitClient::synchronousStashList(const QString &workingDirectory,
2037 QList<Stash> *stashes,
2038 QString *errorMessage /* = 0 */)
2041 QStringList arguments(QLatin1String("stash"));
2042 arguments << QLatin1String("list") << QLatin1String(noColorOption);
2043 QByteArray outputText;
2044 QByteArray errorText;
2045 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
2047 const QString msg = tr("Cannot retrieve stash list of \"%1\": %2").
2048 arg(QDir::toNativeSeparators(workingDirectory),
2049 commandOutputFromLocal8Bit(errorText));
2051 *errorMessage = msg;
2053 outputWindow()->append(msg);
2058 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText))
2059 if (stash.parseStashLine(line))
2060 stashes->push_back(stash);
2064 QString GitClient::readConfig(const QString &workingDirectory, const QStringList &configVar)
2066 QStringList arguments;
2067 arguments << QLatin1String("config") << configVar;
2069 QByteArray outputText;
2070 QByteArray errorText;
2071 if (fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText, false))
2072 return commandOutputFromLocal8Bit(outputText);
2076 // Read a single-line config value, return trimmed
2077 QString GitClient::readConfigValue(const QString &workingDirectory, const QString &configVar)
2079 return readConfig(workingDirectory, QStringList(configVar)).remove(QLatin1Char('\n'));
2082 bool GitClient::cloneRepository(const QString &directory,const QByteArray &url)
2084 QDir workingDirectory(directory);
2085 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt |
2086 VCSBase::VCSBasePlugin::ShowStdOutInLogWindow|
2087 VCSBase::VCSBasePlugin::ShowSuccessMessage;
2089 if (workingDirectory.exists()) {
2090 if (!synchronousInit(workingDirectory.path()))
2093 QStringList arguments(QLatin1String("remote"));
2094 arguments << QLatin1String("add") << QLatin1String("origin") << url;
2095 if (!fullySynchronousGit(workingDirectory.path(), arguments, 0, 0, true))
2099 arguments << QLatin1String("fetch");
2100 const Utils::SynchronousProcessResponse resp =
2101 synchronousGit(workingDirectory.path(), arguments, flags);
2102 if (resp.result != Utils::SynchronousProcessResponse::Finished)
2106 arguments << QLatin1String("config")
2107 << QLatin1String("branch.master.remote")
2108 << QLatin1String("origin");
2109 if (!fullySynchronousGit(workingDirectory.path(), arguments, 0, 0, true))
2113 arguments << QLatin1String("config")
2114 << QLatin1String("branch.master.merge")
2115 << QLatin1String("refs/heads/master");
2116 if (!fullySynchronousGit(workingDirectory.path(), arguments, 0, 0, true))
2121 QStringList arguments(QLatin1String("clone"));
2122 arguments << url << workingDirectory.dirName();
2123 workingDirectory.cdUp();
2124 const Utils::SynchronousProcessResponse resp =
2125 synchronousGit(workingDirectory.path(), arguments, flags);
2126 // TODO: Turn this into a VCSBaseClient and use resetCachedVcsInfo(...)
2127 Core::VcsManager *vcsManager = m_core->vcsManager();
2128 vcsManager->resetVersionControlForDirectory(workingDirectory.absolutePath());
2129 return (resp.result == Utils::SynchronousProcessResponse::Finished);
2133 QString GitClient::vcsGetRepositoryURL(const QString &directory)
2135 QStringList arguments(QLatin1String("config"));
2136 QByteArray outputText;
2138 arguments << QLatin1String("remote.origin.url");
2140 if (fullySynchronousGit(directory, arguments, &outputText, 0, false))
2141 return commandOutputFromLocal8Bit(outputText);
2145 GitSettings *GitClient::settings() const
2150 void GitClient::connectRepositoryChanged(const QString & repository, VCSBase::Command *cmd)
2152 // Bind command success termination with repository to changed signal
2153 if (!m_repositoryChangedSignalMapper) {
2154 m_repositoryChangedSignalMapper = new QSignalMapper(this);
2155 connect(m_repositoryChangedSignalMapper, SIGNAL(mapped(QString)),
2156 GitPlugin::instance()->gitVersionControl(), SIGNAL(repositoryChanged(QString)));
2158 m_repositoryChangedSignalMapper->setMapping(cmd, repository);
2159 connect(cmd, SIGNAL(success()), m_repositoryChangedSignalMapper, SLOT(map()),
2160 Qt::QueuedConnection);
2163 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
2164 unsigned GitClient::gitVersion(bool silent, QString *errorMessage) const
2166 const QString newGitBinary = gitBinaryPath();
2167 if (m_gitVersionForBinary != newGitBinary && !newGitBinary.isEmpty()) {
2168 // Do not execute repeatedly if that fails (due to git
2169 // not being installed) until settings are changed.
2170 m_cachedGitVersion = synchronousGitVersion(silent, errorMessage);
2171 m_gitVersionForBinary = newGitBinary;
2173 return m_cachedGitVersion;
2176 QString GitClient::gitVersionString(bool silent, QString *errorMessage) const
2178 if (const unsigned version = gitVersion(silent, errorMessage)) {
2180 QTextStream(&rc) << (version >> 16) << '.'
2181 << (0xFF & (version >> 8)) << '.'
2182 << (version & 0xFF);
2187 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
2188 unsigned GitClient::synchronousGitVersion(bool silent, QString *errorMessage) const
2190 // run git --version
2191 QByteArray outputText;
2192 QByteArray errorText;
2193 const bool rc = fullySynchronousGit(QString(), QStringList("--version"), &outputText, &errorText);
2195 const QString msg = tr("Cannot determine git version: %1").arg(commandOutputFromLocal8Bit(errorText));
2197 *errorMessage = msg;
2200 outputWindow()->append(msg);
2202 outputWindow()->appendError(msg);
2207 // cut 'git version 1.6.5.1.sha'
2208 const QString output = commandOutputFromLocal8Bit(outputText);
2209 const QRegExp versionPattern(QLatin1String("^[^\\d]+([\\d])\\.([\\d])\\.([\\d]).*$"));
2210 QTC_ASSERT(versionPattern.isValid(), return 0);
2211 QTC_ASSERT(versionPattern.exactMatch(output), return 0);
2212 const unsigned major = versionPattern.cap(1).toUInt();
2213 const unsigned minor = versionPattern.cap(2).toUInt();
2214 const unsigned patch = versionPattern.cap(3).toUInt();
2215 return version(major, minor, patch);
2218 } // namespace Internal
2221 #include "gitclient.moc"