changeset 378:9fb990c8a724

Investigate approaches to alter Mercurial configuration files
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 09 Feb 2012 04:15:17 +0100
parents 86f049e6bcae
children fa2be7a05af6
files src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java src/org/tmatesoft/hg/internal/ConfigFile.java src/org/tmatesoft/hg/internal/Internals.java
diffstat 3 files changed, 247 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java	Thu Feb 09 04:15:17 2012 +0100
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2012 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.core;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.tmatesoft.hg.internal.ConfigFile;
+import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.internal.Internals;
+import org.tmatesoft.hg.repo.HgRepository;
+
+/**
+ * WORK IN PROGRESS, DO NOT USE
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@Experimental(reason="Investigating approaches to alter Hg configuration files")
+public final class HgUpdateConfigCommand extends HgAbstractCommand<HgUpdateConfigCommand> {
+	
+	private final HgRepository repo;
+	private final File configFile;
+	
+	private Map<String,List<String>> toRemove;
+	private Map<String,Map<String,String>> toSet;
+
+	private HgUpdateConfigCommand(HgRepository hgRepo, File configurationFile) {
+		repo = hgRepo;
+		configFile = configurationFile;
+	}
+	
+	public static HgUpdateConfigCommand forRepository(HgRepository hgRepo) {
+		return new HgUpdateConfigCommand(hgRepo, new File(".hg/hgrc"));
+	}
+	
+	public static HgUpdateConfigCommand forUser(HgRepository hgRepo) {
+		return new HgUpdateConfigCommand(null, Internals.getUserConfigurationFileToWrite());
+	}
+	
+	public static HgUpdateConfigCommand forInstallation() {
+		return new HgUpdateConfigCommand(null, Internals.getInstallationConfigurationFileToWrite());
+	}
+	
+	/**
+	 * Remove a property altogether
+	 * @return <code>this</code> for convenience
+	 */
+	public HgUpdateConfigCommand remove(String section, String key) {
+		if (toRemove == null) {
+			toRemove = new LinkedHashMap<String, List<String>>();
+		}
+		List<String> s = toRemove.get(section);
+		if (s == null) {
+			toRemove.put(section, s = new ArrayList<String>(5));
+		}
+		s.add(key);
+		if (toSet != null && toSet.containsKey(section)) {
+			toSet.get(section).remove(key);
+		}
+		return this;
+	}
+
+	/**
+	 * Delete single attribute in a multi-valued property
+	 * @return <code>this</code> for convenience
+	 */
+	public HgUpdateConfigCommand remove(String section, String key, String value) {
+		throw new UnsupportedOperationException();
+	}
+	
+	/**
+	 * Set single-valued properties or update multi-valued with a single value
+	 * @return <code>this</code> for convenience
+	 */
+	public HgUpdateConfigCommand put(String section, String key, String value) {
+		if (toSet == null) {
+			toSet =  new LinkedHashMap<String, Map<String,String>>();
+		}
+		Map<String,String> s = toSet.get(section);
+		if (s == null) {
+			toSet.put(section, s = new LinkedHashMap<String, String>());
+		}
+		s.put(key, value);
+		return this;
+	}
+	
+	/**
+	 * Multi-valued properties
+	 * @return <code>this</code> for convenience
+	 */
+	public HgUpdateConfigCommand add(String section, String key, String value) {
+		throw new UnsupportedOperationException();
+	}
+	
+	public void execute() throws HgException {
+		try {
+			ConfigFile cfg = new ConfigFile();
+			cfg.addLocation(configFile);
+			if (toRemove != null) {
+				for (Map.Entry<String,List<String>> s : toRemove.entrySet()) {
+					for (String e : s.getValue()) {
+						cfg.putString(s.getKey(), e, null);
+					}
+				}
+			}
+			if (toSet != null) {
+				for (Map.Entry<String,Map<String,String>> s : toSet.entrySet()) {
+					for (Map.Entry<String, String> e : s.getValue().entrySet()) {
+						cfg.putString(s.getKey(), e.getKey(), e.getValue());
+					}
+				}
+			}
+			cfg.writeTo(configFile);
+		} catch (IOException ex) {
+			throw new HgInvalidFileException("Failed to update configuration file", ex, configFile);
+		}
+	}
+
+
+	public static void main(String[] args) throws Exception {
+		HgUpdateConfigCommand cmd = HgUpdateConfigCommand.forUser(null);
+		cmd.remove("test1", "sample1");
+		cmd.put("test2", "sample2", "value2");
+		cmd.put("ui", "user-name", "Another User <email@domain.com>");
+		cmd.execute();
+	}
+}
--- a/src/org/tmatesoft/hg/internal/ConfigFile.java	Thu Feb 02 16:16:19 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/ConfigFile.java	Thu Feb 09 04:15:17 2012 +0100
@@ -17,11 +17,18 @@
 package org.tmatesoft.hg.internal;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -52,7 +59,7 @@
 	}
 
 	public Map<String,String> getSection(String sectionName) {
-		if (sections ==  null) {
+		if (sections == null) {
 			return Collections.emptyMap();
 		}
 		int x = sections.indexOf(sectionName);
@@ -79,7 +86,31 @@
 		String value = getSection(sectionName).get(key);
 		return value == null ? defaultValue : value;
 	}
-
+	
+	public void putString(String sectionName, String key, String newValue) {
+		Map<String, String> section = null;
+		if (sections == null) {
+			// init, in case we didn't read any file with prev config
+			sections = new ArrayList<String>();
+			content = new ArrayList<Map<String,String>>();
+		}
+		int x = sections.indexOf(sectionName);
+		if (x == -1) {
+			if (newValue == null) {
+				return;
+			}
+			sections.add(sectionName);
+			content.add(section = new LinkedHashMap<String, String>());
+		} else {
+			section = content.get(x);
+		}
+		if (newValue == null) {
+			section.remove(key);
+		} else {
+			section.put(key, newValue);
+		}
+	}
+	
 	// TODO handle %include and %unset directives
 	// TODO "" and lists
 	// XXX perhaps, single string to keep whole section with substrings for keys/values to minimize number of arrays (String.value)
@@ -141,4 +172,42 @@
 		}
 		assert sections.size() == content.size();
 	}
