From bcba9ea604e33c2d26f4180646e2875eb8766126 Mon Sep 17 00:00:00 2001
From: Friedemann Kleint <Friedemann.Kleint@nokia.com>
Date: Thu, 7 Jan 2010 17:29:14 +0100
Subject: [PATCH] VCS [git]: Annotate previous version/single filelog
 annotation. Task-number: QTCREATORBUG-503

---
 src/plugins/git/gitclient.cpp | 154 ++++++++++++++++++++++++++++++++--
 src/plugins/git/gitclient.h   |  20 ++++-
 src/plugins/git/giteditor.cpp |  26 +++++-
 src/plugins/git/giteditor.h   |   4 +-
 src/plugins/git/gitplugin.cpp |   4 +-
 5 files changed, 195 insertions(+), 13 deletions(-)

diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp
index a323454575f..ff152c5b1b6 100644
--- a/src/plugins/git/gitclient.cpp
+++ b/src/plugins/git/gitclient.cpp
@@ -173,6 +173,8 @@ VCSBase::VCSBaseEditor
         outputEditor = m_core->editorManager()->openEditorWithContents(kind, &title, m_msgWait);
         outputEditor->file()->setProperty(registerDynamicProperty, dynamicPropertyValue);
         rc = VCSBase::VCSBaseEditor::getVcsBaseEditor(outputEditor);
+        connect(rc, SIGNAL(annotateRevisionRequested(QString,QString,int)),
+                this, SLOT(slotBlameRevisionRequested(QString,QString,int)));
         QTC_ASSERT(rc, return 0);
         rc->setSource(source);
         if (setSourceCodec)
@@ -258,7 +260,7 @@ void GitClient::status(const QString &workingDirectory)
             Qt::QueuedConnection);
 }
 
-void GitClient::log(const QString &workingDirectory, const QStringList &fileNames)
+void GitClient::log(const QString &workingDirectory, const QStringList &fileNames, bool enableAnnotationContextMenu)
 {
     if (Git::Constants::debug)
         qDebug() << "log" << workingDirectory << fileNames;
@@ -278,6 +280,7 @@ void GitClient::log(const QString &workingDirectory, const QStringList &fileName
     const QString kind = QLatin1String(Git::Constants::GIT_LOG_EDITOR_KIND);
     const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileNames);
     VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, false, "logFileName", sourceFile);
+    editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
     executeGit(workingDirectory, arguments, editor);
 }
 
@@ -297,7 +300,21 @@ void GitClient::show(const QString &source, const QString &id)
     executeGit(workDir, arguments, editor);
 }
 
-void GitClient::blame(const QString &workingDirectory, const QString &fileName, int lineNumber /* = -1 */)
+void GitClient::slotBlameRevisionRequested(const QString &source, QString change, int lineNumber)
+{
+    // This might be invoked with a verbose revision description
+    // "SHA1 author subject" from the annotation context menu. Strip the rest.
+    const int blankPos = change.indexOf(QLatin1Char(' '));
+    if (blankPos != -1)
+        change.truncate(blankPos);
+    const QFileInfo fi(source);
+    blame(fi.absolutePath(), fi.fileName(), change, lineNumber);
+}
+
+void GitClient::blame(const QString &workingDirectory,
+                      const QString &fileName,
+                      const QString &revision /* = QString() */,
+                      int lineNumber /* = -1 */)
 {
     if (Git::Constants::debug)
         qDebug() << "blame" << workingDirectory << fileName << lineNumber;
@@ -306,12 +323,14 @@ void GitClient::blame(const QString &workingDirectory, const QString &fileName,
     if (m_plugin->settings().spaceIgnorantBlame)
         arguments << QLatin1String("-w");
     arguments << QLatin1String("--") << fileName;
-
+    if (!revision.isEmpty())
+        arguments << revision;
     const QString kind = QLatin1String(Git::Constants::GIT_BLAME_EDITOR_KIND);
-    const QString title = tr("Git Blame %1").arg(fileName);
+    const QString id = VCSBase::VCSBaseEditor::getTitleId(workingDirectory, QStringList(fileName), revision);
+    const QString title = tr("Git Blame %1").arg(id);
     const QString sourceFile = VCSBase::VCSBaseEditor::getSource(workingDirectory, fileName);
 
-    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, true, "blameFileName", sourceFile);
+    VCSBase::VCSBaseEditor *editor = createVCSEditor(kind, title, sourceFile, true, "blameFileName", id);
     executeGit(workingDirectory, arguments, editor, false, GitCommand::NoReport, lineNumber);
 }
 
