changeset 610:5c68567b3645

Refresh tags, branches, bookmarks and ignore when their files (or csets in the repo) are changed
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 09 May 2013 21:06:48 +0200
parents e4a71afd3c71
children 7fc7fba4df30
files src/org/tmatesoft/hg/internal/ChangelogMonitor.java src/org/tmatesoft/hg/internal/CommitFacility.java src/org/tmatesoft/hg/internal/DirstateReader.java src/org/tmatesoft/hg/internal/FileChangeMonitor.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/repo/HgBookmarks.java src/org/tmatesoft/hg/repo/HgBranches.java src/org/tmatesoft/hg/repo/HgIgnore.java src/org/tmatesoft/hg/repo/HgRepository.java src/org/tmatesoft/hg/repo/HgTags.java
diffstat 10 files changed, 361 insertions(+), 103 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ChangelogMonitor.java	Thu May 09 21:06:48 2013 +0200
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2013 TMate Software Ltd
+ *  
+ * 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; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.core.SessionContext;
+import org.tmatesoft.hg.repo.HgRepository;
+
+/**
+ * Track changes to a repository based on recent changelog revision.
+ * TODO shall be merged with {@link RevlogChangeMonitor} and {@link FileChangeMonitor} into 
+ * a single facility available from {@link SessionContext}
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class ChangelogMonitor {
+	private final HgRepository repo;
+	private int changelogRevCount = -1;
+	private Nodeid changelogLastRev = null;
+	
+	public ChangelogMonitor(HgRepository hgRepo) {
+		repo = hgRepo;
+	}
+	
+	// memorize the state of the repository's changelog
+	public void touch() {
+		changelogRevCount = repo.getChangelog().getRevisionCount();
+		changelogLastRev = safeGetRevision(changelogRevCount-1);
+	}
+	
+	// if present state doesn't match the one we remember
+	public boolean isChanged() {
+		int rc = repo.getChangelog().getRevisionCount();
+		if (rc != changelogRevCount) {
+			return true;
+		}
+		Nodeid r = safeGetRevision(rc-1);
+		return !r.equals(changelogLastRev);
+	}
+	
+	// handles empty repository case
+	private Nodeid safeGetRevision(int revIndex) {
+		if (revIndex >= 0) {
+			return repo.getChangelog().getRevision(revIndex);
+		}
+		return Nodeid.NULL;
+	}
+}
--- a/src/org/tmatesoft/hg/internal/CommitFacility.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/CommitFacility.java	Thu May 09 21:06:48 2013 +0200
@@ -192,7 +192,7 @@
 				fncache.write();
 			} catch (IOException ex) {
 				// see comment above for fnchache.read()
-				repo.getSessionContext().getLog().dump(getClass(), Severity.Error, ex, "Failed to write fncache, error ignored");
+				repo.getLog().dump(getClass(), Severity.Error, ex, "Failed to write fncache, error ignored");
 			}
 		}
 		// bring dirstate up to commit state
@@ -210,6 +210,9 @@
 		if (p1Commit != NO_REVISION || p2Commit != NO_REVISION) {
 			repo.getRepo().getBookmarks().updateActive(p1Cset, p2Cset, changesetRev);
 		}
+		// TODO Revisit: might be reasonable to send out a "Repo changed" notification, to clear
+		// e.g. cached branch, tags and so on, not to rely on file change detection methods?
+		// The same notificaion might come useful once Pull is implemented
 		return changesetRev;
 	}
 /*
--- a/src/org/tmatesoft/hg/internal/DirstateReader.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/DirstateReader.java	Thu May 09 21:06:48 2013 +0200
@@ -102,7 +102,7 @@
 				} else if (state == 'm') {
 					target.next(EntryKind.Merged, r);
 				} else {
-					repo.getSessionContext().getLog().dump(getClass(), Severity.Warn, "Dirstate record for file %s (size: %d, tstamp:%d) has unknown state '%c'", r.name(), r.size(), r.modificationTime(), state);
+					repo.getLog().dump(getClass(), Severity.Warn, "Dirstate record for file %s (size: %d, tstamp:%d) has unknown state '%c'", r.name(), r.size(), r.modificationTime(), state);
 				}
 			}
 		} catch (IOException ex) {
@@ -178,7 +178,7 @@
 				branch = b == null || b.length() == 0 ? HgRepository.DEFAULT_BRANCH_NAME : b;
 				r.close();
 			} catch (FileNotFoundException ex) {
-				internalRepo.getSessionContext().getLog().dump(HgDirstate.class, Debug, ex, null); // log verbose debug, exception might be legal here 
+				internalRepo.getLog().dump(HgDirstate.class, Debug, ex, null); // log verbose debug, exception might be legal here 
 				// IGNORE
 			} catch (IOException ex) {
 				throw new HgInvalidControlFileException("Error reading file with branch information", ex, branchFile);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/FileChangeMonitor.java	Thu May 09 21:06:48 2013 +0200
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2013 TMate Software Ltd
+ *  
+ * 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; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.io.File;
+
+/**
+ * This shall become interface/abstract class accessible from SessionContext,
+ * with plugable implementations, e.g. Java7 (file monitoring facilities) based,
+ * or any other convenient means. It shall allow both "check at the moment asked" 
+ * and "collect changes and dispatch on demand" implementation approaches, so that
+ * implementors may use best available technology   
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class FileChangeMonitor {
+	private final File file;
+	private long lastModified;
+	private long length;
+	
+	/**
+	 * First round: support for 1-monitor-1-file only
+	 * Next round: 1-monitor-N files
+	 */
+	public FileChangeMonitor(File f) {
+		file = f;
+	}
+	
+	// shall work for files that do not exist
+	public void touch(Object source) {
+		lastModified = file.lastModified();
+		length = file.length();
+	}
+	
+	public void check(Object source, Action onChange) {
+		if (changed(source)) {
+			onChange.changed();
+		}
+	}
+
+	public boolean changed(Object source) {
+		if (file.lastModified() != lastModified) {
+			return true;
+		}
+		return file.length() != length; 
+	}
+	
+	public interface Action {
+		public void changed();
+	}
+}
--- a/src/org/tmatesoft/hg/internal/Internals.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Thu May 09 21:06:48 2013 +0200
@@ -38,6 +38,7 @@
 import org.tmatesoft.hg.repo.HgRepositoryFiles;
 import org.tmatesoft.hg.repo.HgRepositoryLock;
 import org.tmatesoft.hg.repo.HgRuntimeException;
