/guitone-1.0rc5/src/model/InventoryWatcher.cpp
C++ | 386 lines | 285 code | 50 blank | 51 comment | 76 complexity | 49dd9a1f7436dd05fb6c3220004b41a2 MD5 | raw file
Possible License(s): GPL-3.0
1/***************************************************************************
2 * Copyright (C) 2008 by Thomas Keller *
3 * me@thomaskeller.biz *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation, either version 3 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
17 ***************************************************************************/
18
19#include "InventoryWatcher.h"
20#include "InventoryItem.h"
21#include "BasicIOParser.h"
22#include "GuitoneCore.h"
23
24InventoryWatcher::InventoryWatcher(QObject * parent)
25 : QFileSystemWatcher(parent),
26 // do not watch more than 200 paths at once
27 lruCache(200)
28{
29 connect(
30 this, SIGNAL(directoryChanged(const QString &)),
31 this, SLOT(pathChanged(const QString &))
32 );
33
34 connect(
35 this, SIGNAL(fileChanged(const QString &)),
36 this, SLOT(pathChanged(const QString &))
37 );
38
39 notifyTimer = new QTimer(this);
40
41 connect(
42 notifyTimer, SIGNAL(timeout()),
43 this, SLOT(notifyChangedPaths())
44 );
45}
46
47InventoryWatcher::~InventoryWatcher()
48{
49 delete notifyTimer;
50}
51
52//! TODO: monitor _MTN/options for changes as well
53void InventoryWatcher::setMonotoneHandle(const MonotoneHandlePtr & handle)
54{
55 monotoneHandle = handle;
56
57 QString workspace = handle->getData();
58 QString revPath(workspace + "/_MTN/revision");
59 QFile file(revPath);
60 if (!file.open(QIODevice::ReadOnly))
61 {
62 C(QString("cannot open %1/_MTN/revision for reading").arg(workspace));
63 }
64 QString contents(file.readAll());
65 file.close();
66
67 BasicIOParser parser(contents);
68 if (!parser.parse())
69 {
70 C("could not parse contents of _MTN/revision");
71 }
72 oldRevisionEntries = parser.getStanzas();
73 addPath(revPath);
74}
75
76void InventoryWatcher::watchPaths(const QStringList & paths)
77{
78 QStringList watchedPaths = files() + directories();
79 QString workspace = monotoneHandle->getData();
80 foreach (const QString & path, paths)
81 {
82 QString fullPath(workspace);
83 if (path != "/") fullPath += "/" + path;
84 if (watchedPaths.indexOf(fullPath) != -1) continue;
85 D(QString("adding %1").arg(fullPath));
86 addPath(fullPath);
87
88 // directories end with a slash and should keep a little longer
89 // around, thats why we give them one additional heat point
90 // more initially
91 QString removedPath = lruCache.add(
92 path, path.right(1) == "/" ? 2 : 1, 1
93 );
94
95 if (!removedPath.isEmpty())
96 {
97 QString fullRemovedPath(workspace);
98 if (removedPath != "/") fullRemovedPath += "/" + removedPath;
99 D(QString("removing %1 because of LRU").arg(fullRemovedPath));
100 removePath(fullRemovedPath);
101 }
102 }
103}
104
105void InventoryWatcher::unwatchPaths(const QStringList & paths)
106{
107 QStringList watchedPaths = files() + directories();
108
109 foreach (const QString & path, paths)
110 {
111 QString fullPath(monotoneHandle->getData());
112 if (path != "/") fullPath += "/" + path;
113 if (watchedPaths.indexOf(fullPath) == -1) continue;
114 removePath(fullPath);
115 lruCache.remove(path);
116 }
117}
118
119void InventoryWatcher::pathChanged(const QString & path)
120{
121 QString workspace = monotoneHandle->getData();
122 I(path.indexOf(workspace) != -1);
123
124 if (path == QString(workspace + "/_MTN/revision"))
125 {
126 checkForBookkeepChanges();
127 // we need to re-add this path since
128 // mtn apparently re-creates the file
129 addPath(workspace + "/_MTN/revision");
130 return;
131 }
132
133 QString nodePath =
134 path.mid(path.indexOf(workspace) + workspace.length() + 1);
135
136 if (nodePath == ".mtn-ignore")
137 {
138 // automate stdio up until 0.46 needs to be restarted in order to
139 // pick up changes in .mtn-ignore
140 L(QString(".mtn-ignore changed, stopping all threads for %1")
141 .arg(workspace));
142 APP->manager()->stopThreads(monotoneHandle);
143
144 // if something inside .mtn-ignore has changed, the complete workspace
145 // could be affected, so do a full reload
146 markPathForNotification("");
147 return;
148 }
149
150 markPathForNotification(nodePath);
151}
152
153void InventoryWatcher::markPathForNotification(const QString & newPath)
154{
155 //
156 // The following algorithm only picks those paths for an update
157 // which are not a sub path of an already existing path
158 //
159 // current: "a/b" "b/c"
160 // add "g": "a/b" "b/c" "g"
161 // add "b": "a/b" "b"
162 // add "a/b/c": "a/b" "b/c"
163 // add "": ""
164 //
165 QStringList actualNotifiedPaths;
166 if (newPath.isEmpty())
167 {
168 actualNotifiedPaths.append("");
169 }
170 else
171 {
172 enum { skip, replace, add } action = add;
173
174 foreach (QString oldPath, changedPaths)
175 {
176 if (newPath.startsWith(oldPath) ||
177 newPath == oldPath || oldPath == "")
178 {
179 action = skip;
180 break;
181 }
182
183 if (oldPath.startsWith(newPath))
184 {
185 action = replace;
186 }
187 }
188
189 if (action == add)
190 {
191 actualNotifiedPaths = changedPaths;
192 actualNotifiedPaths.append(newPath);
193 }
194 else
195 if (action == replace)
196 {
197 foreach (QString oldPath, changedPaths)
198 {
199 if (!oldPath.startsWith(newPath))
200 {
201 actualNotifiedPaths.append(oldPath);
202 }
203 }
204 actualNotifiedPaths.append(newPath);
205 }
206 else
207 if (action == skip)
208 {
209 actualNotifiedPaths = changedPaths;
210 }
211 else
212 I(false);
213 }
214
215 changedPaths = actualNotifiedPaths;
216
217 if (notifyTimer->isActive())
218 {
219 notifyTimer->stop();
220 }
221
222 // FIXME: make this configurable eventually
223 notifyTimer->start(500);
224}
225
226void InventoryWatcher::notifyChangedPaths()
227{
228 notifyTimer->stop();
229 D(QString("notifying changed paths %1").arg(changedPaths.join(",")));
230 foreach (QString path, changedPaths)
231 {
232 emit changedPath(path);
233 }
234 changedPaths.clear();
235}
236
237void InventoryWatcher::checkForBookkeepChanges()
238{
239 QString workspace = monotoneHandle->getData();
240 QFile file(workspace + "/_MTN/revision");
241 if (!file.open(QIODevice::ReadOnly))
242 {
243 C(QString("cannot open %1/_MTN/revision for reading").arg(workspace));
244 return;
245 }
246 QString contents(file.readAll());
247 file.close();
248
249 BasicIOParser parser(contents);
250 if (!parser.parse())
251 {
252 C("could not parse contents of _MTN/revision");
253 return;
254 }
255
256 StanzaList newRevisionEntries = parser.getStanzas();
257
258 foreach (const Stanza & newStanza, newRevisionEntries)
259 {
260 QString newEntryType = newStanza.at(0).sym;
261 // we don't care about patches, since we're notified of them
262 // separately; and the other two are uninteresting for us as well
263 if (newEntryType == "format_version" ||
264 newEntryType == "new_manifest" ||
265 newEntryType == "patch")
266 {
267 continue;
268 }
269
270 bool foundOrAlreadyHandled = false;
271 enum { unknown, file, dir } type = unknown;
272
273 // Unfortunately monotone's revision format doesn't record the node type
274 // so we can only derive type info from add_dir or add_file, but
275 // unfortunately not for deleted nodes or attribute changes.
276 // In this case this code only works as long as the file is still
277 // still present in the file system so we can manually query its type.
278
279 type = newEntryType == "add_dir" ? dir :
280 newEntryType == "add_file" ? file : unknown;
281
282 for (int i=0; i<oldRevisionEntries.size(); )
283 {
284 Stanza oldStanza = oldRevisionEntries.at(i);
285 QString oldEntryType = oldStanza.at(0).sym;
286 if (oldEntryType == "format_version" ||
287 oldEntryType == "new_manifest" ||
288 oldEntryType == "patch")
289 {
290 oldRevisionEntries.removeAt(i);
291 foundOrAlreadyHandled = true;
292 continue;
293 }
294
295 if (newEntryType != oldEntryType)
296 {
297 i++;
298 continue;
299 }
300
301 if (newEntryType == "old_revision")
302 {
303 if (oldStanza.at(0).vals.at(0) != newStanza.at(0).vals.at(0))
304 {
305 emit changedBaseRevision(newStanza.at(0).vals.at(0));
306 }
307 oldRevisionEntries.removeAt(i);
308 foundOrAlreadyHandled = true;
309 break;
310 }
311
312 // one stanza entries to compare
313 if (newEntryType == "delete" ||
314 newEntryType == "add_file" ||
315 newEntryType == "add_dir")
316 {
317 if (oldStanza.at(0).vals.at(0) == newStanza.at(0).vals.at(0))
318 {
319 oldRevisionEntries.removeAt(i);
320 foundOrAlreadyHandled = true;
321 break;
322 }
323 }
324
325 // two stanza entries to compare
326 if (newEntryType == "rename" || newEntryType == "clear")
327 {
328 if (oldStanza.at(0).vals.at(0) == newStanza.at(0).vals.at(0) &&
329 oldStanza.at(1).vals.at(0) == newStanza.at(1).vals.at(0))
330 {
331 oldRevisionEntries.removeAt(i);
332 foundOrAlreadyHandled = true;
333 break;
334 }
335 }
336
337 // three stanza entries to compare
338 if (newEntryType == "set")
339 {
340 if (oldStanza.at(0).vals.at(0) == newStanza.at(0).vals.at(0) &&
341 oldStanza.at(1).vals.at(0) == newStanza.at(1).vals.at(0) &&
342 oldStanza.at(2).vals.at(0) == newStanza.at(2).vals.at(0))
343 {
344 oldRevisionEntries.removeAt(i);
345 foundOrAlreadyHandled = true;
346 break;
347 }
348 }
349
350 i++;
351 }
352
353 if (foundOrAlreadyHandled) continue;
354
355 QString path = newStanza.at(0).vals.at(0);
356 if (type == unknown)
357 {
358 QFileInfo info(path);
359 type = info.isDir() ? dir : unknown;
360 }
361
362 if (type == dir)
363 path += "/";
364
365 markPathForNotification(path);
366 }
367
368 // now loop through all the left old items;
369 // we should only have a subset of items in this list now
370 foreach (const Stanza & oldStanza, oldRevisionEntries)
371 {
372 markPathForNotification(oldStanza.at(0).vals.at(0));
373 }
374
375 oldRevisionEntries = newRevisionEntries;
376}
377
378void InventoryWatcher::clearAllWatches()
379{
380 notifyTimer->stop();
381 changedPaths.clear();
382 removePaths(directories());
383 removePaths(files());
384 lruCache.clear();
385}
386