@@ -423,6 +442,131 @@ bool GitClient::synchronousCheckout(const QString &workingDirectory,
     return true;
 }
 
+static inline QString msgParentRevisionFailed(const QString &workingDirectory,
+                                              const QString &revision,
+                                              const QString &why)
+{
+    return GitClient::tr("Unable to find parent revisions of %1 in %2: %3").arg(revision, workingDirectory, why);
+}
+
+static inline QString msgInvalidRevision()
+{
+    return GitClient::tr("Invalid revision");
+}
+
+// Split a line of "<commit> <parent1> ..." to obtain parents from "rev-list" or "log".
+static inline bool splitCommitParents(const QString &line,
+                                      QString *commit = 0,
+                                      QStringList *parents = 0)
+{
+    if (commit)
+        commit->clear();
+    if (parents)
+        parents->clear();
+    QStringList tokens = line.trimmed().split(QLatin1Char(' '));
+    if (tokens.size() < 2)
+        return false;
+    if (commit)
+        *commit = tokens.front();
+    tokens.pop_front();
+    if (parents)
+        *parents = tokens;
+    return true;
+}
+
+// Find out the immediate parent revisions of a revision of the repository.
+// Might be several in case of merges.
+bool GitClient::synchronousParentRevisions(const QString &workingDirectory,
+                                           const QStringList &files /* = QStringList() */,
+                                           const QString &revision,
+                                           QStringList *parents,
+                                           QString *errorMessage)
+{
+    if (Git::Constants::debug)
+        qDebug() << Q_FUNC_INFO << workingDirectory << revision;
+    QByteArray outputTextData;
+    QByteArray errorText;
+    QStringList arguments;
+    arguments << QLatin1String("rev-list") << QLatin1String(GitClient::noColorOption)
+              << QLatin1String("--parents") << QLatin1String("--max-count=1") << revision;
+    if (!files.isEmpty()) {
+        arguments.append(QLatin1String("--"));
+        arguments.append(files);
+    }
+    const bool rc = synchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
+    if (!rc) {
+        *errorMessage = msgParentRevisionFailed(workingDirectory, revision, QString::fromLocal8Bit(errorText));
+        return false;
+    }
+    // Should result in one line of blank-delimited revisions, specifying current first
+    // unless it is top.
+    QString outputText = QString::fromLocal8Bit(outputTextData);
+    outputText.remove(QLatin1Char('\r'));
+    outputText.remove(QLatin1Char('\n'));
+    if (!splitCommitParents(outputText, 0, parents)) {
+        *errorMessage = msgParentRevisionFailed(workingDirectory, revision, msgInvalidRevision());
+        return false;
+    }
+    if (Git::Constants::debug)
+        qDebug() << workingDirectory << files << revision << "->" << *parents;
+    return true;
+}
+
+// Short SHA1, author, subject
+static const char defaultShortLogFormatC[] = "%h (%an \"%s\")";
+
+bool GitClient::synchronousShortDescription(const QString &workingDirectory, const QString &revision,
+                                    QString *description, QString *errorMessage)
+{
+    // Short SHA 1, author, subject
+    return synchronousShortDescription(workingDirectory, revision,
+                               QLatin1String(defaultShortLogFormatC),
+                               description, errorMessage);
+}
+
+// Convenience working on a list of revisions
+bool GitClient::synchronousShortDescriptions(const QString &workingDirectory, const QStringList &revisions,
+                                            QStringList *descriptions, QString *errorMessage)
+{
+    descriptions->clear();
+    foreach (const QString &revision, revisions) {
+        QString description;
+        if (!synchronousShortDescription(workingDirectory, revision, &description, errorMessage)) {
+            descriptions->clear();
+            return false;
+        }
+        descriptions->push_back(description);
+    }
+    return true;
+}
+
+// Format an entry in a one-liner for selection list using git log.
+bool GitClient::synchronousShortDescription(const QString &workingDirectory,
+                                    const QString &revision,
+                                    const QString &format,
+                                    QString *description,
+                                    QString *errorMessage)
+{
+    if (Git::Constants::debug)
+        qDebug() << Q_FUNC_INFO << workingDirectory << revision;
+    QByteArray outputTextData;
+    QByteArray errorText;
+    QStringList arguments;
+    arguments << QLatin1String("log") << QLatin1String(GitClient::noColorOption)
+              << (QLatin1String("--pretty=format:") + format)
+              << QLatin1String("--max-count=1") << revision;
+    const bool rc = synchronousGit(workingDirectory, arguments, &outputTextData, &errorText);
+    if (!rc) {
+        *errorMessage = tr("Unable to describe revision %1 in %2: %3").arg(revision, workingDirectory, QString::fromLocal8Bit(errorText));
+        return false;
+    }
+    *description = QString::fromLocal8Bit(outputTextData);
+    description->remove(QLatin1Char('\r'));
+    if (description->endsWith(QLatin1Char('\n')))
+        description->truncate(description->size() - 1);
+    return true;
+}
+
 bool GitClient::synchronousStash(const QString &workingDirectory, QString *errorMessage)
 {
     if (Git::Constants::debug)
diff --git a/src/plugins/git/gitclient.h b/src/plugins/git/gitclient.h
index ef53fd3ff88..c086e88a24b 100644
--- a/src/plugins/git/gitclient.h
+++ b/src/plugins/git/gitclient.h
@@ -79,8 +79,10 @@ public:
               const QStringList &unstagedFileNames, const QStringList &stagedFileNames= QStringList());
 
     void status(const QString &workingDirectory);
