changeset 548:dca5bd5b2a06

Merge from branch "fswatcher"
author Chris Cannam
date Tue, 14 Feb 2012 17:55:39 +0000
parents a4e699d32a9a (current diff) 9f91d1b2ed51 (diff)
children 06f7ae09015f
files
diffstat 9 files changed, 537 insertions(+), 202 deletions(-) [+]
line wrap: on
line diff
--- a/easyhg.pro	Fri Feb 10 13:08:07 2012 +0000
+++ b/easyhg.pro	Tue Feb 14 17:55:39 2012 +0000
@@ -1,5 +1,5 @@
 
-CONFIG += debug
+CONFIG += release
 
 TEMPLATE = app
 TARGET = EasyMercurial
@@ -64,7 +64,8 @@
     src/annotatedialog.h \
     src/hgignoredialog.h \
     src/versiontester.h \
-    src/squeezedlabel.h
+    src/squeezedlabel.h \
+    src/fswatcher.h
 SOURCES = \
     src/main.cpp \
     src/mainwindow.cpp \
@@ -101,7 +102,8 @@
     src/annotatedialog.cpp \
     src/hgignoredialog.cpp \
     src/versiontester.cpp \
-    src/squeezedlabel.cpp
+    src/squeezedlabel.cpp \
+    src/fswatcher.cpp
 
 macx-* {
     SOURCES += src/common_osx.mm
--- a/src/filestates.cpp	Fri Feb 10 13:08:07 2012 +0000
+++ b/src/filestates.cpp	Tue Feb 14 17:55:39 2012 +0000
@@ -142,6 +142,18 @@
     return (m_stateMap.contains(file));
 }
 
+QStringList FileStates::trackedFiles() const
+{
+    QStringList all;
+    all << filesInState(Modified);
+    all << filesInState(Added);
+    all << filesInState(Removed);
+    all << filesInState(InConflict);
+    all << filesInState(Missing);
+    all << filesInState(Clean);
+    return all;
+}
+
 FileStates::Activities FileStates::activitiesSupportedBy(State s)
 {
     Activities a;
--- a/src/filestates.h	Fri Feb 10 13:08:07 2012 +0000
+++ b/src/filestates.h	Tue Feb 14 17:55:39 2012 +0000
@@ -52,6 +52,8 @@
     State stateOf(QString file) const;
     bool isKnown(QString file) const;
 
+    QStringList trackedFiles() const;
+
     enum Activity {
 
         // These are in the order in which they want to be listed in
--- a/src/filestatuswidget.cpp	Fri Feb 10 13:08:07 2012 +0000
+++ b/src/filestatuswidget.cpp	Tue Feb 14 17:55:39 2012 +0000
@@ -178,6 +178,13 @@
     return m_showAllFiles->isChecked();
 }
 
+bool FileStatusWidget::shouldShow(FileStates::State s) const
+{
+    if (shouldShowAll()) return true;
+    else return (s != FileStates::Clean &&
+                 s != FileStates::Ignored);
+}
+
 QString FileStatusWidget::labelFor(FileStates::State s, bool addHighlightExplanation)
 {
     QSettings settings;
@@ -193,11 +200,9 @@
                 .arg(m_simpleLabels[s])
                 .arg(m_descriptions[s]);
         }
-    } else {
-        return QString("<qt><b>%1</b></qt>")
-            .arg(m_simpleLabels[s]);
     }
-    settings.endGroup();
+    return QString("<qt><b>%1</b></qt>")
+        .arg(m_simpleLabels[s]);
 }
 
 void FileStatusWidget::setNoModificationsLabelText()
@@ -425,6 +430,12 @@
 
         QListWidget *w = m_stateListMap[s];
         w->clear();
+
+        if (!shouldShow(s)) {
+            w->parentWidget()->hide();
+            continue;
+        }
+
         QStringList files = m_fileStates.filesInState(s);
 
         QStringList highPriority, lowPriority;
--- a/src/filestatuswidget.h	Fri Feb 10 13:08:07 2012 +0000
+++ b/src/filestatuswidget.h	Tue Feb 14 17:55:39 2012 +0000
@@ -55,6 +55,7 @@
     QStringList getSelectedRemovableFiles() const;
 
     bool shouldShowAll() const;
+    bool shouldShow(FileStates::State) const;
 
 signals:
     void selectionChanged();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fswatcher.cpp	Tue Feb 14 17:55:39 2012 +0000
