1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
11 ** Licensees holding valid Qt Commercial licenses may use this file in
12 ** accordance with the Qt Commercial License Agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and Nokia.
16 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 ** If you are unsure which license is appropriate for your use, please
26 ** contact the sales department at http://qt.nokia.com/contact.
28 **************************************************************************/
30 #include "gitclient.h"
31 #include "gitcommand.h"
34 #include "commitdata.h"
35 #include "gitconstants.h"
36 #include "gitplugin.h"
37 #include "gitsubmiteditor.h"
38 #include "gitversioncontrol.h"
40 #include <coreplugin/actionmanager/actionmanager.h>
41 #include <coreplugin/coreconstants.h>
42 #include <coreplugin/editormanager/editormanager.h>
43 #include <coreplugin/icore.h>
44 #include <coreplugin/messagemanager.h>
45 #include <coreplugin/progressmanager/progressmanager.h>
46 #include <coreplugin/uniqueidmanager.h>
47 #include <coreplugin/filemanager.h>
48 #include <coreplugin/iversioncontrol.h>
50 #include <texteditor/itexteditor.h>
51 #include <utils/qtcassert.h>
52 #include <utils/synchronousprocess.h>
53 #include <vcsbase/vcsbaseeditor.h>
54 #include <vcsbase/vcsbaseoutputwindow.h>
55 #include <vcsbase/vcsbaseplugin.h>
57 #include <projectexplorer/environment.h>
59 #include <QtCore/QRegExp>
60 #include <QtCore/QTemporaryFile>
61 #include <QtCore/QTime>
62 #include <QtCore/QFileInfo>
63 #include <QtCore/QDir>
64 #include <QtCore/QSignalMapper>
66 #include <QtGui/QMainWindow> // for msg box parent
67 #include <QtGui/QMessageBox>
68 #include <QtGui/QPushButton>
70 static const char *const kGitDirectoryC = ".git";
71 static const char *const kBranchIndicatorC = "# On branch";
73 static inline QString msgServerFailure()
75 return Git::Internal::GitClient::tr(
76 "Note that the git plugin for QtCreator is not able to interact with the server "
77 "so far. Thus, manual ssh-identification etc. will not work.");
80 inline Core::IEditor* locateEditor(const Core::ICore *core, const char *property, const QString &entry)
82 foreach (Core::IEditor *ed, core->editorManager()->openedEditors())
83 if (ed->file()->property(property).toString() == entry)
88 // Return converted command output, remove '\r' read on Windows
89 static inline QString commandOutputFromLocal8Bit(const QByteArray &a)
91 QString output = QString::fromLocal8Bit(a);
92 output.remove(QLatin1Char('\r'));
96 // Return converted command output split into lines
97 static inline QStringList commandOutputLinesFromLocal8Bit(const QByteArray &a)
99 QString output = commandOutputFromLocal8Bit(a);
100 const QChar newLine = QLatin1Char('\n');
101 if (output.endsWith(newLine))
102 output.truncate(output.size() - 1);
103 if (output.isEmpty())
104 return QStringList();
105 return output.split(newLine);
108 static inline VCSBase::VCSBaseOutputWindow *outputWindow()
110 return VCSBase::VCSBaseOutputWindow::instance();
116 static inline QString msgRepositoryNotFound(const QString &dir)
118 return GitClient::tr("Unable to determine the repository for %1.").arg(dir);
121 static inline QString msgParseFilesFailed()
123 return GitClient::tr("Unable to parse the file output.");
126 // ---------------- GitClient
128 const char *GitClient::stashNamePrefix = "stash@{";
130 GitClient::GitClient(GitPlugin* plugin)
131 : m_msgWait(tr("Waiting for data...")),
133 m_core(Core::ICore::instance()),
134 m_repositoryChangedSignalMapper(0),
135 m_cachedGitVersion(0),
136 m_hasCachedGitVersion(false)
138 if (QSettings *s = m_core->settings()) {
139 m_settings.fromSettings(s);
140 m_binaryPath = m_settings.gitBinaryPath();
144 GitClient::~GitClient()
148 const char *GitClient::noColorOption = "--no-color";
150 QString GitClient::findRepositoryForDirectory(const QString &dir)
152 // Check for ".git/config"
153 const QString checkFile = QLatin1String(kGitDirectoryC) + QLatin1String("/config");
154 return VCSBase::VCSBasePlugin::findRepositoryForDirectory(dir, checkFile);
157 /* Create an editor associated to VCS output of a source file/directory
158 * (using the file's codec). Makes use of a dynamic property to find an
159 * existing instance and to reuse it (in case, say, 'git diff foo' is
161 VCSBase::VCSBaseEditor
162 *GitClient::createVCSEditor(const QString &id,
164 // Source file or directory
165 const QString &source,
167 // Dynamic property and value to identify that editor
168 const char *registerDynamicProperty,
169 const QString &dynamicPropertyValue) const
171 VCSBase::VCSBaseEditor *rc = 0;
172 Core::IEditor* outputEditor = locateEditor(m_core, registerDynamicProperty, dynamicPropertyValue);
175 outputEditor->createNew(m_msgWait);
176 rc = VCSBase::VCSBaseEditor::getVcsBaseEditor(outputEditor);
177 QTC_ASSERT(rc, return 0);
179 // Create new, set wait message, set up with source and codec
180 outputEditor = m_core->editorManager()->openEditorWithContents(id, &title, m_msgWait);
181 outputEditor->file()->setProperty(registerDynamicProperty, dynamicPropertyValue);
182 rc = VCSBase::VCSBaseEditor::getVcsBaseEditor(outputEditor);
183 connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,int)),
184 this, SLOT(slotBlameRevisionRequested(QString,QString,int)));
185 QTC_ASSERT(rc, return 0);
186 rc->setSource(source);
188 rc->setCodec(VCSBase::VCSBaseEditor::getCodec(source));
190 m_core->editorManager()->activateEditor(outputEditor);
191 rc->setForceReadOnly(true);
195 void GitClient::diff(const QString &workingDirectory,
196 const QStringList &diffArgs,
197 const QStringList &unstagedFileNames,
198 const QStringList &stagedFileNames)
201 if (Git::Constants::debug)
202 qDebug() << "diff" << workingDirectory << unstagedFileNames << stagedFileNames;
204 const QString binary = QLatin1String(Constants::GIT_BINARY);
205 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
206 const QString title = tr("Git Diff");
208 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, workingDirectory, true, "originalFileName", workingDirectory);
209 editor->setDiffBaseDirectory(workingDirectory);
211 // Create a batch of 2 commands to be run after each other in case
212 // we have a mixture of staged/unstaged files as is the case
213 // when using the submit dialog.
214 GitCommand *command = createCommand(workingDirectory, editor);
216 QStringList commonDiffArgs;
217 commonDiffArgs << QLatin1String("diff") << QLatin1String(noColorOption);
218 if (m_settings.diffPatience)
219 commonDiffArgs << QLatin1String("--patience");
220 if (unstagedFileNames.empty() && stagedFileNames.empty()) {
221 QStringList arguments(commonDiffArgs);
222 arguments << diffArgs;
223 outputWindow()->appendCommand(workingDirectory, binary, arguments);
224 command->addJob(arguments, m_settings.timeoutSeconds);
227 if (!unstagedFileNames.empty()) {
228 QStringList arguments(commonDiffArgs);
229 arguments << QLatin1String("--") << unstagedFileNames;
230 outputWindow()->appendCommand(workingDirectory, binary, arguments);
231 command->addJob(arguments, m_settings.timeoutSeconds);
233 if (!stagedFileNames.empty()) {
234 QStringList arguments(commonDiffArgs);
235 arguments << QLatin1String("--cached") << diffArgs << QLatin1String("--") << stagedFileNames;
236 outputWindow()->appendCommand(workingDirectory, binary, arguments);
237 command->addJob(arguments, m_settings.timeoutSeconds);
243 void GitClient::diff(const QString &workingDirectory,
244 const QStringList &diffArgs,
245 const QString &fileName)
247 if (Git::Constants::debug)
248 qDebug() << "diff" << workingDirectory << fileName;
249 QStringList arguments;
250 arguments << QLatin1String("diff") << QLatin1String(noColorOption)
252 if (!fileName.isEmpty())
253 arguments << QLatin1String("--") << fileName;
254 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
255 const QString title = tr("Git Diff %1").arg(fileName);
256 const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileName);
257 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, sourceFile, true, "originalFileName", sourceFile);
258 executeGit(workingDirectory, arguments, editor);
261 void GitClient::diffBranch(const QString &workingDirectory,
262 const QStringList &diffArgs,
263 const QString &branchName)
265 if (Git::Constants::debug)
266 qDebug() << "diffBranch" << workingDirectory << branchName;
267 QStringList arguments;
268 arguments << QLatin1String("diff") << QLatin1String(noColorOption)
269 << diffArgs << branchName;
271 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
272 const QString title = tr("Git Diff Branch %1").arg(branchName);
273 const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, QStringList());
274 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, sourceFile, true,
275 "BranchName", branchName);
276 executeGit(workingDirectory, arguments, editor);
279 void GitClient::status(const QString &workingDirectory)
281 // @TODO: Use "--no-color" once it is supported
282 QStringList statusArgs(QLatin1String("status"));
283 statusArgs << QLatin1String("-u");
284 VCSBase::VCSBaseOutputWindow *outwin = outputWindow();
285 outwin->setRepository(workingDirectory);
286 GitCommand *command = executeGit(workingDirectory, statusArgs, 0, true);
287 connect(command, SIGNAL(finished(bool,int,QVariant)), outwin, SLOT(clearRepository()),
288 Qt::QueuedConnection);
291 static const char graphLogFormatC[] = "%h %an %s %ci";
293 // Create a graphical log.
294 void GitClient::graphLog(const QString &workingDirectory)
296 if (Git::Constants::debug)
297 qDebug() << "log" << workingDirectory;
299 QStringList arguments;
300 arguments << QLatin1String("log") << QLatin1String(noColorOption);
302 if (m_settings.logCount > 0)
303 arguments << QLatin1String("-n") << QString::number(m_settings.logCount);
304 arguments << (QLatin1String("--pretty=format:") + QLatin1String(graphLogFormatC))
305 << QLatin1String("--topo-order") << QLatin1String("--graph");
307 const QString title = tr("Git Log");
308 const QString editorId = QLatin1String(Git::Constants::GIT_LOG_EDITOR_ID);
309 const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, QStringList());
310 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, sourceFile, false, "logFileName", sourceFile);
311 executeGit(workingDirectory, arguments, editor);
314 void GitClient::log(const QString &workingDirectory, const QStringList &fileNames, bool enableAnnotationContextMenu)
316 if (Git::Constants::debug)
317 qDebug() << "log" << workingDirectory << fileNames;
319 QStringList arguments;
320 arguments << QLatin1String("log") << QLatin1String(noColorOption);
322 if (m_settings.logCount > 0)
323 arguments << QLatin1String("-n") << QString::number(m_settings.logCount);
325 if (!fileNames.isEmpty())
326 arguments.append(fileNames);
328 const QString msgArg = fileNames.empty() ? workingDirectory :
329 fileNames.join(QString(", "));
330 const QString title = tr("Git Log %1").arg(msgArg);
331 const QString editorId = QLatin1String(Git::Constants::GIT_LOG_EDITOR_ID);
332 const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileNames);
333 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, sourceFile, false, "logFileName", sourceFile);
334 editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
335 executeGit(workingDirectory, arguments, editor);
338 // Do not show "0000" or "^32ae4"
339 static inline bool canShow(const QString &sha)
341 if (sha.startsWith(QLatin1Char('^')))
343 if (sha.count(QLatin1Char('0')) == sha.size())
348 static inline QString msgCannotShow(const QString &sha)
350 return GitClient::tr("Cannot describe '%1'.").arg(sha);
353 void GitClient::show(const QString &source, const QString &id)
355 if (Git::Constants::debug)
356 qDebug() << "show" << source << id;
358 outputWindow()->append(msgCannotShow(id));
362 QStringList arguments;
363 arguments << QLatin1String("show") << QLatin1String(noColorOption) << id;
365 const QString title = tr("Git Show %1").arg(id);
366 const QString editorId = QLatin1String(Git::Constants::GIT_DIFF_EDITOR_ID);
367 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, source, true, "show", id);
369 const QFileInfo sourceFi(source);
370 const QString workDir = sourceFi.isDir() ? sourceFi.absoluteFilePath() : sourceFi.absolutePath();
371 executeGit(workDir, arguments, editor);
374 void GitClient::slotBlameRevisionRequested(const QString &source, QString change, int lineNumber)
376 // This might be invoked with a verbose revision description
377 // "SHA1 author subject" from the annotation context menu. Strip the rest.
378 const int blankPos = change.indexOf(QLatin1Char(' '));
380 change.truncate(blankPos);
381 const QFileInfo fi(source);
382 blame(fi.absolutePath(), fi.fileName(), change, lineNumber);
385 void GitClient::blame(const QString &workingDirectory,
386 const QString &fileName,
387 const QString &revision /* = QString() */,
388 int lineNumber /* = -1 */)
390 if (Git::Constants::debug)
391 qDebug() << "blame" << workingDirectory << fileName << lineNumber;
392 QStringList arguments(QLatin1String("blame"));
393 arguments << QLatin1String("--root");
394 if (m_plugin->settings().spaceIgnorantBlame)
395 arguments << QLatin1String("-w");
396 arguments << QLatin1String("--") << fileName;
397 if (!revision.isEmpty())
398 arguments << revision;
399 const QString editorId = QLatin1String(Git::Constants::GIT_BLAME_EDITOR_ID);
400 const QString id = VCSBase::VCSBaseEditor::getTitleId(workingDirectory, QStringList(fileName), revision);
401 const QString title = tr("Git Blame %1").arg(id);
402 const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileName);
404 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, sourceFile, true, "blameFileName", id);
405 executeGit(workingDirectory, arguments, editor, false, GitCommand::NoReport, lineNumber);
408 void GitClient::checkoutBranch(const QString &workingDirectory, const QString &branch)
410 QStringList arguments(QLatin1String("checkout"));
412 GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
413 connectRepositoryChanged(workingDirectory, cmd);
416 bool GitClient::synchronousCheckoutBranch(const QString &workingDirectory,
417 const QString &branch,
418 QString *errorMessage /* = 0 */)
420 QByteArray outputText;
421 QByteArray errorText;
422 QStringList arguments;
423 arguments << QLatin1String("checkout") << branch;
424 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
425 const QString output = commandOutputFromLocal8Bit(outputText);
426 outputWindow()->append(output);
428 const QString stdErr = commandOutputFromLocal8Bit(errorText);
429 //: Meaning of the arguments: %1: Branch, %2: Repository, %3: Error message
430 const QString msg = tr("Unable to checkout %1 of %2: %3").arg(branch, workingDirectory, stdErr);
434 outputWindow()->appendError(msg);
441 void GitClient::checkout(const QString &workingDirectory, const QString &fileName)
443 // Passing an empty argument as the file name is very dangereous, since this makes
444 // git checkout apply to all files. Almost looks like a bug in git.
445 if (fileName.isEmpty())
448 QStringList arguments;
449 arguments << QLatin1String("checkout") << QLatin1String("HEAD") << QLatin1String("--")
452 executeGit(workingDirectory, arguments, 0, true);
455 void GitClient::hardReset(const QString &workingDirectory, const QString &commit)
457 QStringList arguments;
458 arguments << QLatin1String("reset") << QLatin1String("--hard");
459 if (!commit.isEmpty())
462 GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
463 connectRepositoryChanged(workingDirectory, cmd);
466 void GitClient::addFile(const QString &workingDirectory, const QString &fileName)
468 QStringList arguments;
469 arguments << QLatin1String("add") << fileName;
471 executeGit(workingDirectory, arguments, 0, true);
474 // Warning: 'intendToAdd' works only from 1.6.1 onwards
475 bool GitClient::synchronousAdd(const QString &workingDirectory,
477 const QStringList &files)
479 if (Git::Constants::debug)
480 qDebug() << Q_FUNC_INFO << workingDirectory << files;
481 QByteArray outputText;
482 QByteArray errorText;
483 QStringList arguments;
484 arguments << QLatin1String("add");
486 arguments << QLatin1String("--intent-to-add");
487 arguments.append(files);
488 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
490 const QString errorMessage = tr("Unable to add %n file(s) to %1: %2", 0, files.size()).
491 arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
492 outputWindow()->appendError(errorMessage);
497 bool GitClient::synchronousDelete(const QString &workingDirectory,
499 const QStringList &files)
501 if (Git::Constants::debug)
502 qDebug() << Q_FUNC_INFO << workingDirectory << files;
503 QByteArray outputText;
504 QByteArray errorText;
505 QStringList arguments;
506 arguments << QLatin1String("rm");
508 arguments << QLatin1String("--force");
509 arguments.append(files);
510 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
512 const QString errorMessage = tr("Unable to remove %n file(s) from %1: %2", 0, files.size()).
513 arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
514 outputWindow()->appendError(errorMessage);
519 bool GitClient::synchronousMove(const QString &workingDirectory,
523 if (Git::Constants::debug)
524 qDebug() << Q_FUNC_INFO << workingDirectory << from << to;
525 QByteArray outputText;
526 QByteArray errorText;
527 QStringList arguments;
528 arguments << QLatin1String("mv");
531 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
533 const QString errorMessage = tr("Unable to move from %1 to %2: %3").
534 arg(from, to, commandOutputFromLocal8Bit(errorText));
535 outputWindow()->appendError(errorMessage);
540 bool GitClient::synchronousReset(const QString &workingDirectory,
541 const QStringList &files,
542 QString *errorMessage)
544 if (Git::Constants::debug)
545 qDebug() << Q_FUNC_INFO << workingDirectory << files;
546 QByteArray outputText;
547 QByteArray errorText;
548 QStringList arguments;
549 arguments << QLatin1String("reset");
550 if (files.isEmpty()) {
551 arguments << QLatin1String("--hard");
553 arguments << QLatin1String("HEAD") << QLatin1String("--") << files;
555 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
556 const QString output = commandOutputFromLocal8Bit(outputText);
557 outputWindow()->append(output);
558 // Note that git exits with 1 even if the operation is successful
559 // Assume real failure if the output does not contain "foo.cpp modified"
560 // or "Unstaged changes after reset" (git 1.7.0).
562 (!output.contains(QLatin1String("modified"))
563 && !output.contains(QLatin1String("Unstaged changes after reset")))) {
564 const QString stdErr = commandOutputFromLocal8Bit(errorText);
565 const QString msg = files.isEmpty() ?
566 tr("Unable to reset %1: %2").arg(workingDirectory, stdErr) :
567 tr("Unable to reset %n file(s) in %1: %2", 0, files.size()).arg(workingDirectory, stdErr);
571 outputWindow()->appendError(msg);
578 // Initialize repository
579 bool GitClient::synchronousInit(const QString &workingDirectory)
581 if (Git::Constants::debug)
582 qDebug() << Q_FUNC_INFO << workingDirectory;
583 QByteArray outputText;
584 QByteArray errorText;
585 const QStringList arguments(QLatin1String("init"));
586 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
587 // '[Re]Initialized...'
588 outputWindow()->append(commandOutputFromLocal8Bit(outputText));
590 outputWindow()->appendError(commandOutputFromLocal8Bit(errorText));
594 /* Checkout, supports:
595 * git checkout -- <files>
596 * git checkout revision -- <files>
597 * git checkout revision -- . */
598 bool GitClient::synchronousCheckoutFiles(const QString &workingDirectory,
599 QStringList files /* = QStringList() */,
600 QString revision /* = QString() */,
601 QString *errorMessage /* = 0 */)
603 if (Git::Constants::debug)
604 qDebug() << Q_FUNC_INFO << workingDirectory << files;
605 if (revision.isEmpty())
606 revision = QLatin1String("HEAD");
608 files = QStringList(QString(QLatin1Char('.')));
609 QByteArray outputText;
610 QByteArray errorText;
611 QStringList arguments;
612 arguments << QLatin1String("checkout") << revision << QLatin1String("--") << files;
613 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
615 const QString fileArg = files.join(QLatin1String(", "));
616 //: Meaning of the arguments: %1: revision, %2: files, %3: repository,
617 //: %4: Error message
618 const QString msg = tr("Unable to checkout %1 of %2 in %3: %4").
619 arg(revision, fileArg, workingDirectory, commandOutputFromLocal8Bit(errorText));
623 outputWindow()->appendError(msg);
630 static inline QString msgParentRevisionFailed(const QString &workingDirectory,
631 const QString &revision,
634 //: Failed to find parent revisions of a SHA1 for "annotate previous"
635 return GitClient::tr("Unable to find parent revisions of %1 in %2: %3").arg(revision, workingDirectory, why);
638 static inline QString msgInvalidRevision()
640 return GitClient::tr("Invalid revision");
643 // Split a line of "<commit> <parent1> ..." to obtain parents from "rev-list" or "log".
644 static inline bool splitCommitParents(const QString &line,
646 QStringList *parents = 0)
652 QStringList tokens = line.trimmed().split(QLatin1Char(' '));
653 if (tokens.size() < 2)
656 *commit = tokens.front();
663 // Find out the immediate parent revisions of a revision of the repository.
664 // Might be several in case of merges.
665 bool GitClient::synchronousParentRevisions(const QString &workingDirectory,
666 const QStringList &files /* = QStringList() */,
667 const QString &revision,
668 QStringList *parents,
669 QString *errorMessage)
671 if (Git::Constants::debug)
672 qDebug() << Q_FUNC_INFO << workingDirectory << revision;
673 QByteArray outputTextData;
674 QByteArray errorText;
675 QStringList arguments;
676 arguments << QLatin1String("rev-list") << QLatin1String(GitClient::noColorOption)
677 << QLatin1String("--parents") << QLatin1String("--max-count=1") << revision;
678 if (!files.isEmpty()) {
679 arguments.append(QLatin1String("--"));
680 arguments.append(files);
682 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
684 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, commandOutputFromLocal8Bit(errorText));
687 // Should result in one line of blank-delimited revisions, specifying current first
689 QString outputText = commandOutputFromLocal8Bit(outputTextData);
690 outputText.remove(QLatin1Char('\n'));
691 if (!splitCommitParents(outputText, 0, parents)) {
692 *errorMessage = msgParentRevisionFailed(workingDirectory, revision, msgInvalidRevision());
695 if (Git::Constants::debug)
696 qDebug() << workingDirectory << files << revision << "->" << *parents;
700 // Short SHA1, author, subject
701 static const char defaultShortLogFormatC[] = "%h (%an \"%s\")";
703 bool GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision,
704 QString *description, QString *errorMessage)
706 // Short SHA 1, author, subject
707 return synchronousShortDescription(workingDirectory, revision,
708 QLatin1String(defaultShortLogFormatC),
709 description, errorMessage);
712 // Convenience working on a list of revisions
713 bool GitClient::synchronousShortDescriptions(const QString &workingDirectory, const QStringList &revisions,
714 QStringList *descriptions, QString *errorMessage)
716 descriptions->clear();
717 foreach (const QString &revision, revisions) {
719 if (!synchronousShortDescription(workingDirectory, revision, &description, errorMessage)) {
720 descriptions->clear();
723 descriptions->push_back(description);
728 static inline QString msgCannotDetermineBranch(const QString &workingDirectory, const QString &why)
730 return GitClient::tr("Unable to retrieve branch of %1: %2").arg(workingDirectory, why);
733 // Retrieve head revision/branch
734 bool GitClient::synchronousTopRevision(const QString &workingDirectory,
735 QString *revision /* = 0 */,
736 QString *branch /* = 0 */,
737 QString *errorMessageIn /* = 0 */)
739 if (Git::Constants::debug)
740 qDebug() << Q_FUNC_INFO << workingDirectory;
741 QByteArray outputTextData;
742 QByteArray errorText;
743 QStringList arguments;
744 QString errorMessage;
749 arguments << QLatin1String("log") << QLatin1String(noColorOption)
750 << QLatin1String("--max-count=1") << QLatin1String("--pretty=format:%H");
751 if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
752 errorMessage = tr("Unable to retrieve top revision of %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
755 *revision = commandOutputFromLocal8Bit(outputTextData);
756 revision->remove(QLatin1Char('\n'));
757 } // revision desired
762 arguments << QLatin1String("branch") << QLatin1String(noColorOption);
763 if (!fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText)) {
764 errorMessage = msgCannotDetermineBranch(workingDirectory, commandOutputFromLocal8Bit(errorText));
767 /* parse output for current branch: \code
771 const QString branchPrefix = QLatin1String("* ");
772 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputTextData)) {
773 if (line.startsWith(branchPrefix)) {
775 branch->remove(0, branchPrefix.size());
779 if (branch->isEmpty()) {
780 errorMessage = msgCannotDetermineBranch(workingDirectory,
781 QString::fromLatin1("Internal error: Failed to parse output: %1").arg(commandOutputFromLocal8Bit(outputTextData)));
786 const bool failed = (revision && revision->isEmpty()) || (branch && branch->isEmpty());
787 if (failed && !errorMessage.isEmpty()) {
788 if (errorMessageIn) {
789 *errorMessageIn = errorMessage;
791 outputWindow()->appendError(errorMessage);
797 // Format an entry in a one-liner for selection list using git log.
798 bool GitClient::synchronousShortDescription(const QString &workingDirectory,
799 const QString &revision,
800 const QString &format,
801 QString *description,
802 QString *errorMessage)
804 if (Git::Constants::debug)
805 qDebug() << Q_FUNC_INFO << workingDirectory << revision;
806 QByteArray outputTextData;
807 QByteArray errorText;
808 QStringList arguments;
809 arguments << QLatin1String("log") << QLatin1String(GitClient::noColorOption)
810 << (QLatin1String("--pretty=format:") + format)
811 << QLatin1String("--max-count=1") << revision;
812 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
814 *errorMessage = tr("Unable to describe revision %1 in %2: %3").arg(revision, workingDirectory, commandOutputFromLocal8Bit(errorText));
817 *description = commandOutputFromLocal8Bit(outputTextData);
818 if (description->endsWith(QLatin1Char('\n')))
819 description->truncate(description->size() - 1);
823 // Create a default message to be used for describing stashes
824 static inline QString creatorStashMessage(const QString &keyword = QString())
826 QString rc = QCoreApplication::applicationName();
827 rc += QLatin1Char(' ');
828 if (!keyword.isEmpty()) {
830 rc += QLatin1Char(' ');
832 rc += QDateTime::currentDateTime().toString(Qt::ISODate);
836 /* Do a stash and return the message as identifier. Note that stash names (stash{n})
837 * shift as they are pushed, so, enforce the use of messages to identify them. Flags:
838 * StashPromptDescription: Prompt the user for a description message.
839 * StashImmediateRestore: Immediately re-apply this stash (used for snapshots), user keeps on working
840 * StashIgnoreUnchanged: Be quiet about unchanged repositories (used for IVersionControl's snapshots). */
842 QString GitClient::synchronousStash(const QString &workingDirectory,
843 const QString &messageKeyword /* = QString() */,
845 bool *unchanged /* =0 */)
850 bool success = false;
851 // Check for changes and stash
852 QString errorMessage;
853 switch (gitStatus(workingDirectory, false, 0, &errorMessage)) {
854 case StatusChanged: {
855 message = creatorStashMessage(messageKeyword);
857 if ((flags & StashPromptDescription)) {
858 if (!inputText(Core::ICore::instance()->mainWindow(),
859 tr("Stash Description"), tr("Description:"), &message))
862 if (!executeSynchronousStash(workingDirectory, message))
864 if ((flags & StashImmediateRestore)
865 && !synchronousStashRestore(workingDirectory, QLatin1String("stash@{0}")))
871 case StatusUnchanged:
874 if (!(flags & StashIgnoreUnchanged))
875 outputWindow()->append(msgNoChangedFiles());
878 outputWindow()->append(errorMessage);
883 if (Git::Constants::debug)
884 qDebug() << Q_FUNC_INFO << '\n' << workingDirectory << messageKeyword << "returns" << message;
888 bool GitClient::executeSynchronousStash(const QString &workingDirectory,
889 const QString &message,
890 QString *errorMessage /* = 0*/)
892 if (Git::Constants::debug)
893 qDebug() << Q_FUNC_INFO << workingDirectory;
894 QByteArray outputText;
895 QByteArray errorText;
896 QStringList arguments;
897 arguments << QLatin1String("stash");
898 if (!message.isEmpty())
899 arguments << QLatin1String("save") << message;
900 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
902 const QString msg = tr("Unable stash in %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
906 outputWindow()->append(msg);
913 // Resolve a stash name from message
914 bool GitClient::stashNameFromMessage(const QString &workingDirectory,
915 const QString &message, QString *name,
916 QString *errorMessage /* = 0 */)
919 if (message.startsWith(QLatin1String(stashNamePrefix))) {
923 // Retrieve list and find via message
924 QList<Stash> stashes;
925 if (!synchronousStashList(workingDirectory, &stashes, errorMessage))
927 foreach (const Stash &s, stashes) {
928 if (s.message == message) {
933 //: Look-up of a stash via its descriptive message failed.
934 const QString msg = tr("Unable to resolve stash message '%1' in %2").arg(message, workingDirectory);
938 outputWindow()->append(msg);
943 bool GitClient::synchronousBranchCmd(const QString &workingDirectory, QStringList branchArgs,
944 QString *output, QString *errorMessage)
946 if (Git::Constants::debug)
947 qDebug() << Q_FUNC_INFO << workingDirectory << branchArgs;
948 branchArgs.push_front(QLatin1String("branch"));
949 QByteArray outputText;
950 QByteArray errorText;
951 const bool rc = fullySynchronousGit(workingDirectory, branchArgs, &outputText, &errorText);
953 *errorMessage = tr("Unable to run a 'git branch' command in %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
956 *output = commandOutputFromLocal8Bit(outputText);
960 bool GitClient::synchronousShow(const QString &workingDirectory, const QString &id,
961 QString *output, QString *errorMessage)
963 if (Git::Constants::debug)
964 qDebug() << Q_FUNC_INFO << workingDirectory << id;
966 *errorMessage = msgCannotShow(id);
969 QStringList args(QLatin1String("show"));
970 args << QLatin1String(noColorOption) << id;
971 QByteArray outputText;
972 QByteArray errorText;
973 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
975 *errorMessage = tr("Unable to run 'git show' in %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
978 *output = commandOutputFromLocal8Bit(outputText);
982 // Retrieve list of files to be cleaned
983 bool GitClient::synchronousCleanList(const QString &workingDirectory,
984 QStringList *files, QString *errorMessage)
986 if (Git::Constants::debug)
987 qDebug() << Q_FUNC_INFO << workingDirectory;
990 args << QLatin1String("clean") << QLatin1String("--dry-run") << QLatin1String("-dxf");
991 QByteArray outputText;
992 QByteArray errorText;
993 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
995 *errorMessage = tr("Unable to run 'git clean' in %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
998 // Filter files that git would remove
999 const QString prefix = QLatin1String("Would remove ");
1000 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText))
1001 if (line.startsWith(prefix))
1002 files->push_back(line.mid(prefix.size()));
1006 bool GitClient::synchronousApplyPatch(const QString &workingDirectory,
1007 const QString &file, QString *errorMessage)
1009 if (Git::Constants::debug)
1010 qDebug() << Q_FUNC_INFO << workingDirectory;
1012 args << QLatin1String("apply") << QLatin1String("--whitespace=fix") << file;
1013 QByteArray outputText;
1014 QByteArray errorText;
1015 const bool rc = fullySynchronousGit(workingDirectory, args, &outputText, &errorText);
1017 if (!errorText.isEmpty())
1018 *errorMessage = tr("There were warnings while applying %1 to %2:\n%3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
1020 *errorMessage = tr("Unable apply patch %1 to %2: %3").arg(file, workingDirectory, commandOutputFromLocal8Bit(errorText));
1026 // Factory function to create an asynchronous command
1027 GitCommand *GitClient::createCommand(const QString &workingDirectory,
1028 VCSBase::VCSBaseEditor* editor,
1029 bool outputToWindow,
1030 int editorLineNumber)
1032 if (Git::Constants::debug)
1033 qDebug() << Q_FUNC_INFO << workingDirectory << editor;
1035 VCSBase::VCSBaseOutputWindow *outputWindow = VCSBase::VCSBaseOutputWindow::instance();
1036 GitCommand* command = new GitCommand(binary(), workingDirectory, processEnvironment(), QVariant(editorLineNumber));
1038 connect(command, SIGNAL(finished(bool,int,QVariant)), editor, SLOT(commandFinishedGotoLine(bool,int,QVariant)));
1039 if (outputToWindow) {
1040 if (editor) { // assume that the commands output is the important thing
1041 connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendDataSilently(QByteArray)));
1043 connect(command, SIGNAL(outputData(QByteArray)), outputWindow, SLOT(appendData(QByteArray)));
1046 QTC_ASSERT(editor, /**/);
1047 connect(command, SIGNAL(outputData(QByteArray)), editor, SLOT(setPlainTextDataFiltered(QByteArray)));
1051 connect(command, SIGNAL(errorText(QString)), outputWindow, SLOT(appendError(QString)));
1055 // Execute a single command
1056 GitCommand *GitClient::executeGit(const QString &workingDirectory,
1057 const QStringList &arguments,
1058 VCSBase::VCSBaseEditor* editor,
1059 bool outputToWindow,
1060 GitCommand::TerminationReportMode tm,
1061 int editorLineNumber,
1062 bool unixTerminalDisabled)
1064 outputWindow()->appendCommand(workingDirectory, QLatin1String(Constants::GIT_BINARY), arguments);
1065 GitCommand *command = createCommand(workingDirectory, editor, outputToWindow, editorLineNumber);
1066 command->addJob(arguments, m_settings.timeoutSeconds);
1067 command->setTerminationReportMode(tm);
1068 command->setUnixTerminalDisabled(unixTerminalDisabled);
1073 // Return fixed arguments required to run
1074 QStringList GitClient::binary() const
1078 args << QLatin1String("cmd.exe") << QLatin1String("/c") << m_binaryPath;
1081 return QStringList(m_binaryPath);
1085 QProcessEnvironment GitClient::processEnvironment() const
1087 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
1088 if (m_settings.adoptPath)
1089 environment.insert(QLatin1String("PATH"), m_settings.path);
1090 // Set up SSH and C locale (required by git using perl).
1091 VCSBase::VCSBasePlugin::setProcessEnvironment(&environment, false);
1095 // Synchronous git execution using Utils::SynchronousProcess, with
1096 // log windows updating.
1097 Utils::SynchronousProcessResponse
1098 GitClient::synchronousGit(const QString &workingDirectory,
1099 const QStringList &gitArguments,
1101 QTextCodec *stdOutCodec)
1103 if (Git::Constants::debug)
1104 qDebug() << "synchronousGit" << workingDirectory << gitArguments;
1105 QStringList args = binary(); // "cmd /c git" on Windows
1106 const QString executable = args.front();
1108 args.append(gitArguments);
1109 return VCSBase::VCSBasePlugin::runVCS(workingDirectory, executable, args,
1110 m_settings.timeoutSeconds * 1000,
1111 flags, stdOutCodec);
1114 bool GitClient::fullySynchronousGit(const QString &workingDirectory,
1115 const QStringList &gitArguments,
1116 QByteArray* outputText,
1117 QByteArray* errorText,
1118 bool logCommandToWindow)
1120 if (Git::Constants::debug)
1121 qDebug() << "fullySynchronousGit" << workingDirectory << gitArguments;
1123 if (logCommandToWindow)
1124 outputWindow()->appendCommand(workingDirectory, m_binaryPath, gitArguments);
1127 process.setWorkingDirectory(workingDirectory);
1128 process.setProcessEnvironment(processEnvironment());
1130 QStringList args = binary(); // "cmd /c git" on Windows
1131 const QString executable = args.front();
1133 args.append(gitArguments);
1134 process.start(executable, args);
1135 process.closeWriteChannel();
1136 if (!process.waitForStarted()) {
1138 const QString msg = QString::fromLatin1("Unable to execute '%1': %2:")
1139 .arg(binary().join(QString(QLatin1Char(' '))), process.errorString());
1140 *errorText = msg.toLocal8Bit();
1145 if (!Utils::SynchronousProcess::readDataFromProcess(process, m_settings.timeoutSeconds * 1000,
1146 outputText, errorText, true)) {
1147 errorText->append(GitCommand::msgTimeout(m_settings.timeoutSeconds).toLocal8Bit());
1148 Utils::SynchronousProcess::stopProcess(process);
1152 if (Git::Constants::debug)
1153 qDebug() << "synchronousGit ex=" << process.exitStatus() << process.exitCode();
1154 return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
1158 askWithDetailedText(QWidget *parent,
1159 const QString &title, const QString &msg,
1161 QMessageBox::StandardButton defaultButton,
1162 QMessageBox::StandardButtons buttons = QMessageBox::Yes|QMessageBox::No)
1164 QMessageBox msgBox(QMessageBox::Question, title, msg, buttons, parent);
1165 msgBox.setDetailedText(inf);
1166 msgBox.setDefaultButton(defaultButton);
1167 return msgBox.exec();
1170 // Convenience that pops up an msg box.
1171 GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory)
1173 QString errorMessage;
1174 const StashResult sr = ensureStash(workingDirectory, &errorMessage);
1175 if (sr == StashFailed)
1176 outputWindow()->appendError(errorMessage);
1180 // Ensure that changed files are stashed before a pull or similar
1181 GitClient::StashResult GitClient::ensureStash(const QString &workingDirectory, QString *errorMessage)
1183 QString statusOutput;
1184 switch (gitStatus(workingDirectory, false, &statusOutput, errorMessage)) {
1187 case StatusUnchanged:
1188 return StashUnchanged;
1193 const int answer = askWithDetailedText(m_core->mainWindow(), tr("Changes"),
1194 tr("You have modified files. Would you like to stash your changes?"),
1195 statusOutput, QMessageBox::Yes, QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel);
1197 case QMessageBox::Cancel:
1198 return StashCanceled;
1199 case QMessageBox::Yes:
1200 if (!executeSynchronousStash(workingDirectory, creatorStashMessage(QLatin1String("push")), errorMessage))
1203 case QMessageBox::No: // At your own risk, so.
1210 // Trim a git status file spec: "modified: foo .cpp" -> "modified: foo .cpp"
1211 static inline QString trimFileSpecification(QString fileSpec)
1213 const int colonIndex = fileSpec.indexOf(QLatin1Char(':'));
1214 if (colonIndex != -1) {
1215 // Collapse the sequence of spaces
1216 const int filePos = colonIndex + 2;
1217 int nonBlankPos = filePos;
1218 for ( ; fileSpec.at(nonBlankPos).isSpace(); nonBlankPos++) ;
1219 if (nonBlankPos > filePos)
1220 fileSpec.remove(filePos, nonBlankPos - filePos);
1225 GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory,
1228 QString *errorMessage,
1231 // Run 'status'. Note that git returns exitcode 1 if there are no added files.
1232 QByteArray outputText;
1233 QByteArray errorText;
1234 // @TODO: Use "--no-color" once it is supported
1235 QStringList statusArgs(QLatin1String("status"));
1237 statusArgs << QLatin1String("-u");
1238 const bool statusRc = fullySynchronousGit(workingDirectory, statusArgs, &outputText, &errorText);
1239 GitCommand::removeColorCodes(&outputText);
1241 *output = commandOutputFromLocal8Bit(outputText);
1242 const bool branchKnown = outputText.contains(kBranchIndicatorC);
1244 *onBranch = branchKnown;
1245 // Is it something really fatal?
1246 if (!statusRc && !branchKnown && !outputText.contains("# Not currently on any branch.")) {
1248 const QString error = commandOutputFromLocal8Bit(errorText);
1249 *errorMessage = tr("Unable to obtain the status: %1").arg(error);
1251 return StatusFailed;
1253 // Unchanged (output text depending on whether -u was passed)
1254 if (outputText.contains("nothing to commit")
1255 || outputText.contains("nothing added to commit but untracked files present"))
1256 return StatusUnchanged;
1257 return StatusChanged;
1260 void GitClient::launchGitK(const QString &workingDirectory)
1262 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
1263 // Locate git in (potentially) custom path. m_binaryPath can be absolute,
1264 // which will be handled correctly.
1265 QTC_ASSERT(!m_binaryPath.isEmpty(), return);
1266 const QString gitBinary = QLatin1String(Constants::GIT_BINARY);
1267 const QProcessEnvironment env = processEnvironment();
1268 const QString path = env.value(QLatin1String("PATH"));
1269 const QString fullGitBinary = Utils::SynchronousProcess::locateBinary(path, m_binaryPath);
1270 if (fullGitBinary.isEmpty()) {
1271 outwin->appendError(tr("Cannot locate %1.").arg(gitBinary));
1274 const QString gitBinDirectory = QFileInfo(fullGitBinary).absolutePath();
1276 // Launch 'wish' shell from git binary directory with the gitk located there
1277 const QString binary = gitBinDirectory + QLatin1String("/wish");
1278 QStringList arguments(gitBinDirectory + QLatin1String("/gitk"));
1280 // Simple: Run gitk from binary path
1281 const QString binary = gitBinDirectory + QLatin1String("/gitk");
1282 QStringList arguments;
1284 if (!m_settings.gitkOptions.isEmpty())
1285 arguments.append(m_settings.gitkOptions.split(QLatin1Char(' ')));
1286 outwin->appendCommand(workingDirectory, binary, arguments);
1287 // This should always use QProcess::startDetached (as not to kill
1288 // the child), but that does not have an environment parameter.
1289 bool success = false;
1290 if (m_settings.adoptPath) {
1291 QProcess *process = new QProcess(this);
1292 process->setWorkingDirectory(workingDirectory);
1293 process->setProcessEnvironment(env);
1294 process->start(binary, arguments);
1295 success = process->waitForStarted();
1297 connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
1302 success = QProcess::startDetached(binary, arguments, workingDirectory);
1305 outwin->appendError(tr("Unable to launch %1.").arg(binary));
1308 bool GitClient::getCommitData(const QString &workingDirectory,
1309 QString *commitTemplate,
1311 QString *errorMessage)
1313 if (Git::Constants::debug)
1314 qDebug() << Q_FUNC_INFO << workingDirectory;
1319 const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
1320 if (repoDirectory.isEmpty()) {
1321 *errorMessage = msgRepositoryNotFound(workingDirectory);
1325 d->panelInfo.repository = repoDirectory;
1327 QDir gitDir(repoDirectory);
1328 if (!gitDir.cd(QLatin1String(kGitDirectoryC))) {
1329 *errorMessage = tr("The repository %1 is not initialized yet.").arg(repoDirectory);
1334 const QString descriptionFile = gitDir.absoluteFilePath(QLatin1String("description"));
1335 if (QFileInfo(descriptionFile).isFile()) {
1336 QFile file(descriptionFile);
1337 if (file.open(QIODevice::ReadOnly|QIODevice::Text))
1338 d->panelInfo.description = commandOutputFromLocal8Bit(file.readAll()).trimmed();
1341 // Run status. Note that it has exitcode 1 if there are no added files.
1344 switch (gitStatus(repoDirectory, true, &output, errorMessage, &onBranch)) {
1347 *errorMessage = tr("You did not checkout a branch.");
1351 case StatusUnchanged:
1352 *errorMessage = msgNoChangedFiles();
1358 // Output looks like:
1359 // # On branch [branchname]
1360 // # Changes to be committed:
1361 // # (use "git reset HEAD <file>..." to unstage)
1363 // # modified: somefile.cpp
1364 // # new File: somenew.h
1366 // # Changed but not updated:
1367 // # (use "git add <file>..." to update what will be committed)
1369 // # modified: someother.cpp
1371 // # Untracked files:
1372 // # (use "git add <file>..." to include in what will be committed)
1374 // # list of files...
1376 if (!d->parseFilesFromStatus(output)) {
1377 *errorMessage = msgParseFilesFailed();
1380 // Filter out untracked files that are not part of the project
1381 VCSBase::VCSBaseSubmitEditor::filterUntrackedFilesOfProject(repoDirectory, &d->untrackedFiles);
1382 if (d->filesEmpty()) {
1383 *errorMessage = msgNoChangedFiles();
1387 d->panelData.author = readConfigValue(workingDirectory, QLatin1String("user.name"));
1388 d->panelData.email = readConfigValue(workingDirectory, QLatin1String("user.email"));
1390 // Get the commit template
1391 QString templateFilename = readConfigValue(workingDirectory, QLatin1String("commit.template"));
1392 if (!templateFilename.isEmpty()) {
1393 // Make relative to repository
1394 const QFileInfo templateFileInfo(templateFilename);
1395 if (templateFileInfo.isRelative())
1396 templateFilename = repoDirectory + QLatin1Char('/') + templateFilename;
1397 QFile templateFile(templateFilename);
1398 if (templateFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
1399 *commitTemplate = QString::fromLocal8Bit(templateFile.readAll());
1401 qWarning("Unable to read commit template %s: %s",
1402 qPrintable(templateFilename),
1403 qPrintable(templateFile.errorString()));
1410 bool GitClient::addAndCommit(const QString &repositoryDirectory,
1411 const GitSubmitEditorPanelData &data,
1412 const QString &messageFile,
1413 const QStringList &checkedFiles,
1414 const QStringList &origCommitFiles,
1415 const QStringList &origDeletedFiles)
1417 if (Git::Constants::debug)
1418 qDebug() << "GitClient::addAndCommit:" << repositoryDirectory << checkedFiles << origCommitFiles;
1419 const QString renamedSeparator = QLatin1String(" -> ");
1421 // Do we need to reset any files that had been added before
1422 // (did the user uncheck any previously added files)
1423 // Split up renamed files ('foo.cpp -> foo2.cpp').
1424 QStringList resetFiles = origCommitFiles.toSet().subtract(checkedFiles.toSet()).toList();
1425 for (QStringList::iterator it = resetFiles.begin(); it != resetFiles.end(); ++it) {
1426 const int renamedPos = it->indexOf(renamedSeparator);
1427 if (renamedPos != -1) {
1428 const QString newFile = it->mid(renamedPos + renamedSeparator.size());
1429 it->truncate(renamedPos);
1430 it = resetFiles.insert(++it, newFile);
1434 if (!resetFiles.isEmpty())
1435 if (!synchronousReset(repositoryDirectory, resetFiles))
1438 // Re-add all to make sure we have the latest changes, but only add those that aren't marked
1439 // for deletion. Purge out renamed files ('foo.cpp -> foo2.cpp').
1440 QStringList addFiles = checkedFiles.toSet().subtract(origDeletedFiles.toSet()).toList();
1441 for (QStringList::iterator it = addFiles.begin(); it != addFiles.end(); ) {
1442 if (it->contains(renamedSeparator)) {
1443 it = addFiles.erase(it);
1448 if (!addFiles.isEmpty())
1449 if (!synchronousAdd(repositoryDirectory, false, addFiles))
1452 // Do the final commit
1454 args << QLatin1String("commit")
1455 << QLatin1String("-F") << QDir::toNativeSeparators(messageFile);
1457 const QString &authorString = data.authorString();
1458 if (!authorString.isEmpty())
1459 args << QLatin1String("--author") << authorString;
1461 QByteArray outputText;
1462 QByteArray errorText;
1463 const bool rc = fullySynchronousGit(repositoryDirectory, args, &outputText, &errorText);
1465 outputWindow()->append(tr("Committed %n file(s).\n", 0, checkedFiles.size()));
1467 outputWindow()->appendError(tr("Unable to commit %n file(s): %1\n", 0, checkedFiles.size()).arg(commandOutputFromLocal8Bit(errorText)));
1472 /* Revert: This function can be called with a file list (to revert single
1473 * files) or a single directory (revert all). Qt Creator currently has only
1474 * 'revert single' in its VCS menus, but the code is prepared to deal with
1475 * reverting a directory pending a sophisticated selection dialog in the
1476 * VCSBase plugin. */
1478 GitClient::RevertResult GitClient::revertI(QStringList files, bool *ptrToIsDirectory, QString *errorMessage)
1480 if (Git::Constants::debug)
1481 qDebug() << Q_FUNC_INFO << files;
1484 return RevertCanceled;
1486 // Figure out the working directory
1487 const QFileInfo firstFile(files.front());
1488 const bool isDirectory = firstFile.isDir();
1489 if (ptrToIsDirectory)
1490 *ptrToIsDirectory = isDirectory;
1491 const QString workingDirectory = isDirectory ? firstFile.absoluteFilePath() : firstFile.absolutePath();
1493 const QString repoDirectory = GitClient::findRepositoryForDirectory(workingDirectory);
1494 if (repoDirectory.isEmpty()) {
1495 *errorMessage = msgRepositoryNotFound(workingDirectory);
1496 return RevertFailed;
1499 // Check for changes
1501 switch (gitStatus(repoDirectory, false, &output, errorMessage)) {
1504 case StatusUnchanged:
1505 return RevertUnchanged;
1507 return RevertFailed;
1510 if (!data.parseFilesFromStatus(output)) {
1511 *errorMessage = msgParseFilesFailed();
1512 return RevertFailed;
1515 // If we are looking at files, make them relative to the repository
1516 // directory to match them in the status output list.
1518 const QDir repoDir(repoDirectory);
1519 const QStringList::iterator cend = files.end();
1520 for (QStringList::iterator it = files.begin(); it != cend; ++it)
1521 *it = repoDir.relativeFilePath(*it);
1524 // From the status output, determine all modified [un]staged files.
1525 const QString modifiedState = QLatin1String("modified");
1526 const QStringList allStagedFiles = data.stagedFileNames(modifiedState);
1527 const QStringList allUnstagedFiles = data.unstagedFileNames(modifiedState);
1528 // Unless a directory was passed, filter all modified files for the
1529 // argument file list.
1530 QStringList stagedFiles = allStagedFiles;
1531 QStringList unstagedFiles = allUnstagedFiles;
1533 const QSet<QString> filesSet = files.toSet();
1534 stagedFiles = allStagedFiles.toSet().intersect(filesSet).toList();
1535 unstagedFiles = allUnstagedFiles.toSet().intersect(filesSet).toList();
1537 if (Git::Constants::debug)
1538 qDebug() << Q_FUNC_INFO << data.stagedFiles << data.unstagedFiles << allStagedFiles << allUnstagedFiles << stagedFiles << unstagedFiles;
1540 if (stagedFiles.empty() && unstagedFiles.empty())
1541 return RevertUnchanged;
1543 // Ask to revert (to do: Handle lists with a selection dialog)
1544 const QMessageBox::StandardButton answer
1545 = QMessageBox::question(m_core->mainWindow(),
1547 tr("The file has been changed. Do you want to revert it?"),
1548 QMessageBox::Yes|QMessageBox::No,
1550 if (answer == QMessageBox::No)
1551 return RevertCanceled;
1553 // Unstage the staged files
1554 if (!stagedFiles.empty() && !synchronousReset(repoDirectory, stagedFiles, errorMessage))
1555 return RevertFailed;
1557 if (!synchronousCheckoutFiles(repoDirectory, stagedFiles + unstagedFiles, QString(), errorMessage))
1558 return RevertFailed;
1562 void GitClient::revert(const QStringList &files)
1565 QString errorMessage;
1566 switch (revertI(files, &isDirectory, &errorMessage)) {
1568 m_plugin->gitVersionControl()->emitFilesChanged(files);
1570 case RevertCanceled:
1572 case RevertUnchanged: {
1573 const QString msg = (isDirectory || files.size() > 1) ? msgNoChangedFiles() : tr("The file is not modified.");
1574 outputWindow()->append(msg);
1578 outputWindow()->append(errorMessage);
1583 bool GitClient::synchronousPull(const QString &workingDirectory)
1585 return synchronousPull(workingDirectory, m_settings.pullRebase);
1588 bool GitClient::synchronousPull(const QString &workingDirectory, bool rebase)
1590 QStringList arguments(QLatin1String("pull"));
1592 arguments << QLatin1String("--rebase");
1593 // Disable UNIX terminals to suppress SSH prompting.
1594 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
1595 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
1596 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, arguments, flags);
1597 // Notify about changed files or abort the rebase.
1598 const bool ok = resp.result == Utils::SynchronousProcessResponse::Finished;
1600 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
1603 syncAbortPullRebase(workingDirectory);
1608 void GitClient::syncAbortPullRebase(const QString &workingDir)
1610 // Abort rebase to clean if something goes wrong
1611 VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance();
1612 outwin->appendError(tr("The command 'git pull --rebase' failed, aborting rebase."));
1613 QStringList arguments;
1614 arguments << QLatin1String("rebase") << QLatin1String("--abort");
1617 const bool rc = fullySynchronousGit(workingDir, arguments, &stdOut, &stdErr, true);
1618 outwin->append(commandOutputFromLocal8Bit(stdOut));
1620 outwin->appendError(commandOutputFromLocal8Bit(stdErr));
1623 // Subversion: git svn
1624 void GitClient::synchronousSubversionFetch(const QString &workingDirectory)
1627 args << QLatin1String("svn") << QLatin1String("fetch");
1628 // Disable UNIX terminals to suppress SSH prompting.
1629 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
1630 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
1631 const Utils::SynchronousProcessResponse resp = synchronousGit(workingDirectory, args, flags);
1632 // Notify about changes.
1633 if (resp.result == Utils::SynchronousProcessResponse::Finished)
1634 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
1637 void GitClient::subversionLog(const QString &workingDirectory)
1639 if (Git::Constants::debug)
1640 qDebug() << "subversionLog" << workingDirectory;
1642 QStringList arguments;
1643 arguments << QLatin1String("svn") << QLatin1String("log");
1644 if (m_settings.logCount > 0)
1645 arguments << (QLatin1String("--limit=") + QString::number(m_settings.logCount));
1647 // Create a command editor, no highlighting or interaction.
1648 const QString title = tr("Git SVN Log");
1649 const QString editorId = QLatin1String(Git::Constants::C_GIT_COMMAND_LOG_EDITOR);
1650 const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, QStringList());
1651 VCSBase::VCSBaseEditor *editor = createVCSEditor(editorId, title, sourceFile, false, "svnLog", sourceFile);
1652 executeGit(workingDirectory, arguments, editor);
1655 bool GitClient::synchronousPush(const QString &workingDirectory)
1657 // Disable UNIX terminals to suppress SSH prompting.
1658 const unsigned flags = VCSBase::VCSBasePlugin::SshPasswordPrompt|VCSBase::VCSBasePlugin::ShowStdOutInLogWindow
1659 |VCSBase::VCSBasePlugin::ShowSuccessMessage;
1660 const Utils::SynchronousProcessResponse resp =
1661 synchronousGit(workingDirectory, QStringList(QLatin1String("push")), flags);
1662 return resp.result == Utils::SynchronousProcessResponse::Finished;
1665 QString GitClient::msgNoChangedFiles()
1667 return tr("There are no modified files.");
1670 void GitClient::stashPop(const QString &workingDirectory)
1672 QStringList arguments(QLatin1String("stash"));
1673 arguments << QLatin1String("pop");
1674 GitCommand *cmd = executeGit(workingDirectory, arguments, 0, true);
1675 connectRepositoryChanged(workingDirectory, cmd);
1678 bool GitClient::synchronousStashRestore(const QString &workingDirectory,
1679 const QString &stash,
1680 const QString &branch /* = QString()*/,
1681 QString *errorMessage)
1683 QStringList arguments(QLatin1String("stash"));
1684 if (branch.isEmpty()) {
1685 arguments << QLatin1String("apply") << stash;
1687 arguments << QLatin1String("branch") << branch << stash;
1689 QByteArray outputText;
1690 QByteArray errorText;
1691 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
1693 const QString stdErr = commandOutputFromLocal8Bit(errorText);
1694 const QString msg = branch.isEmpty() ?
1695 tr("Unable to restore stash %1: %2").arg(workingDirectory, stdErr) :
1696 tr("Unable to restore stash %1 to branch %2: %3").arg(workingDirectory, branch, stdErr);
1698 *errorMessage = msg;
1700 outputWindow()->append(msg);
1704 QString output = commandOutputFromLocal8Bit(outputText);
1705 if (!output.isEmpty())
1706 outputWindow()->append(output);
1707 GitPlugin::instance()->gitVersionControl()->emitRepositoryChanged(workingDirectory);
1711 bool GitClient::synchronousStashRemove(const QString &workingDirectory,
1712 const QString &stash /* = QString() */,
1713 QString *errorMessage /* = 0 */)
1715 QStringList arguments(QLatin1String("stash"));
1716 if (stash.isEmpty()) {
1717 arguments << QLatin1String("clear");
1719 arguments << QLatin1String("drop") << stash;
1721 QByteArray outputText;
1722 QByteArray errorText;
1723 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
1725 const QString stdErr = commandOutputFromLocal8Bit(errorText);
1726 const QString msg = stash.isEmpty() ?
1727 tr("Unable to remove stashes of %1: %2").arg(workingDirectory, stdErr) :
1728 tr("Unable to remove stash %1 of %2: %3").arg(stash, workingDirectory, stdErr);
1730 *errorMessage = msg;
1732 outputWindow()->append(msg);
1736 QString output = commandOutputFromLocal8Bit(outputText);
1737 if (!output.isEmpty())
1738 outputWindow()->append(output);
1742 void GitClient::branchList(const QString &workingDirectory)
1744 QStringList arguments(QLatin1String("branch"));
1745 arguments << QLatin1String("-r") << QLatin1String(noColorOption);
1746 executeGit(workingDirectory, arguments, 0, true);
1749 void GitClient::stashList(const QString &workingDirectory)
1751 QStringList arguments(QLatin1String("stash"));
1752 arguments << QLatin1String("list") << QLatin1String(noColorOption);
1753 executeGit(workingDirectory, arguments, 0, true);
1756 bool GitClient::synchronousStashList(const QString &workingDirectory,
1757 QList<Stash> *stashes,
1758 QString *errorMessage /* = 0 */)
1761 QStringList arguments(QLatin1String("stash"));
1762 arguments << QLatin1String("list") << QLatin1String(noColorOption);
1763 QByteArray outputText;
1764 QByteArray errorText;
1765 const bool rc = fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText);
1767 const QString msg = tr("Unable retrieve stash list of %1: %2").arg(workingDirectory, commandOutputFromLocal8Bit(errorText));
1769 *errorMessage = msg;
1771 outputWindow()->append(msg);
1776 foreach(const QString &line, commandOutputLinesFromLocal8Bit(outputText))
1777 if (stash.parseStashLine(line))
1778 stashes->push_back(stash);
1779 if (Git::Constants::debug)
1780 qDebug() << Q_FUNC_INFO << *stashes;
1784 QString GitClient::readConfig(const QString &workingDirectory, const QStringList &configVar)
1786 QStringList arguments;
1787 arguments << QLatin1String("config") << configVar;
1789 QByteArray outputText;
1790 QByteArray errorText;
1791 if (fullySynchronousGit(workingDirectory, arguments, &outputText, &errorText, false))
1792 return commandOutputFromLocal8Bit(outputText);
1796 // Read a single-line config value, return trimmed
1797 QString GitClient::readConfigValue(const QString &workingDirectory, const QString &configVar)
1799 return readConfig(workingDirectory, QStringList(configVar)).remove(QLatin1Char('\n'));
1802 GitSettings GitClient::settings() const
1807 void GitClient::setSettings(const GitSettings &s)
1809 if (s != m_settings) {
1811 if (QSettings *coreSettings = m_core->settings())
1812 m_settings.toSettings(coreSettings);
1813 m_binaryPath = m_settings.gitBinaryPath();
1814 m_cachedGitVersion = 0u;
1815 m_hasCachedGitVersion = false;
1819 void GitClient::connectRepositoryChanged(const QString & repository, GitCommand *cmd)
1821 // Bind command success termination with repository to changed signal
1822 if (!m_repositoryChangedSignalMapper) {
1823 m_repositoryChangedSignalMapper = new QSignalMapper(this);
1824 connect(m_repositoryChangedSignalMapper, SIGNAL(mapped(QString)),
1825 m_plugin->gitVersionControl(), SIGNAL(repositoryChanged(QString)));
1827 m_repositoryChangedSignalMapper->setMapping(cmd, repository);
1828 connect(cmd, SIGNAL(success()), m_repositoryChangedSignalMapper, SLOT(map()),
1829 Qt::QueuedConnection);
1832 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
1833 unsigned GitClient::gitVersion(bool silent, QString *errorMessage /* = 0 */)
1835 if (!m_hasCachedGitVersion) {
1836 // Do not execute repeatedly if that fails (due to git
1837 // not being installed) until settings are changed.
1838 m_cachedGitVersion = synchronousGitVersion(silent, errorMessage);
1839 m_hasCachedGitVersion = true;
1841 return m_cachedGitVersion;
1844 QString GitClient::gitVersionString(bool silent, QString *errorMessage)
1846 if (const unsigned version = gitVersion(silent, errorMessage)) {
1848 QTextStream(&rc) << (version >> 16) << '.'
1849 << (0xFF & (version >> 8)) << '.'
1850 << (version & 0xFF);
1856 // determine version as '(major << 16) + (minor << 8) + patch' or 0.
1857 unsigned GitClient::synchronousGitVersion(bool silent, QString *errorMessage /* = 0 */)
1859 // run git --version
1860 QByteArray outputText;
1861 QByteArray errorText;
1862 const bool rc = fullySynchronousGit(QString(), QStringList("--version"), &outputText, &errorText);
1864 const QString msg = tr("Unable to determine git version: %1").arg(commandOutputFromLocal8Bit(errorText));
1866 *errorMessage = msg;
1869 outputWindow()->append(msg);
1871 outputWindow()->appendError(msg);
1876 // cut 'git version 1.6.5.1.sha'
1877 const QString output = commandOutputFromLocal8Bit(outputText);
1878 const QRegExp versionPattern(QLatin1String("^[^\\d]+([\\d])\\.([\\d])\\.([\\d]).*$"));
1879 QTC_ASSERT(versionPattern.isValid(), return 0);
1880 QTC_ASSERT(versionPattern.exactMatch(output), return 0);
1881 const unsigned major = versionPattern.cap(1).toUInt();
1882 const unsigned minor = versionPattern.cap(2).toUInt();
1883 const unsigned patch = versionPattern.cap(3).toUInt();
1884 return version(major, minor, patch);
1887 } // namespace Internal