+
+	public void writeTo(File f) throws IOException {
+		byte[] data = compose();
+		if (!f.exists()) {
+			f.createNewFile();
+		}
+		FileChannel fc = new FileOutputStream(f).getChannel();
+		FileLock fl = fc.lock();
+		try {
+			fc.write(ByteBuffer.wrap(data));
+		} finally {
+			fl.release();
+			fc.close();
+		}
+	}
+	
+	private byte[] compose() {
+		ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
+		PrintStream ps = new PrintStream(bos);
+		Iterator<String> sectionNames = sections.iterator();
+		for (Map<String,String> s : content) {
+			final String name = sectionNames.next(); // iterate through names despite section may be empty
+			if (s.isEmpty()) {
+				continue; // do not write an empty section
+			}
+			ps.print('[');
+			ps.print(name);
+			ps.println(']');
+			for (Map.Entry<String, String> e : s.entrySet()) {
+				ps.print(e.getKey());
+				ps.print('=');
+				ps.println(e.getValue());
+			}
+			ps.println();
+		}
+		ps.flush();
+		return bos.toByteArray();
+	}
 }
--- a/src/org/tmatesoft/hg/internal/Internals.java	Thu Feb 02 16:16:19 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Thu Feb 09 04:15:17 2012 +0100
@@ -122,6 +122,7 @@
 			//
 			// XXX perhaps, makes sense to compare getenv(USERPROFILE) and getenv(HOME) and use 
 			// them if set (and use both if their values do not match). Only if both not set, rely to user.home?
+			// Also respect #getUserConfigurationFileToWrite() below
 			configFile.addLocation(new File(System.getProperty("user.home"), "Mercurial.ini"));
 		} else {
 			// FIXME read from install-root
@@ -143,4 +144,33 @@
 		configFile.addLocation(new File(repoRoot, "hgrc"));
 		return configFile;
 	}
+
+	/**
+	 * @return
+	 */
+	public static File getInstallationConfigurationFileToWrite() {
+		// TODO Auto-generated method stub
+		// FIXME find out install-root 
+		throw new UnsupportedOperationException();
+	}
+
+	/**
+	 * @return
+	 */
+	public static File getUserConfigurationFileToWrite() {
+		final File rv = new File(System.getProperty("user.home"), ".hgrc");
+		if (rv.exists() && rv.canWrite()) {
+			return rv;
+		}
+		if (runningOnWindows()) {
+			// try another well-known location
+			// TODO comment above regarding USERPROFILE and HOME variables applies here as well
+			File f = new File(System.getProperty("user.home"), "Mercurial.ini");
+			if (f.exists() && f.canWrite()) {
+				return f;
+			}
+		}
+		// fallback to default value
+		return rv; 
+	}
 }