@@ -0,0 +1,271 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    EasyMercurial
+
+    Based on hgExplorer by Jari Korhonen
+    Copyright (c) 2010 Jari Korhonen
+    Copyright (c) 2011 Chris Cannam
+    Copyright (c) 2011 Queen Mary, University of London
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "fswatcher.h"
+#include "debug.h"
+
+#include <QMutexLocker>
+#include <QDir>
+
+#include <deque>
+
+#define DEBUG_FSWATCHER 1
+
+/*
+ * Watching the filesystem is trickier than it seems at first glance.
+ *
+ * We ideally should watch every directory, and every file that is
+ * tracked by Hg. If a new file is created in a directory, then we
+ * need to respond in order to show it as a potential candidate to be
+ * added.
+ *
+ * Complicating matters though is that Hg itself might modify the
+ * filesystem. This can happen even in "read-only" operations: for
+ * example, hg stat creates files called hg-checklink and hg-checkexec
+ * to test properties of the filesystem. So we need to know to ignore
+ * those files; unfortunately, when watching a directory (which is how
+ * we find out about the creation of new files) we are notified only
+ * that the directory has changed -- we aren't told what changed.
+ *
+ * This means that, when a directory changes, we need to rescan the
+ * directory to learn whether the set of files in it _excluding_ files
+ * matching our ignore patterns differs from the previous scan, and
+ * ignore the change if it doesn't.
+ */
+
+FsWatcher::FsWatcher() :
+    m_lastToken(0),
+    m_lastCounter(0)
+{
+    connect(&m_watcher, SIGNAL(directoryChanged(QString)),
+	    this, SLOT(fsDirectoryChanged(QString)));
+    connect(&m_watcher, SIGNAL(fileChanged(QString)),
+	    this, SLOT(fsFileChanged(QString)));
+}
+
+FsWatcher::~FsWatcher()
+{
+}
+
+void
+FsWatcher::setWorkDirPath(QString path)
+{
+    QMutexLocker locker(&m_mutex);
+    if (m_workDirPath == path) return;
+    m_watcher.removePaths(m_watcher.directories());
+    m_watcher.removePaths(m_watcher.files());
+    m_workDirPath = path;
+    addWorkDirectory(path);
+    debugPrint();
+}
+
+void
+FsWatcher::setTrackedFilePaths(QStringList paths)
+{
+    QMutexLocker locker(&m_mutex);
+
+    QSet<QString> alreadyWatched = 
+	QSet<QString>::fromList(m_watcher.files());
+
+    foreach (QString path, paths) {
+        path = m_workDirPath + QDir::separator() + path;
+        if (!alreadyWatched.contains(path)) {
+            m_watcher.addPath(path);
+        } else {
+            alreadyWatched.remove(path);
+        }
+    }
+
+    // Remove the remaining paths, those that were being watched
+    // before but that are not in the list we were given
+    foreach (QString path, alreadyWatched) {
+        m_watcher.removePath(path);
+    }
+
+    debugPrint();
+}
+
+void
+FsWatcher::addWorkDirectory(QString path)
+{
+    // QFileSystemWatcher will refuse to add a file or directory to
+    // its watch list that it is already watching -- fine -- but it
+    // prints a warning when this happens, which we wouldn't want.  So
+    // we'll check for duplicates ourselves.
+    QSet<QString> alreadyWatched = 
+	QSet<QString>::fromList(m_watcher.directories());
+    
+    std::deque<QString> pending;
+    pending.push_back(path);
+
+    while (!pending.empty()) {
+
+        QString path = pending.front();
+        pending.pop_front();
+        if (!alreadyWatched.contains(path)) {
+            m_watcher.addPath(path);
+            m_dirContents[path] = scanDirectory(path);
+        }
+
+        QDir d(path);
+        if (d.exists()) {
+            d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
+                        QDir::Readable | QDir::NoSymLinks);
+            foreach (QString entry, d.entryList()) {
+                if (entry.startsWith('.')) continue;
+                QString entryPath = d.absoluteFilePath(entry);
+                pending.push_back(entryPath);
+            }
+        }
+    }
+}
+
+void
+FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
+{
+    QMutexLocker locker(&m_mutex);
+    m_ignoredPrefixes = prefixes;
+}
+
+void
+FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
+{
+    QMutexLocker locker(&m_mutex);
+    m_ignoredSuffixes = suffixes;
+}
+
+int
+FsWatcher::getNewToken()
+{
+    QMutexLocker locker(&m_mutex);
+    int token = ++m_lastToken;
+    m_tokenMap[token] = m_lastCounter;
+    return token;
+}
+
+QSet<QString>
+FsWatcher::getChangedPaths(int token)
+{
+    QMutexLocker locker(&m_mutex);
+    size_t lastUpdatedAt = m_tokenMap[token];
+    QSet<QString> changed;
+    for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
+	 i != m_changes.end(); ++i) {
+	if (i.value() > lastUpdatedAt) {
+	    changed.insert(i.key());
+	}
+    }
+    m_tokenMap[token] = m_lastCounter;
+    return changed;
+}
+
+void
+FsWatcher::fsDirectoryChanged(QString path)
+{
+    {
+	QMutexLocker locker(&m_mutex);
+
+	if (shouldIgnore(path)) return;
+
+        QSet<QString> files = scanDirectory(path);
+        if (files == m_dirContents[path]) {
+#ifdef DEBUG_FSWATCHER
+            std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring" << std::endl;
+#endif
+            return;
+        } else {
+#ifdef DEBUG_FSWATCHER
+            std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
+#endif
+            m_dirContents[path] = files;
+        }
+
+        size_t counter = ++m_lastCounter;
+        m_changes[path] = counter;
+    }
+
+    emit changed();
+}
+
+void
+FsWatcher::fsFileChanged(QString path)
+{
+    {
+        QMutexLocker locker(&m_mutex);
+
+        // We don't check whether the file matches an ignore pattern,
+        // because we are only notified for file changes if we are
+        // watching the file explicitly, i.e. the file is in the
+        // tracked file paths list. So we never want to ignore them
+
+        std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
+
+        size_t counter = ++m_lastCounter;
+        m_changes[path] = counter;
+    }
+
+    emit changed();
+}
+
+bool
+FsWatcher::shouldIgnore(QString path)
+{
+    QFileInfo fi(path);
+    QString fn(fi.fileName());
+    foreach (QString pfx, m_ignoredPrefixes) {
+        if (fn.startsWith(pfx)) {
+            std::cerr << "(ignoring: " << path << ")" << std::endl;
+            return true;
+        }
+    }
+    foreach (QString sfx, m_ignoredSuffixes) {
+        if (fn.endsWith(sfx)) {
+            std::cerr << "(ignoring: " << path << ")" << std::endl;
+            return true;
+        }
+    }
+    return false;
+}
+
+QSet<QString>
+FsWatcher::scanDirectory(QString path)
+{
+    QSet<QString> files;
+    QDir d(path);
+    if (d.exists()) {
+        d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
+                    QDir::Readable | QDir::NoSymLinks);
+        foreach (QString entry, d.entryList()) {
+            if (entry.startsWith('.')) continue;
+            if (shouldIgnore(entry)) continue;
+            files.insert(entry);
+        }
+    }
+//    std::cerr << "scanDirectory:" << std::endl;
+//    foreach (QString f, files) std::cerr << f << std::endl;
+    return files;
+}
+
+void
+FsWatcher::debugPrint()
+{
+#ifdef DEBUG_FSWATCHER
+    std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
+              << " directories and " << m_watcher.files().size()
+              << " files" << std::endl;
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/fswatcher.h	Tue Feb 14 17:55:39 2012 +0000
@@ -0,0 +1,148 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    EasyMercurial
+
+    Based on HgExplorer by Jari Korhonen
+    Copyright (c) 2010 Jari Korhonen
+    Copyright (c) 2011 Chris Cannam
+    Copyright (c) 2011 Queen Mary, University of London
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef FSWATCHER_H
+#define FSWATCHER_H
+
+#include <QObject>
+#include <QMutex>
+#include <QString>
+#include <QSet>
+#include <QHash>
+#include <QMap>
+#include <QStringList>
+#include <QFileSystemWatcher>
+
+class FsWatcher : public QObject
+{
+    Q_OBJECT
+
+public:
+    FsWatcher();
+    virtual ~FsWatcher();
+
+    /**
+     * Set the root path of the work directory to be monitored. This
+     * directory and all its subdirectories (recursively) will be
+     * monitored for changes.
+     *
+     * If this path differs from the currently set work dir path, then
+     * the tracked file paths will also be cleared. Call
+     * setTrackedFilePaths afterwards to ensure non-directory files
+     * are monitored.
+     */
+    void setWorkDirPath(QString path);
+
+    /**
+     * Provide a set of paths for files which should be tracked. These
+     * will be the only non-directory files monitored for changes. The
+     * paths should be relative to the work directory.
+     */
+    void setTrackedFilePaths(QStringList paths);
+    
+    /**
+     * Provide a set of prefixes to ignore. Files whose names start
+     * with a prefix in this set will be ignored when they change.
+     */
+    void setIgnoredFilePrefixes(QStringList prefixes);
+
+    /**
+     * Provide a set of suffixes to ignore. Files whose names end
+     * with a suffix in this set will be ignored when they change.
+     */
+    void setIgnoredFileSuffixes(QStringList suffixes);
+
+    /**
+     * Return a token to identify the current caller in subsequent
+     * calls to getChangedPaths().  Only changes that occur after this
+     * has been called can be detected by the caller.
+     */
+    int getNewToken();
+    
+    /**
+     * Return a list of all non-ignored file paths that have been
+     * observed to have changed since the last call to getChangedPaths
+     * with the same token.
+     */
+    QSet<QString> getChangedPaths(int token);
+    
+signals:
+    /**
+     * Emitted when something has changed. Use the asynchronous
+     * interface to find out what.
+     */
+    void changed();
+
+private slots:
+    void fsDirectoryChanged(QString);
+    void fsFileChanged(QString);
+
+private:
+    // call with lock already held
+    void addWorkDirectory(QString path);
+
+    // call with lock already held
+    bool shouldIgnore(QString path);
+
+    // call with lock already held. Returns set of non-ignored files in dir
+    QSet<QString> scanDirectory(QString path);
+
+    // call with lock already held
+    void debugPrint();
+
+private:
+    /**
+     * A change associates a filename with a counter (the
+     * size_t). Each time a file is changed, its counter is assigned
+     * from m_lastCounter and m_lastCounter is incremented.
+     *
+     * This is not especially efficient -- we often want to find "all
+     * files whose counter is greater than X" which involves a
+     * traversal. Maybe something better later.
+     */
+    QHash<QString, size_t> m_changes;
+
+    /**
+     * Associates a token (the client identifier) with a counter. The
+     * counter represents the value of m_lastCounter at the moment
+     * getChangedPaths last completed for that token. Any files in
+     * m_changes whose counters are larger must have been modified.
+     */
+    QMap<int, size_t> m_tokenMap;
+
+    /**
+     * Associates a directory path with a list of all the files in it
+     * that do not match our ignore patterns. When a directory is
+     * signalled as having changed, then we need to rescan it and
+     * compare against this list before we can determine whether to
+     * notify about the change or not.
+     */
+    QHash<QString, QSet<QString> > m_dirContents;
+
+    QStringList m_ignoredPrefixes;
+    QStringList m_ignoredSuffixes;
+
+    /// Everything in this class is synchronised.
+    QMutex m_mutex;
+
+    QString m_workDirPath;
+    int m_lastToken;
+    size_t m_lastCounter;
+    QFileSystemWatcher m_watcher;
+};
+
+#endif
--- a/src/mainwindow.cpp	Fri Feb 10 13:08:07 2012 +0000
+++ b/src/mainwindow.cpp	Tue Feb 14 17:55:39 2012 +0000
@@ -52,14 +52,12 @@
 #include "workstatuswidget.h"
 #include "hgignoredialog.h"
 #include "versiontester.h"
+#include "fswatcher.h"
 
 
 MainWindow::MainWindow(QString myDirPath) :
     m_myDirPath(myDirPath),
-    m_helpDialog(0),
-    m_fsWatcherGeneralTimer(0),
-    m_fsWatcherRestoreTimer(0),
-    m_fsWatcherSuspended(false)
+    m_helpDialog(0)
 {
     setWindowIcon(QIcon(":images/easyhg-icon.png"));
 
@@ -67,7 +65,11 @@
 
     m_showAllFiles = false;
 
-    m_fsWatcher = 0;
+    m_fsWatcher = new FsWatcher();
+    m_fsWatcherToken = m_fsWatcher->getNewToken();
+    m_commandSequenceInProgress = false;
+    connect(m_fsWatcher, SIGNAL(changed()), this, SLOT(checkFilesystem()));
+
     m_commitsSincePush = 0;
     m_shouldHgStat = true;
 
@@ -251,14 +253,16 @@
 {
     QStringList params;
 
-    if (m_showAllFiles) {
-        params << "stat" << "-A";
-    } else {
-        params << "stat" << "-ardum";
-    }
+    // We always stat all files, regardless of whether we're showing
+    // them all, because we need them for the filesystem monitor
+    params << "stat" << "-A";
 
     m_lastStatOutput = "";
 
+    // We're about to do a stat, so we can silently bring ourselves
+    // up-to-date on any file changes to this point
+    (void)m_fsWatcher->getChangedPaths(m_fsWatcherToken);
+
     m_runner->requestAction(HgAction(ACT_STAT, m_workFolderPath, params));
 }
 
@@ -1301,14 +1305,6 @@
     m_mergeCommitComment = "";
     m_stateUnknown = true;
     m_needNewLog = true;
-    if (m_fsWatcher) {
-        delete m_fsWatcherGeneralTimer;
-        m_fsWatcherGeneralTimer = 0;
-        delete m_fsWatcherRestoreTimer;
-        m_fsWatcherRestoreTimer = 0;
-        delete m_fsWatcher;
-        m_fsWatcher = 0;
-    }
 }
 
 void MainWindow::hgServe()
@@ -1821,126 +1817,22 @@
     }
 }
 