+import org.tmatesoft.hg.util.LogFacility;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.PathRewrite;
 
@@ -150,7 +151,7 @@
 	}
 	
 	public File getRepositoryFile(HgRepositoryFiles f) {
-		return f.residesUnderRepositoryRoot() ? getFileFromRepoDir(f.getName()) : getFileFromDataDir(f.getName());
+		return f.residesUnderRepositoryRoot() ? getFileFromRepoDir(f.getName()) : new File(repo.getWorkingDir(), f.getName());
 	}
 
 	/**
@@ -189,6 +190,10 @@
 		return repo.getSessionContext();
 	}
 	
+	public LogFacility getLog() {
+		return getSessionContext().getLog();
+	}
+	
 	public HgRepository getRepo() {
 		return repo;
 	}
--- a/src/org/tmatesoft/hg/repo/HgBookmarks.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgBookmarks.java	Thu May 09 21:06:48 2013 +0200
@@ -31,31 +31,38 @@
 import org.tmatesoft.hg.core.HgRepositoryLockException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.internal.FileChangeMonitor;
 import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.internal.LineReader;
 import org.tmatesoft.hg.util.LogFacility;
 
 /**
+ * Access to bookmarks state
  * 
  * @see http://mercurial.selenic.com/wiki/Bookmarks
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
 public final class HgBookmarks {
-	private final Internals internalRepo;
+	private final Internals repo;
 	private Map<String, Nodeid> bookmarks = Collections.emptyMap();
-	private String activeBookmark; 
+	private String activeBookmark;
+	private FileChangeMonitor activeTracker, bmFileTracker;
 
 	HgBookmarks(Internals internals) {
-		internalRepo = internals;
+		repo = internals;
 	}
 	
 	/*package-local*/ void read() throws HgInvalidControlFileException {
-		final LogFacility log = internalRepo.getSessionContext().getLog();
-		final HgRepository repo = internalRepo.getRepo();
-		File all = internalRepo.getFileFromRepoDir(HgRepositoryFiles.Bookmarks.getName());
+		readBookmarks();
+		readActiveBookmark();
+	}
+	
+	private void readBookmarks() throws HgInvalidControlFileException {
+		final LogFacility log = repo.getLog();
+		File all = repo.getRepositoryFile(HgRepositoryFiles.Bookmarks);
 		LinkedHashMap<String, Nodeid> bm = new LinkedHashMap<String, Nodeid>();
-		if (all.canRead()) {
+		if (all.canRead() && all.isFile()) {
 			LineReader lr1 = new LineReader(all, log);
 			ArrayList<String> c = new ArrayList<String>();
 			lr1.read(new LineReader.SimpleLineCollector(), c);
@@ -65,7 +72,7 @@
 					if (x > 0) {
 						Nodeid nid = Nodeid.fromAscii(s.substring(0, x));
 						String name = new String(s.substring(x+1));
-						if (repo.getChangelog().isKnown(nid)) {
+						if (repo.getRepo().getChangelog().isKnown(nid)) {
 							// copy name part not to drag complete line
 							bm.put(name, nid);
 						} else {
@@ -82,18 +89,40 @@
 		} else {
 			bookmarks = Collections.emptyMap();
 		}
+		if (bmFileTracker == null) {
+			bmFileTracker = new FileChangeMonitor(all);
+		}
+		bmFileTracker.touch(this);
+	}
 		
+	private void readActiveBookmark() throws HgInvalidControlFileException { 
 		activeBookmark = null;
-		File active = internalRepo.getFileFromRepoDir(HgRepositoryFiles.BookmarksCurrent.getName());
-		if (active.canRead()) {
-			LineReader lr2 = new LineReader(active, log);
+		File active = repo.getRepositoryFile(HgRepositoryFiles.BookmarksCurrent);
+		if (active.canRead() && active.isFile()) {
+			LineReader lr2 = new LineReader(active, repo.getLog());
 			ArrayList<String> c = new ArrayList<String>(2);
 			lr2.read(new LineReader.SimpleLineCollector(), c);
 			if (c.size() > 0) {
 				activeBookmark = c.get(0);
 			}
 		}
+		if (activeTracker == null) {
+			activeTracker = new FileChangeMonitor(active);
+		}
+		activeTracker.touch(this);
 	}
+	
+	/*package-local*/void reloadIfChanged() throws HgInvalidControlFileException {
+		assert activeTracker != null;
+		assert bmFileTracker != null;
+		if (bmFileTracker.changed(this)) {
+			readBookmarks();
+		}
+		if (activeTracker.changed(this)) {
+			readActiveBookmark();
+		}
+	}
+
 
 	/**
 	 * Tell name of the active bookmark 
@@ -155,8 +184,8 @@
 	}
 	
 	private void write() throws HgIOException, HgRepositoryLockException {
-		File bookmarksFile = internalRepo.getRepositoryFile(HgRepositoryFiles.Bookmarks);
-		HgRepositoryLock workingDirLock = internalRepo.getRepo().getWorkingDirLock();
+		File bookmarksFile = repo.getRepositoryFile(HgRepositoryFiles.Bookmarks);
+		HgRepositoryLock workingDirLock = repo.getRepo().getWorkingDirLock();
 		FileWriter fileWriter = null;
 		workingDirLock.acquire();
 		try {
@@ -174,7 +203,7 @@
 					fileWriter.close();
 				}
 			} catch (IOException ex) {
-				internalRepo.getSessionContext().getLog().dump(getClass(), Error, ex, null);
+				repo.getLog().dump(getClass(), Error, ex, null);
 			}
 			workingDirLock.release();
 		}
--- a/src/org/tmatesoft/hg/repo/HgBranches.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgBranches.java	Thu May 09 21:06:48 2013 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2012 TMate Software Ltd
+ * Copyright (c) 2011-2013 TMate Software Ltd
  *  
  * 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
@@ -38,6 +38,7 @@
 import java.util.regex.Pattern;
 
 import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.ChangelogMonitor;
 import org.tmatesoft.hg.internal.Experimental;
 import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
@@ -50,17 +51,18 @@
  */
 public class HgBranches {
 	
+	private final Internals internalRepo;
+	private final ChangelogMonitor repoChangeTracker;
 	private final Map<String, BranchInfo> branches = new TreeMap<String, BranchInfo>();
-	private final HgRepository repo;
-	private final Internals internalRepo;
 	private boolean isCacheActual = false;
 
 	HgBranches(Internals internals) {
 		internalRepo = internals;
-		repo = internals.getRepo(); // merely a cached value
+		repoChangeTracker = new ChangelogMonitor(internals.getRepo());
 	}
 
 	private int readCache() {
+		final HgRepository repo = internalRepo.getRepo();
 		File branchheadsCache = getCacheFile();
 		int lastInCache = -1;
 		if (!branchheadsCache.canRead()) {
@@ -130,6 +132,7 @@
 
 	void collect(final ProgressSupport ps) throws HgInvalidControlFileException {
 		branches.clear();
+		final HgRepository repo = internalRepo.getRepo();
 		ps.start(1 + repo.getChangelog().getRevisionCount() * 2);
 		//
 		int lastCached = readCache();
@@ -227,15 +230,16 @@
 		for (BranchInfo bi : branches.values()) {
 			bi.validate(clog, rmap);
 		}
+		repoChangeTracker.touch();
 		ps.done();
 	}
 
-	public List<BranchInfo> getAllBranches() {
+	public List<BranchInfo> getAllBranches() throws HgInvalidControlFileException {
 		return new LinkedList<BranchInfo>(branches.values());
 				
 	}
 
-	public BranchInfo getBranch(String name) {
+	public BranchInfo getBranch(String name) throws HgInvalidControlFileException {
 		return branches.get(name);
 	}
 
@@ -258,6 +262,7 @@
 		if (!branchheadsCache.canWrite()) {
 			return;
 		}
+		final HgRepository repo = internalRepo.getRepo();
 		final int lastRev = repo.getChangelog().getLastRevision();
 		final Nodeid lastNid = repo.getChangelog().getRevision(lastRev);
 		BufferedWriter bw = new BufferedWriter(new FileWriter(branchheadsCache));
@@ -280,6 +285,12 @@
 		// prior to 1.8 used to be .hg/branchheads.cache
 		return internalRepo.getFileFromRepoDir("cache/branchheads");
 	}
+	
+	/*package-local*/ void reloadIfChanged(ProgressSupport ps) throws HgInvalidControlFileException {
+		if (repoChangeTracker.isChanged()) {
+			collect(ps);
+		}
+	}
 
 	public static class BranchInfo {
 		private final String name;
--- a/src/org/tmatesoft/hg/repo/HgIgnore.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgIgnore.java	Thu May 09 21:06:48 2013 +0200
@@ -16,6 +16,9 @@
  */
 package org.tmatesoft.hg.repo;
 
+import static org.tmatesoft.hg.repo.HgRepositoryFiles.HgIgnore;
+import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
@@ -26,6 +29,8 @@
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
+import org.tmatesoft.hg.internal.FileChangeMonitor;
+import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.PathRewrite;
 
@@ -39,21 +44,47 @@
 
 	private List<Pattern> entries;
 	private final PathRewrite globPathHelper;
+	private FileChangeMonitor ignoreFileTracker; 
 
 	HgIgnore(PathRewrite globPathRewrite) {
 		entries = Collections.emptyList();
 		globPathHelper = globPathRewrite;
 	}
 
-	/* package-local */ List<String> read(File hgignoreFile) throws IOException {
-		if (!hgignoreFile.exists()) {
-			return null;
+	/* package-local */ void read(Internals repo) throws HgInvalidControlFileException {
+		File ignoreFile = repo.getRepositoryFile(HgIgnore);
+		BufferedReader fr = null;
+		try {
+			if (ignoreFile.canRead() && ignoreFile.isFile()) {
+				fr = new BufferedReader(new FileReader(ignoreFile));
+				final List<String> errors = read(fr);
+				if (errors != null) {
+					repo.getLog().dump(getClass(), Warn, "Syntax errors parsing %s:\n%s", ignoreFile.getName(), Internals.join(errors, ",\n"));
+				}
+			}
+			if (ignoreFileTracker == null) {
+				ignoreFileTracker = new FileChangeMonitor(ignoreFile);
+			}
+			ignoreFileTracker.touch(this);
+		} catch (IOException ex) {
+			final String m = String.format("Error reading %s file", ignoreFile);
+			throw new HgInvalidControlFileException(m, ex, ignoreFile);
+		} finally {
+			try {
+				if (fr != null) {
+					fr.close();
+				}
+			} catch (IOException ex) {
+				repo.getLog().dump(getClass(), Warn, ex, null); // it's read, don't treat as error
+			}
 		}
-		BufferedReader fr = new BufferedReader(new FileReader(hgignoreFile));
-		try {
-			return read(fr);
-		} finally {
-			fr.close();
+	}
+	
+	/*package-local*/ void reloadIfChanged(Internals repo) throws HgInvalidControlFileException {
+		assert ignoreFileTracker != null;
+		if (ignoreFileTracker.changed(this)) {
+			entries = Collections.emptyList();
+			read(repo);
 		}
 	}
 
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Thu May 09 21:06:48 2013 +0200
@@ -16,13 +16,12 @@
  */
 package org.tmatesoft.hg.repo;
 
-import static org.tmatesoft.hg.repo.HgRepositoryFiles.*;
-import static org.tmatesoft.hg.util.LogFacility.Severity.*;
+import static org.tmatesoft.hg.repo.HgRepositoryFiles.LastMessage;
+import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;
 
 import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
-import java.io.StringReader;
 import java.nio.CharBuffer;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -30,7 +29,6 @@
 
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
-import org.tmatesoft.hg.internal.ByteArrayChannel;
 import org.tmatesoft.hg.internal.ConfigFile;
 import org.tmatesoft.hg.internal.DirstateReader;
 import org.tmatesoft.hg.internal.Experimental;
@@ -40,7 +38,6 @@
 import org.tmatesoft.hg.internal.RevlogStream;
 import org.tmatesoft.hg.internal.SubrepoManager;
 import org.tmatesoft.hg.repo.ext.HgExtensionsManager;
-import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.Pair;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.PathRewrite;
@@ -110,11 +107,11 @@
 	private SubrepoManager subRepos;
 	private HgBookmarks bookmarks;
 	private HgExtensionsManager extManager;
-
-	private final org.tmatesoft.hg.internal.Internals impl;
 	private HgIgnore ignore;
 	private HgRepoConfig repoConfig;
 	
+	private final org.tmatesoft.hg.internal.Internals impl;
+	
 	/*
 	 * TODO [post-1.0] move to a better place, e.g. WorkingCopy container that tracks both dirstate and branches 
 	 * (and, perhaps, undo, lastcommit and other similar information), and is change listener so that we don't need to
@@ -157,7 +154,7 @@
 				return HgRepository.this.getChangelog().content;
 			}
 		});
-		normalizePath = impl.buildNormalizePathRewrite(); 
+		normalizePath = impl.buildNormalizePathRewrite();
 	}
 
 	@Override
@@ -201,55 +198,34 @@
 	}
 	
 	/**
+	 * Access snapshot of repository tags.
+	 * 
 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
 	 */
 	public HgTags getTags() throws HgInvalidControlFileException {
 		if (tags == null) {
-			tags = new HgTags(this);
-			HgDataFile hgTags = getFileNode(HgTags.getPath());
-			if (hgTags.exists()) {
-				for (int i = 0; i <= hgTags.getLastRevision(); i++) { // TODO post-1.0 in fact, would be handy to have walk(start,end) 
-					// method for data files as well, though it looks odd.
-					try {
-						ByteArrayChannel sink = new ByteArrayChannel();
-						hgTags.content(i, sink);
-						final String content = new String(sink.toArray(), "UTF8");
-						tags.readGlobal(new StringReader(content));
-					} catch (CancelledException ex) {
-						 // IGNORE, can't happen, we did not configure cancellation
-						getSessionContext().getLog().dump(getClass(), Debug, ex, null);
-					} catch (IOException ex) {
-						// UnsupportedEncodingException can't happen (UTF8)
-						// only from readGlobal. Need to reconsider exceptions thrown from there:
-						// BufferedReader wraps String and unlikely to throw IOException, perhaps, log is enough?
-						getSessionContext().getLog().dump(getClass(), Error, ex, null);
-						// XXX need to decide what to do this. failure to read single revision shall not break complete cycle
-					}
-				}
-			}
-			File file2read = null;
-			try {
-				file2read = new File(getWorkingDir(), HgTags.getPath());
-				tags.readGlobal(file2read); // XXX replace with HgDataFile.workingCopy
-				file2read = impl.getFileFromRepoDir(HgLocalTags.getName()); // XXX pass internalrepo to readLocal, keep filename there
-				tags.readLocal(file2read);
-			} catch (IOException ex) {
-				getSessionContext().getLog().dump(getClass(), Error, ex, null);
-				throw new HgInvalidControlFileException("Failed to read tags", ex, file2read);
-			}
+			tags = new HgTags(impl);
+			tags.read();
+		} else {
+			tags.reloadIfChanged();
 		}
 		return tags;
 	}
 	
 	/**
-	 * Access branch information
+	 * Access branch information. Returns a snapshot of branch information as it's available at the time of the call.
+	 * If repository get changed, use this method to obtain an up-to-date state. 
+	 * 
 	 * @return branch manager instance, never <code>null</code>
 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
 	 */
 	public HgBranches getBranches() throws HgInvalidControlFileException {
+		final ProgressSupport ps = ProgressSupport.Factory.get(null);
 		if (branches == null) {
 			branches = new HgBranches(impl);
-			branches.collect(ProgressSupport.Factory.get(null));
+			branches.collect(ps);
+		} else {
+			branches.reloadIfChanged(ps);
 		}
 		return branches;
 	}
@@ -370,16 +346,9 @@
 		// TODO read config for additional locations
 		if (ignore == null) {
 			ignore = new HgIgnore(getToRepoPathHelper());
-			File ignoreFile = new File(getWorkingDir(), HgIgnore.getPath());
-			try {
-				final List<String> errors = ignore.read(ignoreFile);
-				if (errors != null) {
-					getSessionContext().getLog().dump(getClass(), Warn, "Syntax errors parsing %s:\n%s", ignoreFile.getName(), Internals.join(errors, ",\n"));
-				}
-			} catch (IOException ex) {
-				final String m = String.format("Error reading %s file", ignoreFile);
-				throw new HgInvalidControlFileException(m, ex, ignoreFile);
-			}
+			ignore.read(impl);
+		} else {
+			ignore.reloadIfChanged(impl);
 		}
 		return ignore;
 	}
@@ -465,6 +434,8 @@
 		if (bookmarks == null) {
 			bookmarks = new HgBookmarks(impl);
 			bookmarks.read();
+		} else {
+			bookmarks.reloadIfChanged();
 		}
 		return bookmarks;
 	}
--- a/src/org/tmatesoft/hg/repo/HgTags.java	Wed May 08 17:11:45 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgTags.java	Thu May 09 21:06:48 2013 +0200
@@ -16,6 +16,9 @@
  */
 package org.tmatesoft.hg.repo;
 
+import static org.tmatesoft.hg.repo.HgRepositoryFiles.HgLocalTags;
+import static org.tmatesoft.hg.repo.HgRepositoryFiles.HgTags;
+import static org.tmatesoft.hg.util.LogFacility.Severity.*;
 import static org.tmatesoft.hg.util.LogFacility.Severity.Error;
 import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;
 
@@ -24,6 +27,7 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.Reader;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -34,6 +38,11 @@
 
 import org.tmatesoft.hg.core.HgBadNodeidFormatException;
 import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.ByteArrayChannel;
+import org.tmatesoft.hg.internal.ChangelogMonitor;
+import org.tmatesoft.hg.internal.FileChangeMonitor;
+import org.tmatesoft.hg.internal.Internals;
+import org.tmatesoft.hg.util.CancelledException;
 
 /**
  * @see http://mercurial.selenic.com/wiki/TagDesign
@@ -45,38 +54,83 @@
 	// global tags come from ".hgtags"
 	// local come from ".hg/localtags"
 
-	private final HgRepository repo;
+	private final Internals repo;
 
 	private final Map<Nodeid, List<String>> globalToName;
 	private final Map<Nodeid, List<String>> localToName;
 	private final Map<String, List<Nodeid>> globalFromName;
 	private final Map<String, List<Nodeid>> localFromName;
 	
+	private FileChangeMonitor globalTagsFileMonitor, localTagsFileMonitor;
+	private ChangelogMonitor repoChangeMonitor;
+	
 	private Map<String, TagInfo> tags;
 	
-	/*package-local*/ HgTags(HgRepository hgRepo) {
-		repo = hgRepo;
+	/*package-local*/ HgTags(Internals internalRepo) {
+		repo = internalRepo;
 		globalToName =  new HashMap<Nodeid, List<String>>();
 		localToName  =  new HashMap<Nodeid, List<String>>();
 		globalFromName = new TreeMap<String, List<Nodeid>>();
 		localFromName  = new TreeMap<String, List<Nodeid>>();
 	}
 	
-	/*package-local*/ void readLocal(File localTags) throws IOException {
-		if (localTags == null || localTags.isDirectory()) {
-			throw new IllegalArgumentException(String.valueOf(localTags));
-		}
-		read(localTags, localToName, localFromName);
+	/*package-local*/ void read() throws HgInvalidControlFileException {
+		readTagsFromHistory();
+		readGlobal();
+		readLocal();
 	}
 	
-	/*package-local*/ void readGlobal(File globalTags) throws IOException {
-		if (globalTags == null || globalTags.isDirectory()) {
-			throw new IllegalArgumentException(String.valueOf(globalTags));
+	private void readTagsFromHistory() throws HgInvalidControlFileException {
+		HgDataFile hgTags = repo.getRepo().getFileNode(HgTags.getPath());
+		if (hgTags.exists()) {
+			for (int i = 0; i <= hgTags.getLastRevision(); i++) { // TODO post-1.0 in fact, would be handy to have walk(start,end) 
+				// method for data files as well, though it looks odd.
+				try {
+					ByteArrayChannel sink = new ByteArrayChannel();
+					hgTags.content(i, sink);
+					final String content = new String(sink.toArray(), "UTF8");
+					readGlobal(new StringReader(content));
+				} catch (CancelledException ex) {
+					 // IGNORE, can't happen, we did not configure cancellation
+					repo.getLog().dump(getClass(), Debug, ex, null);
+				} catch (IOException ex) {
+					// UnsupportedEncodingException can't happen (UTF8)
+					// only from readGlobal. Need to reconsider exceptions thrown from there:
+					// BufferedReader wraps String and unlikely to throw IOException, perhaps, log is enough?
+					repo.getLog().dump(getClass(), Error, ex, null);
+					// XXX need to decide what to do this. failure to read single revision shall not break complete cycle
+				}
+			}
 		}
-		read(globalTags, globalToName, globalFromName);
+		if (repoChangeMonitor == null) {
+			repoChangeMonitor = new ChangelogMonitor(repo.getRepo());
+		}
+		repoChangeMonitor.touch();
+	}
+	
+	private void readLocal() throws HgInvalidControlFileException {
+		File localTags = repo.getRepositoryFile(HgLocalTags);
+		if (localTags.canRead() && localTags.isFile()) {
+			read(localTags, localToName, localFromName);
+		}
+		if (localTagsFileMonitor == null) { 
+			localTagsFileMonitor = new FileChangeMonitor(localTags);
+		}
+		localTagsFileMonitor.touch(this);
+	}
+	
+	private void readGlobal() throws HgInvalidControlFileException {
+		File globalTags = repo.getRepositoryFile(HgTags); // XXX replace with HgDataFile.workingCopy
+		if (globalTags.canRead() && globalTags.isFile()) {
+			read(globalTags, globalToName, globalFromName);
+		}
+		if (globalTagsFileMonitor == null) {
+			globalTagsFileMonitor = new FileChangeMonitor(globalTags);
+		}
+		globalTagsFileMonitor.touch(this);
 	}
 
-	/*package-local*/ void readGlobal(Reader globalTags) throws IOException {
+	private void readGlobal(Reader globalTags) throws IOException {
 		BufferedReader r = null;
 		try {
 			r = new BufferedReader(globalTags);
@@ -88,7 +142,7 @@
 		}
 	}
 	
-	private void read(File f, Map<Nodeid,List<String>> nid2name, Map<String, List<Nodeid>> name2nid) throws IOException {
+	private void read(File f, Map<Nodeid,List<String>> nid2name, Map<String, List<Nodeid>> name2nid) throws HgInvalidControlFileException {
 		if (!f.canRead()) {
 			return;
 		}
@@ -96,9 +150,17 @@
 		try {
 			r = new BufferedReader(new FileReader(f));
 			read(r, nid2name, name2nid);
+		} catch (IOException ex) {
+			repo.getLog().dump(getClass(), Error, ex, null);
+			throw new HgInvalidControlFileException("Failed to read tags", ex, f);
 		} finally {
 			if (r != null) {
-				r.close();
+				try {
+					r.close();
+				} catch (IOException ex) {
+					// since it's read operation, do not treat close failure as error, but let user know, anyway
+					repo.getLog().dump(getClass(), Warn, ex, null);
+				}
 			}
 		}
 	}
@@ -112,7 +174,7 @@
 			}
 			final int spacePos = line.indexOf(' ');
 			if (line.length() < 40+2 /*nodeid, space and at least single-char tagname*/ || spacePos != 40) {
-				repo.getSessionContext().getLog().dump(getClass(), Warn, "Bad tags line: %s", line); 
+				repo.getLog().dump(getClass(), Warn, "Bad tags line: %s", line); 
 				continue;
 			}
 			try {
@@ -154,7 +216,7 @@
 					revTags.add(tagName);
 				}
 			} catch (HgBadNodeidFormatException ex) {
-				repo.getSessionContext().getLog().dump(getClass(), Error, "Bad revision '%s' in line '%s':%s", line.substring(0, spacePos), line, ex.getMessage()); 
+				repo.getLog().dump(getClass(), Error, "Bad revision '%s' in line '%s':%s", line.substring(0, spacePos), line, ex.getMessage()); 
 			}
 		}
 	}
@@ -217,6 +279,23 @@
 		return rv;
 	}
 
+	// can be called only after instance has been initialized (#read() invoked) 
+	/*package-local*/void reloadIfChanged() throws HgInvalidControlFileException {
+		assert repoChangeMonitor != null;
+		assert localTagsFileMonitor != null;
+		assert globalTagsFileMonitor != null;
+		if (repoChangeMonitor.isChanged() || globalTagsFileMonitor.changed(this)) {
+			globalFromName.clear();
+			globalToName.clear();
+			readTagsFromHistory();
+			readGlobal();
+			tags = null;
+		}
+		if (localTagsFileMonitor.changed(this)) {
+			readLocal();
+			tags = null;
+		}
+	}
 	
 	public final class TagInfo {
 		private final String name;
@@ -235,8 +314,8 @@
 
 		public String branch() throws HgInvalidControlFileException {
 			if (branch == null) {
-				int x = repo.getChangelog().getRevisionIndex(revision());
-				branch = repo.getChangelog().range(x, x).get(0).branch();
+				int x = repo.getRepo().getChangelog().getRevisionIndex(revision());
+				branch = repo.getRepo().getChangelog().range(x, x).get(0).branch();
 			}
 			return branch;
 		}