-    void log(const QString &workingDirectory, const QStringList &fileNames);
-    void blame(const QString &workingDirectory, const QString &fileName, int lineNumber = -1);
+    void log(const QString &workingDirectory, const QStringList &fileNames,
+             bool enableAnnotationContextMenu = false);
+    void blame(const QString &workingDirectory, const QString &fileName,
+               const QString &revision = QString(), int lineNumber = -1);
     void showCommit(const QString &workingDirectory, const QString &commit);
     void checkout(const QString &workingDirectory, const QString &file);
     void checkoutBranch(const QString &workingDirectory, const QString &branch);
@@ -95,6 +97,17 @@ public:
                               QString *output, QString *errorMessage);
     bool synchronousShow(const QString &workingDirectory, const QString &id,
                               QString *output, QString *errorMessage);
+    bool synchronousParentRevisions(const QString &workingDirectory,
+                                    const QStringList &files /* = QStringList() */,
+                                    const QString &revision,
+                                    QStringList *parents,
+                                    QString *errorMessage);
+    bool synchronousShortDescription(const QString &workingDirectory, const QString &revision,
+                                     QString *description, QString *errorMessage);
+    bool synchronousShortDescription(const QString &workingDirectory, const QString &revision,
+                                     const QString &format, QString *description, QString *errorMessage);
+    bool synchronousShortDescriptions(const QString &workingDirectory, const QStringList &revisions,
+                                      QStringList *descriptions, QString *errorMessage);
 
     void pull(const QString &workingDirectory);
     void push(const QString &workingDirectory);
@@ -145,6 +158,9 @@ public:
 public slots:
     void show(const QString &source, const QString &id);
 