-void MainWindow::updateFileSystemWatcher()
+void MainWindow::updateFsWatcher()
 {
-    bool justCreated = false;
-    if (!m_fsWatcher) {
-        m_fsWatcher = new QFileSystemWatcher();
-        justCreated = true;
-    }
-
-    // QFileSystemWatcher will refuse to add a file or directory to
-    // its watch list that it is already watching -- fine, that's what
-    // we want -- but it prints a warning when this happens, which is
-    // annoying because it would be the normal case for us.  So we'll
-    // check for duplicates ourselves.
-    QSet<QString> alreadyWatched;
-    QStringList dl(m_fsWatcher->directories());
-    foreach (QString d, dl) alreadyWatched.insert(d);
-    
-    std::deque<QString> pending;
-    pending.push_back(m_workFolderPath);
-
-    while (!pending.empty()) {
-
-        QString path = pending.front();
-        pending.pop_front();
-        if (!alreadyWatched.contains(path)) {
-            m_fsWatcher->addPath(path);
-            DEBUG << "Added to file system watcher: " << path << endl;
-        }
-
-        QDir d(path);
-        if (d.exists()) {
-            d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
-                        QDir::Readable | QDir::NoSymLinks);
-            foreach (QString entry, d.entryList()) {
-                if (entry.startsWith('.')) continue;
-                QString entryPath = d.absoluteFilePath(entry);
-                pending.push_back(entryPath);
-            }
-        }
-    }
-
-    // The general timer isn't really related to the fs watcher
-    // object, it just does something similar -- every now and then we
-    // do a refresh just to update the history dates etc
-
-    m_fsWatcherGeneralTimer = new QTimer(this);
-    connect(m_fsWatcherGeneralTimer, SIGNAL(timeout()),
-            this, SLOT(checkFilesystem()));
-    m_fsWatcherGeneralTimer->setInterval(30 * 60 * 1000); // half an hour
-    m_fsWatcherGeneralTimer->start();
-
-    if (justCreated) {
-        connect(m_fsWatcher, SIGNAL(directoryChanged(QString)),
-                this, SLOT(fsDirectoryChanged(QString)));
-        connect(m_fsWatcher, SIGNAL(fileChanged(QString)),
-                this, SLOT(fsFileChanged(QString)));
-    }
-}
-
-void MainWindow::suspendFileSystemWatcher()
-{
-    DEBUG << "MainWindow::suspendFileSystemWatcher" << endl;
-    if (m_fsWatcher) {
-        m_fsWatcherSuspended = true;
-        if (m_fsWatcherRestoreTimer) {
-            delete m_fsWatcherRestoreTimer;
-            m_fsWatcherRestoreTimer = 0;
-        }
-        m_fsWatcherGeneralTimer->stop();
-    }
-}
-
-void MainWindow::restoreFileSystemWatcher()
-{
-    DEBUG << "MainWindow::restoreFileSystemWatcher" << endl;
-    if (m_fsWatcherRestoreTimer) delete m_fsWatcherRestoreTimer;
-        
-    // The restore timer is used to leave a polite interval between
-    // being asked to restore the watcher and actually doing so.  It's
-    // a single shot timer each time it's used, but we don't use
-    // QTimer::singleShot because we want to stop the previous one if
-    // it's running (via deleting it)
-
-    m_fsWatcherRestoreTimer = new QTimer(this);
-    connect(m_fsWatcherRestoreTimer, SIGNAL(timeout()),
-            this, SLOT(actuallyRestoreFileSystemWatcher()));
-    m_fsWatcherRestoreTimer->setInterval(1000);
-    m_fsWatcherRestoreTimer->setSingleShot(true);
-    m_fsWatcherRestoreTimer->start();
-}
-
-void MainWindow::actuallyRestoreFileSystemWatcher()
-{
-    DEBUG << "MainWindow::actuallyRestoreFileSystemWatcher" << endl;
-    if (m_fsWatcher) {
-        m_fsWatcherSuspended = false;
-        m_fsWatcherGeneralTimer->start();
-    }
+    m_fsWatcher->setWorkDirPath(m_workFolderPath);
+    m_fsWatcher->setTrackedFilePaths(m_hgTabs->getFileStates().trackedFiles());
 }
 
 void MainWindow::checkFilesystem()
 {
     DEBUG << "MainWindow::checkFilesystem" << endl;
-    hgRefresh();
-}
-
-void MainWindow::fsDirectoryChanged(QString d)
-{
-    DEBUG << "MainWindow::fsDirectoryChanged " << d << endl;
-    if (!m_fsWatcherSuspended) {
-        hgStat();
+    if (!m_commandSequenceInProgress) {
+        if (!m_fsWatcher->getChangedPaths(m_fsWatcherToken).empty()) {
+            hgRefresh();
+            return;
+        }
     }
-}
-
-void MainWindow::fsFileChanged(QString f)
-{
-    DEBUG << "MainWindow::fsFileChanged " << f << endl;
-    if (!m_fsWatcherSuspended) {
-        hgStat();
-    }
+    updateFsWatcher();
 }
 
 QString MainWindow::format1(QString head)
@@ -2088,21 +1980,14 @@
 
 void MainWindow::commandStarting(HgAction action)
 {
-    // Annoyingly, hg stat actually modifies the working directory --
-    // it creates files called hg-checklink and hg-checkexec to test
-    // properties of the filesystem.  For safety's sake, suspend the
-    // fs watcher while running commands, and restore it shortly after
-    // a command has finished.
-
-    if (action.action == ACT_STAT) {
-        suspendFileSystemWatcher();
-    }
+    m_commandSequenceInProgress = true;
 }
 
-void MainWindow::commandFailed(HgAction action, QString stderr, QString stdout)
+void MainWindow::commandFailed(HgAction action, QString stdErr, QString stdOut)
 {
     DEBUG << "MainWindow::commandFailed" << endl;
-    restoreFileSystemWatcher();
+
+    m_commandSequenceInProgress = false;
 
     QString setstr;
 #ifdef Q_OS_MAC
@@ -2130,7 +2015,7 @@
              tr("Failed to run Mercurial"),
              tr("Failed to run Mercurial"),
              tr("The Mercurial program either could not be found or failed to run.<br><br>Check that the Mercurial program path is correct in %1.").arg(setstr),
-             stderr);
+             stdErr);
         settings(SettingsDialog::PathsTab);
         return;
     case ACT_TEST_HG_EXT:
