src/main/java/com/semanticcms/autogit/servlet/AutoGitContextListener.java
private void updateGitStatus() throws IOException, ParseException {
		long now = System.currentTimeMillis();
		Path gtl;
		synchronized(gitToplevelLock) {
			gtl = gitToplevel;
		}
		GitStatus newStatus;
		if(gtl == null) {
			// Java 1.8: Inline this
			List<UncommittedChange> emptyList = Collections.emptyList();
			newStatus = new GitStatus(now, State.DISABLED, emptyList);
		} else {
			// Get a list of modules including "" for main module
			List<String> modules;
			{
				if(DEBUG) log("Finding modules");
				ProcessBuilder pb =
					new ProcessBuilder("git", "submodule", "--quiet", "foreach", "--recursive", "echo \"$path\"")
					.directory(gtl.toFile())
				;
				Process p = pb.start();
				ProcessResult result = ProcessResult.getProcessResult(p);
				if(result.getExitVal() != 0) throw new IOException("Unable to find submodules: " + result.getStderr());
				List<String> submodules = StringUtility.splitLines(result.getStdout());
				if(DEBUG) {
					for(String submodule : submodules) log("Got submodule: " + submodule);
				}
				// Add the empty module list
				modules = new ArrayList<>(submodules.size() + 1);
				modules.addAll(submodules);
				modules.add("");
			}
			// Get the status of each module
			State state = State.SYNCHRONIZED;
			List<UncommittedChange> uncommittedChanges = new ArrayList<>();
			for(String module : modules) {
				if(DEBUG) log("Getting status of module \"" + module + "\"");
				File workingDir;
				if(module.isEmpty()) {
					// Main module
					workingDir = gtl.toFile();
				} else {
					// Submodule
					workingDir = new File(gtl.toFile(), module);
				}
				ProcessBuilder pb =
					new ProcessBuilder("git", "status", "--porcelain", "-z")
					.directory(workingDir)
				;
				Process p = pb.start();
				ProcessResult result = ProcessResult.getProcessResult(p);
				if(result.getExitVal() != 0) throw new IOException("Unable to get status: " + result.getStderr());
				// Split on NUL (ASCII 0)
				List<String> split = new ArrayList<>(StringUtility.splitString(result.getStdout(), (char)0));
				if(!split.isEmpty()) {
					// Remove last empty part of split
					String last = split.remove(split.size()-1);
					if(!last.isEmpty()) throw new ParseException("Last element of split is not empty: " + last, 0);
				}
				//if((split.size() % 2) != 0) throw new ParseException("Unexpected split size: " + split.size(), 0);
				int i = 0;
				while(i < split.size()) {
					char x;
					char y;
					String from;
					String to;
					{
						String first = split.get(i++);
						if(first.length() < 3) throw new ParseException("split1 length too short: " + first.length(), 0);
						x = first.charAt(0);
						y = first.charAt(1);
						if(first.charAt(2) != ' ') throw new ParseException("Third character of split1 is not a space: " + first.charAt(2), 0);
						if(x == 'R') {
							// Is rename, will have both to and from
							to = first.substring(3);
							from = split.get(i++);
						} else {
							// Will have from only from
							to = null;
							from = first.substring(3);
						}
					}
					if(DEBUG) {
						log("x = \"" + x + "\"");
						log("y = \"" + y + "\"");
						log("from = \"" + from + "\"");
						log("to = \"" + to + "\"");
					}
					UncommittedChange uncommittedChange = new UncommittedChange(x, y, module, from, to);
					uncommittedChanges.add(uncommittedChange);
					// Keep the highest state
					State meaningState = uncommittedChange.getMeaning().getState();
					if(meaningState.compareTo(state) > 0) state = meaningState;
				}
			}
			if(DEBUG) log("state = " + state);
			// TODO: auto-commit any modified tasklogs
			// TODO: git pull all modules always to stay in sync
			// TODO: git push all modules if state is synchronized
			newStatus = new GitStatus(now, state, Collections.unmodifiableList(uncommittedChanges));
		}
		synchronized(statusLock) {
			status = newStatus;
		}
	}

	/**
	 * Gets the current Git status, will not block.
	 */
	public GitStatus getGitStatus() {
		long now = System.currentTimeMillis();
		synchronized(statusLock) {
			// Timeout when status not updated recently enough
			long statusTime = status.getStatusTime();
			long millisSince = now - statusTime;
			if(
				millisSince >= TIMEOUT_MILLIS
				|| millisSince <= -TIMEOUT_MILLIS // Can happen when system time reset
			) {
				// Java 1.8: Inline this
				List<UncommittedChange> emptyList = Collections.emptyList();
				return new GitStatus(statusTime, State.TIMEOUT, emptyList);
			}
			// Return most recently determined status
			return status;
		}
	}

	public static AutoGitContextListener getInstance(ServletContext sc) {
		return (AutoGitContextListener)sc.getAttribute(APPLICATION_SCOPE_KEY);
	}

	private static final String GIT_STATUS_REQUEST_CACHE_KEY = AutoGitContextListener.class.getName() + ".getGitStatus.cache";

	public static GitStatus getGitStatus(
		ServletContext servletContext,
		ServletRequest request
	) {
		// Look for cached value
		GitStatus gitStatus = (GitStatus)request.getAttribute(GIT_STATUS_REQUEST_CACHE_KEY);
		if(gitStatus == null) {
			AutoGitContextListener gitContext = AutoGitContextListener.getInstance(servletContext);
			if(gitContext == null) {
				// Java 1.8: Inline this
				List<UncommittedChange> emptyList = Collections.emptyList();
				gitStatus = new GitStatus(System.currentTimeMillis(), State.DISABLED, emptyList);
			} else {
				gitStatus = gitContext.getGitStatus();
			}
			request.setAttribute(GIT_STATUS_REQUEST_CACHE_KEY, gitStatus);
		}
		return gitStatus;
	}
}
private void updateGitStatus() throws IOException, ParseException {
		long now = System.currentTimeMillis();
		Path gtl;
		synchronized(gitToplevelLock) {
			gtl = gitToplevel;
		}
		GitStatus newStatus;
		if(gtl == null) {
			// Java 1.8: Inline this
			List<UncommittedChange> emptyList = Collections.emptyList();
			newStatus = new GitStatus(now, State.DISABLED, emptyList);
		} else {
			// Get a list of modules including "" for main module
			List<String> modules;
			{
				if(DEBUG) log("Finding modules");
				ProcessBuilder pb =
					new ProcessBuilder("git", "submodule", "--quiet", "foreach", "--recursive", "echo \"$path\"")
					.directory(gtl.toFile())
				;
				Process p = pb.start();
				ProcessResult result = ProcessResult.getProcessResult(p);
				if(result.getExitVal() != 0) throw new IOException("Unable to find submodules: " + result.getStderr());
				List<String> submodules = StringUtility.splitLines(result.getStdout());
				if(DEBUG) {
					for(String submodule : submodules) log("Got submodule: " + submodule);
				}
				// Add the empty module list
				modules = new ArrayList<>(submodules.size() + 1);
				modules.addAll(submodules);
				modules.add("");
			}
			// Get the status of each module
			State state = State.SYNCHRONIZED;
			List<UncommittedChange> uncommittedChanges = new ArrayList<>();
			for(String module : modules) {
				if(DEBUG) log("Getting status of module \"" + module + "\"");
				File workingDir;
				if(module.isEmpty()) {
					// Main module
					workingDir = gtl.toFile();
				} else {
					// Submodule
					workingDir = new File(gtl.toFile(), module);
				}
				ProcessBuilder pb =
					new ProcessBuilder("git", "status", "--porcelain", "-z")
					.directory(workingDir)
				;
				Process p = pb.start();
				ProcessResult result = ProcessResult.getProcessResult(p);
				if(result.getExitVal() != 0) throw new IOException("Unable to get status: " + result.getStderr());
				// Split on NUL (ASCII 0)
				List<String> split = new ArrayList<>(StringUtility.splitString(result.getStdout(), (char)0));
				if(!split.isEmpty()) {
					// Remove last empty part of split
					String last = split.remove(split.size()-1);
					if(!last.isEmpty()) throw new ParseException("Last element of split is not empty: " + last, 0);
				}
				//if((split.size() % 2) != 0) throw new ParseException("Unexpected split size: " + split.size(), 0);
				int i = 0;
				while(i < split.size()) {
					char x;
					char y;
					String from;
					String to;
					{
						String first = split.get(i++);
						if(first.length() < 3) throw new ParseException("split1 length too short: " + first.length(), 0);
						x = first.charAt(0);
						y = first.charAt(1);
						if(first.charAt(2) != ' ') throw new ParseException("Third character of split1 is not a space: " + first.charAt(2), 0);
						if(x == 'R') {
							// Is rename, will have both to and from
							to = first.substring(3);
							from = split.get(i++);
						} else {
							// Will have from only from
							to = null;
							from = first.substring(3);
						}
					}
					if(DEBUG) {
						log("x = \"" + x + "\"");
						log("y = \"" + y + "\"");
						log("from = \"" + from + "\"");
						log("to = \"" + to + "\"");
					}
					UncommittedChange uncommittedChange = new UncommittedChange(x, y, module, from, to);
					uncommittedChanges.add(uncommittedChange);
					// Keep the highest state
					State meaningState = uncommittedChange.getMeaning().getState();
					if(meaningState.compareTo(state) > 0) state = meaningState;
				}
			}
			if(DEBUG) log("state = " + state);
			// TODO: auto-commit any modified tasklogs
			// TODO: git pull all modules always to stay in sync
			// TODO: git push all modules if state is synchronized
			newStatus = new GitStatus(now, state, Collections.unmodifiableList(uncommittedChanges));
		}
		synchronized(statusLock) {
			status = newStatus;
		}
	}

	/**
	 * Gets the current Git status, will not block.
	 */
	public GitStatus getGitStatus() {
		long now = System.currentTimeMillis();
		synchronized(statusLock) {
			// Timeout when status not updated recently enough
			long statusTime = status.getStatusTime();
			long millisSince = now - statusTime;
			if(
				millisSince >= TIMEOUT_MILLIS
				|| millisSince <= -TIMEOUT_MILLIS // Can happen when system time reset
			) {
				// Java 1.8: Inline this
				List<UncommittedChange> emptyList = Collections.emptyList();
				return new GitStatus(statusTime, State.TIMEOUT, emptyList);
			}
			// Return most recently determined status
			return status;
		}
	}

	public static AutoGitContextListener getInstance(ServletContext sc) {
		return (AutoGitContextListener)sc.getAttribute(APPLICATION_SCOPE_KEY);
	}

	private static final String GIT_STATUS_REQUEST_CACHE_KEY = AutoGitContextListener.class.getName() + ".getGitStatus.cache";

	public static GitStatus getGitStatus(
		ServletContext servletContext,
		ServletRequest request
	) {
		// Look for cached value
		GitStatus gitStatus = (GitStatus)request.getAttribute(GIT_STATUS_REQUEST_CACHE_KEY);
		if(gitStatus == null) {
			AutoGitContextListener gitContext = AutoGitContextListener.getInstance(servletContext);
			if(gitContext == null) {
				// Java 1.8: Inline this
				List<UncommittedChange> emptyList = Collections.emptyList();
				gitStatus = new GitStatus(System.currentTimeMillis(), State.DISABLED, emptyList);
			} else {
				gitStatus = gitContext.getGitStatus();
			}
			request.setAttribute(GIT_STATUS_REQUEST_CACHE_KEY, gitStatus);
		}
		return gitStatus;
	}
}
Updates the current Git status, will block on I/O.
Automatically does git pull and git push when there are no uncommitted changes.
<p>
Called on startup.
Also called when file change detected and the Git status should be updated.
</p>
TODO: Add timeout within this method?