+private slots:
+    void slotBlameRevisionRequested(const QString &source, QString change, int lineNumber);
+
 private:
     VCSBase::VCSBaseEditor *createVCSEditor(const QString &kind,
                                                  QString title,
diff --git a/src/plugins/git/giteditor.cpp b/src/plugins/git/giteditor.cpp
index 6dd67f86d9e..dd124773aef 100644
--- a/src/plugins/git/giteditor.cpp
+++ b/src/plugins/git/giteditor.cpp
@@ -32,13 +32,14 @@
 #include "annotationhighlighter.h"
 #include "gitconstants.h"
 #include "gitplugin.h"
+#include "gitclient.h"
 #include "gitsettings.h"
 #include <QtCore/QTextCodec>
 
 #include <coreplugin/editormanager/editormanager.h>
 #include <utils/qtcassert.h>
 #include <vcsbase/diffhighlighter.h>
-
+#include <vcsbase/vcsbaseoutputwindow.h>
 #include <QtCore/QDebug>
 #include <QtCore/QDir>
 #include <QtCore/QFileInfo>
@@ -64,6 +65,7 @@ GitEditor::GitEditor(const VCSBase::VCSBaseEditorParameters *type,
 {
     QTC_ASSERT(m_changeNumberPattern8.isValid(), return);
     QTC_ASSERT(m_changeNumberPattern40.isValid(), return);
+    setAnnotateRevisionTextFormat(tr("Blame %1"));
     if (Git::Constants::debug)
         qDebug() << "GitEditor::GitEditor" << type->type << type->kind;
 }
@@ -186,6 +188,28 @@ void GitEditor::commandFinishedGotoLine(bool ok, const QVariant &v)
     }
 }
 
+QStringList GitEditor::annotationPreviousVersions(const QString &revision) const
+{
+    QStringList revisions;
+    QString errorMessage;
+    GitClient *client = GitPlugin::instance()->gitClient();
+    const QFileInfo fi(source());
+    const QString workingDirectory = fi.absolutePath();
+    // Get the SHA1's of the file.
+    if (!client->synchronousParentRevisions(workingDirectory, QStringList(fi.fileName()),
+                                            revision, &revisions, &errorMessage)) {
+        VCSBase::VCSBaseOutputWindow::instance()->appendSilently(errorMessage);
+        return QStringList();
+    }
+    // Format verbose, SHA1 being first token
+    QStringList descriptions;
+    if (!client->synchronousShortDescriptions(workingDirectory, revisions, &descriptions, &errorMessage)) {
+        VCSBase::VCSBaseOutputWindow::instance()->appendSilently(errorMessage);
+        return QStringList();
+    }
+    return descriptions;
+}
+
 } // namespace Internal
 } // namespace Git
 
diff --git a/src/plugins/git/giteditor.h b/src/plugins/git/giteditor.h
index 29e9e75aa19..02e0b24434c 100644
--- a/src/plugins/git/giteditor.h
+++ b/src/plugins/git/giteditor.h
@@ -41,8 +41,6 @@ QT_END_NAMESPACE
 namespace Git {
 namespace Internal {
 
-class GitPlugin;
-
 class GitEditor : public VCSBase::VCSBaseEditor
 {
     Q_OBJECT
@@ -62,10 +60,10 @@ private:
     virtual VCSBase::DiffHighlighter *createDiffHighlighter() const;
     virtual VCSBase::BaseAnnotationHighlighter *createAnnotationHighlighter(const QSet<QString> &changes) const;
     virtual QString fileNameFromDiffSpecification(const QTextBlock &diffFileName) const;
+    virtual QStringList annotationPreviousVersions(const QString &revision) const;
 
     const QRegExp m_changeNumberPattern8;
     const QRegExp m_changeNumberPattern40;
-    GitPlugin *m_plugin;
 };
 
 } // namespace Git
diff --git a/src/plugins/git/gitplugin.cpp b/src/plugins/git/gitplugin.cpp
index 30d44c7a73c..eb1d8350266 100644
--- a/src/plugins/git/gitplugin.cpp
+++ b/src/plugins/git/gitplugin.cpp
@@ -416,7 +416,7 @@ void GitPlugin::logFile()
 {
     const VCSBase::VCSBasePluginState state = currentState();
     QTC_ASSERT(state.hasFile(), return)
-    m_gitClient->log(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()));
+    m_gitClient->log(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()), true);
 }
 
 void GitPlugin::blameFile()
@@ -424,7 +424,7 @@ void GitPlugin::blameFile()
     const VCSBase::VCSBasePluginState state = currentState();
     QTC_ASSERT(state.hasFile(), return)
     const int lineNumber = VCSBase::VCSBaseEditor::lineNumberOfCurrentEditor(state.currentFile());
-    m_gitClient->blame(state.currentFileTopLevel(), state.relativeCurrentFile(), lineNumber);
+    m_gitClient->blame(state.currentFileTopLevel(), state.relativeCurrentFile(), QString(), lineNumber);
 }
 
 void GitPlugin::logProject()
-- 
GitLab