@@ -2139,7 +2024,7 @@
              tr("Failed to run Mercurial"),
              tr("Failed to run Mercurial with extension enabled"),
              tr("The Mercurial program failed to run with the EasyMercurial interaction extension enabled.<br>This may indicate an installation problem.<br><br>You may be able to continue working if you switch off &ldquo;Use EasyHg Mercurial Extension&rdquo; in %1.  Note that remote repositories that require authentication might not work if you do this.").arg(setstr),
-             stderr);
+             stdErr);
         settings(SettingsDialog::PathsTab);
         return;
     case ACT_CLONEFROMREMOTE:
@@ -2148,20 +2033,20 @@
         enableDisableActions();
         break; // go on to default report
     case ACT_INCOMING:
-        if (stderr.contains("authorization failed")) {
-            reportAuthFailed(stderr);
+        if (stdErr.contains("authorization failed")) {
+            reportAuthFailed(stdErr);
             return;
-        } else if (stderr.contains("entry cancelled")) {
+        } else if (stdErr.contains("entry cancelled")) {
             // ignore this, user cancelled username or password dialog
             return;
         } else {
-            // Incoming returns non-zero code and no stderr if the
+            // Incoming returns non-zero code and no stdErr if the
             // check was successful but there are no changes
             // pending. This is the only case where we need to remove
             // warning messages, because it's the only case where a
             // non-zero code can be returned even though the command
             // has for our purposes succeeded
-            QString replaced = stderr;
+            QString replaced = stdErr;
             while (1) {
                 QString r1 = replaced;
                 r1.replace(QRegExp("warning: [^\\n]*"), "");
@@ -2175,31 +2060,33 @@
         }
         break; // go on to default report
     case ACT_PULL:
-        if (stderr.contains("authorization failed")) {
-            reportAuthFailed(stderr);
+        if (stdErr.contains("authorization failed")) {
+            reportAuthFailed(stdErr);
             return;
-        } else if (stderr.contains("entry cancelled")) {
+        } else if (stdErr.contains("entry cancelled")) {
             // ignore this, user cancelled username or password dialog
             return;
-        } else if (stderr.contains("no changes found") || stdout.contains("no changes found")) {
+        } else if (stdErr.contains("no changes found") || stdOut.contains("no changes found")) {
             // success: hg 2.1 starts returning failure code for empty pull/push
-            commandCompleted(action, stdout);
+            m_commandSequenceInProgress = true; // there may be further commands
+            commandCompleted(action, stdOut);
             return;
         }
         break; // go on to default report
     case ACT_PUSH:
-        if (stderr.contains("creates new remote head")) {
-            reportNewRemoteHeads(stderr);
+        if (stdErr.contains("creates new remote head")) {
+            reportNewRemoteHeads(stdErr);
             return;
-        } else if (stderr.contains("authorization failed")) {
-            reportAuthFailed(stderr);
+        } else if (stdErr.contains("authorization failed")) {
+            reportAuthFailed(stdErr);
             return;
-        } else if (stderr.contains("entry cancelled")) {
+        } else if (stdErr.contains("entry cancelled")) {
             // ignore this, user cancelled username or password dialog
             return;
-        } else if (stderr.contains("no changes found") || stdout.contains("no changes found")) {
+        } else if (stdErr.contains("no changes found") || stdOut.contains("no changes found")) {
             // success: hg 2.1 starts returning failure code for empty pull/push
-            commandCompleted(action, stdout);
+            m_commandSequenceInProgress = true; // there may be further commands
+            commandCompleted(action, stdOut);
             return;
         }
         break; // go on to default report
@@ -2209,22 +2096,23 @@
         // problem, something else will fail too).  Pretend it
         // succeeded, so that any further actions that are contingent
         // on the success of the heads query get carried out properly.
+        m_commandSequenceInProgress = true; // there may be further commands
         commandCompleted(action, "");
         return;
     case ACT_FOLDERDIFF:
     case ACT_CHGSETDIFF:
-        // external program, unlikely to be anything useful in stderr
+        // external program, unlikely to be anything useful in stdErr
         // and some return with failure codes when something as basic
         // as the user closing the window via the wm happens
         return;
     case ACT_MERGE:
-        if (stderr.contains("working directory ancestor")) {
+        if (stdErr.contains("working directory ancestor")) {
             // arguably we should prevent this upfront, but that's
             // trickier!
             MoreInformationDialog::information
                 (this, tr("Merge"), tr("Merge has no effect"),
                  tr("You asked to merge a revision with one of its ancestors.<p>This has no effect, because the ancestor's changes already exist in both revisions."),
-                 stderr);
+                 stdErr);
             return;
         }
         // else fall through
@@ -2232,7 +2120,7 @@
         MoreInformationDialog::information
             (this, tr("Merge"), tr("Merge failed"),
              tr("Some files were not merged successfully.<p>You can Merge again to repeat the interactive merge; use Revert to abandon the merge entirely; or edit the files that are in conflict in an editor and, when you are happy with them, choose Mark Resolved in each file's right-button menu."),
-             stderr);
+             stdErr);
         m_mergeCommitComment = "";
         return;
     case ACT_STAT:
@@ -2251,17 +2139,16 @@
         (this,
          tr("Command failed"),
          tr("Command failed"),
-         (stderr == "" ?
+         (stdErr == "" ?
           tr("A Mercurial command failed to run correctly.  This may indicate an installation problem or some other problem with EasyMercurial.") :
           tr("A Mercurial command failed to run correctly.  This may indicate an installation problem or some other problem with EasyMercurial.<br><br>See &ldquo;More Details&rdquo; for the command output.")),
-         stderr);
+         stdErr);
 }
 
 void MainWindow::commandCompleted(HgAction completedAction, QString output)
 {
 //    std::cerr << "commandCompleted: " << completedAction.action << std::endl;
 
-    restoreFileSystemWatcher();
     HGACTIONS action = completedAction.action;
 
     if (action == ACT_NONE) return;
@@ -2320,7 +2207,6 @@
 
     case ACT_STAT:
         m_lastStatOutput = output;
-        updateFileSystemWatcher();
         break;
 
     case ACT_RESOLVE_LIST:
@@ -2624,10 +2510,12 @@
     }
 
     if (noMore) {
+        m_commandSequenceInProgress = false;
         m_stateUnknown = false;
         enableDisableActions();
         m_hgTabs->updateHistory();
         updateRecentMenu();
+        checkFilesystem();
     }
 }
 
@@ -2996,7 +2884,7 @@
     m_exitAct->setStatusTip(tr("Exit EasyMercurial"));
 
     //Repository actions
-    m_hgRefreshAct = new QAction(QIcon(":/images/status.png"), tr("&Refresh"), this);
+    m_hgRefreshAct = new QAction(QIcon(":/images/status.png"), tr("&Re-Read Working Folder"), this);
     m_hgRefreshAct->setShortcut(tr("Ctrl+R"));
     m_hgRefreshAct->setStatusTip(tr("Refresh the window to show the current state of the working folder"));
 
@@ -3118,22 +3006,19 @@
 {
     int sz = 32;
 
-    m_fileToolBar = addToolBar(tr("File"));
-    m_fileToolBar->setIconSize(QSize(sz, sz));
-    m_fileToolBar->addAction(m_openAct);
-    m_fileToolBar->addAction(m_hgRefreshAct);
-    m_fileToolBar->setMovable(false);
-
-    m_repoToolBar = addToolBar(tr("Remote"));
-    m_repoToolBar->setIconSize(QSize(sz, sz));
-    m_repoToolBar->addAction(m_hgIncomingAct);
-    m_repoToolBar->addAction(m_hgPullAct);
-    m_repoToolBar->addAction(m_hgPushAct);
-    m_repoToolBar->setMovable(false);
+    bool spacingReqd = false;
+#ifndef Q_OS_MAC
+    spacingReqd = true;
+#endif
 
     m_workFolderToolBar = addToolBar(tr("Work"));
     addToolBar(Qt::LeftToolBarArea, m_workFolderToolBar);
     m_workFolderToolBar->setIconSize(QSize(sz, sz));
+    if (spacingReqd) {
+        QWidget *w = new QWidget;
+        w->setFixedHeight(6);
+        m_workFolderToolBar->addWidget(w);
+    }
     m_workFolderToolBar->addAction(m_hgFolderDiffAct);
     m_workFolderToolBar->addSeparator();
     m_workFolderToolBar->addAction(m_hgRevertAct);
@@ -3145,6 +3030,17 @@
     m_workFolderToolBar->addAction(m_hgRemoveAct);
     m_workFolderToolBar->setMovable(false);
 
+    m_repoToolBar = addToolBar(tr("Remote"));
+    m_repoToolBar->setIconSize(QSize(sz, sz));
+    if (spacingReqd) m_repoToolBar->addWidget(new QLabel(" "));
+    m_repoToolBar->addAction(m_openAct);
+    if (spacingReqd) m_repoToolBar->addWidget(new QLabel(" "));
+    m_repoToolBar->addSeparator();
+    m_repoToolBar->addAction(m_hgIncomingAct);
+    m_repoToolBar->addAction(m_hgPullAct);
+    m_repoToolBar->addAction(m_hgPushAct);
+    m_repoToolBar->setMovable(false);
+
     updateToolBarStyle();
 }
 
--- a/src/mainwindow.h	Fri Feb 10 13:08:07 2012 +0000
+++ b/src/mainwindow.h	Tue Feb 14 17:55:39 2012 +0000
@@ -27,7 +27,6 @@
 
 #include <QMainWindow>
 #include <QListWidget>
-#include <QFileSystemWatcher>
 
 QT_BEGIN_NAMESPACE
 class QAction;
@@ -36,6 +35,7 @@
 QT_END_NAMESPACE
 
 class WorkStatusWidget;
+class FsWatcher;
 
 class MainWindow : public QMainWindow
 {
@@ -113,10 +113,8 @@
     void hgIgnoreFiles(QStringList);
     void hgUnIgnoreFiles(QStringList);
 
-    void fsDirectoryChanged(QString);
-    void fsFileChanged(QString);
+    void updateFsWatcher();
     void checkFilesystem();
-    void actuallyRestoreFileSystemWatcher();
 
     void newerVersionAvailable(QString);
 
@@ -176,10 +174,6 @@
 
     void clearState();
 
-    void updateFileSystemWatcher();
-    void suspendFileSystemWatcher();
-    void restoreFileSystemWatcher();
-
     void updateClosedHeads();
 
     void updateWorkFolderAndRepoNames();
@@ -243,7 +237,6 @@
     QAction *m_aboutAct;
     QAction *m_helpAct;
 
-    QToolBar *m_fileToolBar;
     QToolBar *m_repoToolBar;
     QToolBar *m_workFolderToolBar;
 
@@ -257,10 +250,9 @@
     QString getMergeBinaryName();
     QString getEditorBinaryName();
 
-    QFileSystemWatcher *m_fsWatcher;
-    QTimer *m_fsWatcherGeneralTimer;
-    QTimer *m_fsWatcherRestoreTimer;
-    bool m_fsWatcherSuspended;
+    FsWatcher *m_fsWatcher;
+    int m_fsWatcherToken;
+    bool m_commandSequenceInProgress;
 
     QString m_lastStatOutput;
     QStringList m_lastRevertedFiles;