Rely upon Constants.CHARSET over Constants.CHARACTER_ENCODING
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RepositoryConfig.java
blob048940d87cd8c991c6efa62199ce15d4c02d0d5c
1 /*
2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
4 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
5 * Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com>
7 * All rights reserved.
9 * Redistribution and use in source and binary forms, with or
10 * without modification, are permitted provided that the following
11 * conditions are met:
13 * - Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
16 * - Redistributions in binary form must reproduce the above
17 * copyright notice, this list of conditions and the following
18 * disclaimer in the documentation and/or other materials provided
19 * with the distribution.
21 * - Neither the name of the Git Development Community nor the
22 * names of its contributors may be used to endorse or promote
23 * products derived from this software without specific prior
24 * written permission.
26 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
27 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
28 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
29 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
31 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
33 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
34 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
36 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
37 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
38 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41 package org.spearce.jgit.lib;
43 import java.io.BufferedReader;
44 import java.io.BufferedWriter;
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.FileNotFoundException;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.io.InputStreamReader;
51 import java.io.OutputStreamWriter;
52 import java.io.PrintWriter;
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.HashMap;
56 import java.util.Iterator;
57 import java.util.List;
58 import java.util.Map;
60 import org.spearce.jgit.util.FS;
62 /**
63 * An object representing the Git config file.
65 * This can be either the repository specific file or the user global
66 * file depending on how it is instantiated.
68 public class RepositoryConfig {
69 /**
70 * Obtain a new configuration instance for ~/.gitconfig.
72 * @return a new configuration instance to read the user's global
73 * configuration file from their home directory.
75 public static RepositoryConfig openUserConfig() {
76 return new RepositoryConfig(null, new File(FS.userHome(), ".gitconfig"));
79 private final RepositoryConfig baseConfig;
81 /** Section name for a remote configuration */
82 public static final String REMOTE_SECTION = "remote";
84 /** Section name for a branch configuration. */
85 public static final String BRANCH_SECTION = "branch";
87 private final File configFile;
89 private boolean readFile;
91 private CoreConfig core;
93 private List<Entry> entries;
95 private Map<String, Object> byName;
97 private static final String MAGIC_EMPTY_VALUE = "%%magic%%empty%%";
99 RepositoryConfig(final Repository repo) {
100 this(openUserConfig(), FS.resolve(repo.getDirectory(), "config"));
104 * Create a Git configuration file reader/writer/cache for a specific file.
106 * @param base
107 * configuration that provides default values if this file does
108 * not set/override a particular key. Often this is the user's
109 * global configuration file, or the system level configuration.
110 * @param cfgLocation
111 * path of the file to load (or save).
113 public RepositoryConfig(final RepositoryConfig base, final File cfgLocation) {
114 baseConfig = base;
115 configFile = cfgLocation;
116 clear();
120 * @return Core configuration values
122 public CoreConfig getCore() {
123 return core;
127 * Obtain an integer value from the configuration.
129 * @param section
130 * section the key is grouped within.
131 * @param name
132 * name of the key to get.
133 * @param defaultValue
134 * default value to return if no value was present.
135 * @return an integer value from the configuration, or defaultValue.
137 public int getInt(final String section, final String name,
138 final int defaultValue) {
139 return getInt(section, null, name, defaultValue);
143 * Obtain an integer value from the configuration.
145 * @param section
146 * section the key is grouped within.
147 * @param subsection
148 * subsection name, such a remote or branch name.
149 * @param name
150 * name of the key to get.
151 * @param defaultValue
152 * default value to return if no value was present.
153 * @return an integer value from the configuration, or defaultValue.
155 public int getInt(final String section, String subsection,
156 final String name, final int defaultValue) {
157 final String str = getString(section, subsection, name);
158 if (str == null)
159 return defaultValue;
161 String n = str.trim();
162 if (n.length() == 0)
163 return defaultValue;
165 int mul = 1;
166 switch (Character.toLowerCase(n.charAt(n.length() - 1))) {
167 case 'g':
168 mul = 1024 * 1024 * 1024;
169 break;
170 case 'm':
171 mul = 1024 * 1024;
172 break;
173 case 'k':
174 mul = 1024;
175 break;
177 if (mul > 1)
178 n = n.substring(0, n.length() - 1).trim();
179 if (n.length() == 0)
180 return defaultValue;
182 try {
183 return mul * Integer.parseInt(n);
184 } catch (NumberFormatException nfe) {
185 throw new IllegalArgumentException("Invalid integer value: "
186 + section + "." + name + "=" + str);
191 * Get a boolean value from the git config
193 * @param section
194 * section the key is grouped within.
195 * @param name
196 * name of the key to get.
197 * @param defaultValue
198 * default value to return if no value was present.
199 * @return true if any value or defaultValue is true, false for missing or
200 * explicit false
202 protected boolean getBoolean(final String section, final String name,
203 final boolean defaultValue) {
204 return getBoolean(section, null, name, defaultValue);
208 * Get a boolean value from the git config
210 * @param section
211 * section the key is grouped within.
212 * @param subsection
213 * subsection name, such a remote or branch name.
214 * @param name
215 * name of the key to get.
216 * @param defaultValue
217 * default value to return if no value was present.
218 * @return true if any value or defaultValue is true, false for missing or
219 * explicit false
221 protected boolean getBoolean(final String section, String subsection,
222 final String name, final boolean defaultValue) {
223 String n = getRawString(section, subsection, name);
224 if (n == null)
225 return defaultValue;
227 n = n.toLowerCase();
228 if (MAGIC_EMPTY_VALUE.equals(n) || "yes".equals(n) || "true".equals(n) || "1".equals(n)) {
229 return true;
230 } else if ("no".equals(n) || "false".equals(n) || "0".equals(n)) {
231 return false;
232 } else {
233 throw new IllegalArgumentException("Invalid boolean value: "
234 + section + "." + name + "=" + n);
239 * @param section
240 * @param subsection
241 * @param name
242 * @return a String value from git config.
244 public String getString(final String section, String subsection, final String name) {
245 String val = getRawString(section, subsection, name);
246 if (MAGIC_EMPTY_VALUE.equals(val)) {
247 return "";
249 return val;
253 * @param section
254 * @param subsection
255 * @param name
256 * @return array of zero or more values from the configuration.
258 public String[] getStringList(final String section, String subsection,
259 final String name) {
260 final Object o = getRawEntry(section, subsection, name);
261 if (o instanceof List) {
262 final List lst = (List) o;
263 final String[] r = new String[lst.size()];
264 for (int i = 0; i < r.length; i++) {
265 final String val = ((Entry) lst.get(i)).value;
266 r[i] = MAGIC_EMPTY_VALUE.equals(val) ? "" : val;
268 return r;
271 if (o instanceof Entry) {
272 final String val = ((Entry) o).value;
273 return new String[] { MAGIC_EMPTY_VALUE.equals(val) ? "" : val };
276 if (baseConfig != null)
277 return baseConfig.getStringList(section, subsection, name);
278 return new String[0];
281 private String getRawString(final String section, final String subsection,
282 final String name) {
283 final Object o = getRawEntry(section, subsection, name);
284 if (o instanceof List) {
285 return ((Entry) ((List) o).get(0)).value;
286 } else if (o instanceof Entry) {
287 return ((Entry) o).value;
288 } else if (baseConfig != null)
289 return baseConfig.getRawString(section, subsection, name);
290 else
291 return null;
294 private Object getRawEntry(final String section, final String subsection,
295 final String name) {
296 if (!readFile) {
297 try {
298 load();
299 } catch (FileNotFoundException err) {
300 // Oh well. No sense in complaining about it.
302 } catch (IOException err) {
303 err.printStackTrace();
307 String ss;
308 if (subsection != null)
309 ss = "."+subsection.toLowerCase();
310 else
311 ss = "";
312 final Object o;
313 o = byName.get(section.toLowerCase() + ss + "." + name.toLowerCase());
314 return o;
318 * Add or modify a configuration value. The parameters will result in a
319 * configuration entry like this.
321 * <pre>
322 * [section &quot;subsection&quot;]
323 * name = value
324 * </pre>
326 * @param section
327 * section name, e.g "branch"
328 * @param subsection
329 * optional subsection value, e.g. a branch name
330 * @param name
331 * parameter name, e.g. "filemode"
332 * @param value
333 * parameter value, e.g. "true"
335 public void setString(final String section, final String subsection,
336 final String name, final String value) {
337 setStringList(section, subsection, name, Collections
338 .singletonList(value));
342 * Remove a configuration value.
344 * @param section
345 * section name, e.g "branch"
346 * @param subsection
347 * optional subsection value, e.g. a branch name
348 * @param name
349 * parameter name, e.g. "filemode"
351 public void unsetString(final String section, final String subsection,
352 final String name) {
353 setStringList(section, subsection, name, Collections
354 .<String> emptyList());
358 * Set a configuration value.
360 * <pre>
361 * [section &quot;subsection&quot;]
362 * name = value
363 * </pre>
365 * @param section
366 * section name, e.g "branch"
367 * @param subsection
368 * optional subsection value, e.g. a branch name
369 * @param name
370 * parameter name, e.g. "filemode"
371 * @param values
372 * list of zero or more values for this key.
374 public void setStringList(final String section, final String subsection,
375 final String name, final List<String> values) {
376 // Update our parsed cache of values for future reference.
378 String key = section.toLowerCase();
379 if (subsection != null)
380 key += "." + subsection.toLowerCase();
381 key += "." + name.toLowerCase();
382 if (values.size() == 0)
383 byName.remove(key);
384 else if (values.size() == 1) {
385 final Entry e = new Entry();
386 e.base = section;
387 e.extendedBase = subsection;
388 e.name = name;
389 e.value = values.get(0);
390 byName.put(key, e);
391 } else {
392 final ArrayList<Entry> eList = new ArrayList<Entry>(values.size());
393 for (final String v : values) {
394 final Entry e = new Entry();
395 e.base = section;
396 e.extendedBase = subsection;
397 e.name = name;
398 e.value = v;
399 eList.add(e);
401 byName.put(key, eList);
404 int entryIndex = 0;
405 int valueIndex = 0;
406 int insertPosition = -1;
408 // Reset the first n Entry objects that match this input name.
410 while (entryIndex < entries.size() && valueIndex < values.size()) {
411 final Entry e = entries.get(entryIndex++);
412 if (e.match(section, subsection, name)) {
413 e.value = values.get(valueIndex++);
414 insertPosition = entryIndex;
418 // Remove any extra Entry objects that we no longer need.
420 if (valueIndex == values.size() && entryIndex < entries.size()) {
421 while (entryIndex < entries.size()) {
422 final Entry e = entries.get(entryIndex++);
423 if (e.match(section, subsection, name))
424 entries.remove(--entryIndex);
428 // Insert new Entry objects for additional/new values.
430 if (valueIndex < values.size() && entryIndex == entries.size()){
431 if (insertPosition < 0) {
432 // We didn't find a matching key above, but maybe there
433 // is already a section available that matches. Insert
434 // after the last key of that section.
436 insertPosition = findSectionEnd(section, subsection);
438 if (insertPosition < 0) {
439 // We didn't find any matching section header for this key,
440 // so we must create a new section header at the end.
442 final Entry e = new Entry();
443 e.prefix = null;
444 e.suffix = null;
445 e.base = section;
446 e.extendedBase = subsection;
447 entries.add(e);
448 insertPosition = entries.size();
450 while (valueIndex < values.size()) {
451 final Entry e = new Entry();
452 e.prefix = null;
453 e.suffix = null;
454 e.base = section;
455 e.extendedBase = subsection;
456 e.name = name;
457 e.value = values.get(valueIndex++);
458 entries.add(insertPosition++, e);
463 private int findSectionEnd(final String section, final String subsection) {
464 for (int i = 0; i < entries.size(); i++) {
465 Entry e = entries.get(i);
466 if (e.match(section, subsection, null)) {
467 i++;
468 while (i < entries.size()) {
469 e = entries.get(i);
470 if (e.match(section, subsection, e.name))
471 i++;
472 else
473 break;
475 return i;
478 return -1;
482 * Create a new default config
484 public void create() {
485 Entry e;
487 clear();
488 readFile = true;
490 e = new Entry();
491 e.base = "core";
492 add(e);
494 e = new Entry();
495 e.base = "core";
496 e.name = "repositoryformatversion";
497 e.value = "0";
498 add(e);
500 e = new Entry();
501 e.base = "core";
502 e.name = "filemode";
503 e.value = "true";
504 add(e);
506 core = new CoreConfig(this);
510 * Save config data to the git config file
512 * @throws IOException
514 public void save() throws IOException {
515 final File tmp = new File(configFile.getParentFile(), configFile
516 .getName()
517 + ".lock");
518 final PrintWriter r = new PrintWriter(new BufferedWriter(
519 new OutputStreamWriter(new FileOutputStream(tmp),
520 Constants.CHARSET))) {
521 @Override
522 public void println() {
523 print('\n');
526 boolean ok = false;
527 try {
528 final Iterator<Entry> i = entries.iterator();
529 while (i.hasNext()) {
530 final Entry e = i.next();
531 if (e.prefix != null) {
532 r.print(e.prefix);
534 if (e.base != null && e.name == null) {
535 r.print('[');
536 r.print(e.base);
537 if (e.extendedBase != null) {
538 r.print(' ');
539 r.print('"');
540 r.print(escapeValue(e.extendedBase));
541 r.print('"');
543 r.print(']');
544 } else if (e.base != null && e.name != null) {
545 if (e.prefix == null || "".equals(e.prefix)) {
546 r.print('\t');
548 r.print(e.name);
549 if (e.value != null) {
550 if (!MAGIC_EMPTY_VALUE.equals(e.value)) {
551 r.print(" = ");
552 r.print(escapeValue(e.value));
555 if (e.suffix != null) {
556 r.print(' ');
559 if (e.suffix != null) {
560 r.print(e.suffix);
562 r.println();
564 ok = true;
565 r.close();
566 if (!tmp.renameTo(configFile)) {
567 configFile.delete();
568 if (!tmp.renameTo(configFile))
569 throw new IOException("Cannot save config file " + configFile + ", rename failed");
571 } finally {
572 r.close();
573 if (tmp.exists() && !tmp.delete()) {
574 System.err.println("(warning) failed to delete tmp config file: " + tmp);
577 readFile = ok;
581 * Read the config file
582 * @throws IOException
584 public void load() throws IOException {
585 clear();
586 readFile = true;
587 final BufferedReader r = new BufferedReader(new InputStreamReader(
588 new FileInputStream(configFile), Constants.CHARSET));
589 try {
590 Entry last = null;
591 Entry e = new Entry();
592 for (;;) {
593 r.mark(1);
594 int input = r.read();
595 final char in = (char) input;
596 if (-1 == input) {
597 break;
598 } else if ('\n' == in) {
599 // End of this entry.
600 add(e);
601 if (e.base != null) {
602 last = e;
604 e = new Entry();
605 } else if (e.suffix != null) {
606 // Everything up until the end-of-line is in the suffix.
607 e.suffix += in;
608 } else if (';' == in || '#' == in) {
609 // The rest of this line is a comment; put into suffix.
610 e.suffix = String.valueOf(in);
611 } else if (e.base == null && Character.isWhitespace(in)) {
612 // Save the leading whitespace (if any).
613 if (e.prefix == null) {
614 e.prefix = "";
616 e.prefix += in;
617 } else if ('[' == in) {
618 // This is a group header line.
619 e.base = readBase(r);
620 input = r.read();
621 if ('"' == input) {
622 e.extendedBase = readValue(r, true, '"');
623 input = r.read();
625 if (']' != input) {
626 throw new IOException("Bad group header.");
628 e.suffix = "";
629 } else if (last != null) {
630 // Read a value.
631 e.base = last.base;
632 e.extendedBase = last.extendedBase;
633 r.reset();
634 e.name = readName(r);
635 if (e.name.endsWith("\n")) {
636 e.name = e.name.substring(0, e.name.length()-1);
637 e.value = MAGIC_EMPTY_VALUE;
638 } else
639 e.value = readValue(r, false, -1);
640 } else {
641 throw new IOException("Invalid line in config file.");
644 } finally {
645 r.close();
648 core = new CoreConfig(this);
651 private void clear() {
652 entries = new ArrayList<Entry>();
653 byName = new HashMap<String, Object>();
656 @SuppressWarnings("unchecked")
657 private void add(final Entry e) {
658 entries.add(e);
659 if (e.base != null) {
660 final String b = e.base.toLowerCase();
661 final String group;
662 if (e.extendedBase != null) {
663 group = b + "." + e.extendedBase;
664 } else {
665 group = b;
667 if (e.name != null) {
668 final String n = e.name.toLowerCase();
669 final String key = group + "." + n;
670 final Object o = byName.get(key);
671 if (o == null) {
672 byName.put(key, e);
673 } else if (o instanceof Entry) {
674 final ArrayList<Object> l = new ArrayList<Object>();
675 l.add(o);
676 l.add(e);
677 byName.put(key, l);
678 } else if (o instanceof List) {
679 ((List<Entry>) o).add(e);
685 private static String escapeValue(final String x) {
686 boolean inquote = false;
687 int lineStart = 0;
688 final StringBuffer r = new StringBuffer(x.length());
689 for (int k = 0; k < x.length(); k++) {
690 final char c = x.charAt(k);
691 switch (c) {
692 case '\n':
693 if (inquote) {
694 r.append('"');
695 inquote = false;
697 r.append("\\n\\\n");
698 lineStart = r.length();
699 break;
701 case '\t':
702 r.append("\\t");
703 break;
705 case '\b':
706 r.append("\\b");
707 break;
709 case '\\':
710 r.append("\\\\");
711 break;
713 case '"':
714 r.append("\\\"");
715 break;
717 case ';':
718 case '#':
719 if (!inquote) {
720 r.insert(lineStart, '"');
721 inquote = true;
723 r.append(c);
724 break;
726 case ' ':
727 if (!inquote && r.length() > 0
728 && r.charAt(r.length() - 1) == ' ') {
729 r.insert(lineStart, '"');
730 inquote = true;
732 r.append(' ');
733 break;
735 default:
736 r.append(c);
737 break;
740 if (inquote) {
741 r.append('"');
743 return r.toString();
746 private static String readBase(final BufferedReader r) throws IOException {
747 final StringBuffer base = new StringBuffer();
748 for (;;) {
749 r.mark(1);
750 int c = r.read();
751 if (c < 0) {
752 throw new IOException("Unexpected end of config file.");
753 } else if (']' == c) {
754 r.reset();
755 break;
756 } else if (' ' == c || '\t' == c) {
757 for (;;) {
758 r.mark(1);
759 c = r.read();
760 if (c < 0) {
761 throw new IOException("Unexpected end of config file.");
762 } else if ('"' == c) {
763 r.reset();
764 break;
765 } else if (' ' == c || '\t' == c) {
766 // Skipped...
767 } else {
768 throw new IOException("Bad base entry. : " + base + "," + c);
771 break;
772 } else if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c) {
773 base.append((char) c);
774 } else {
775 throw new IOException("Bad base entry. : " + base + ", " + c);
778 return base.toString();
781 private static String readName(final BufferedReader r) throws IOException {
782 final StringBuffer name = new StringBuffer();
783 for (;;) {
784 r.mark(1);
785 int c = r.read();
786 if (c < 0) {
787 throw new IOException("Unexpected end of config file.");
788 } else if ('=' == c) {
789 break;
790 } else if (' ' == c || '\t' == c) {
791 for (;;) {
792 r.mark(1);
793 c = r.read();
794 if (c < 0) {
795 throw new IOException("Unexpected end of config file.");
796 } else if ('=' == c) {
797 break;
798 } else if (';' == c || '#' == c || '\n' == c) {
799 r.reset();
800 break;
801 } else if (' ' == c || '\t' == c) {
802 // Skipped...
803 } else {
804 throw new IOException("Bad entry delimiter.");
807 break;
808 } else if (Character.isLetterOrDigit((char) c) || c == '-') {
809 // From the git-config man page:
810 // The variable names are case-insensitive and only
811 // alphanumeric characters and - are allowed.
812 name.append((char) c);
813 } else if ('\n' == c) {
814 r.reset();
815 name.append((char) c);
816 break;
817 } else {
818 throw new IOException("Bad config entry name: " + name + (char) c);
821 return name.toString();
824 private static String readValue(final BufferedReader r, boolean quote,
825 final int eol) throws IOException {
826 final StringBuffer value = new StringBuffer();
827 boolean space = false;
828 for (;;) {
829 r.mark(1);
830 int c = r.read();
831 if (c < 0) {
832 if (value.length() == 0)
833 throw new IOException("Unexpected end of config file.");
834 break;
836 if ('\n' == c) {
837 if (quote) {
838 throw new IOException("Newline in quotes not allowed.");
840 r.reset();
841 break;
843 if (eol == c) {
844 break;
846 if (!quote) {
847 if (Character.isWhitespace((char) c)) {
848 space = true;
849 continue;
851 if (';' == c || '#' == c) {
852 r.reset();
853 break;
856 if (space) {
857 if (value.length() > 0) {
858 value.append(' ');
860 space = false;
862 if ('\\' == c) {
863 c = r.read();
864 switch (c) {
865 case -1:
866 throw new IOException("End of file in escape.");
867 case '\n':
868 continue;
869 case 't':
870 value.append('\t');
871 continue;
872 case 'b':
873 value.append('\b');
874 continue;
875 case 'n':
876 value.append('\n');
877 continue;
878 case '\\':
879 value.append('\\');
880 continue;
881 case '"':
882 value.append('"');
883 continue;
884 default:
885 throw new IOException("Bad escape: " + ((char) c));
888 if ('"' == c) {
889 quote = !quote;
890 continue;
892 value.append((char) c);
894 return value.length() > 0 ? value.toString() : null;
897 public String toString() {
898 return "RepositoryConfig[" + configFile.getPath() + "]";
901 static class Entry {
902 String prefix;
904 String base;
906 String extendedBase;
908 String name;
910 String value;
912 String suffix;
914 boolean match(final String aBase, final String aExtendedBase,
915 final String aName) {
916 return eq(base, aBase) && eq(extendedBase, aExtendedBase)
917 && eq(name, aName);
920 private static boolean eq(final String a, final String b) {
921 if (a == b)
922 return true;
923 if (a == null || b == null)
924 return false;
925 return a.equals(b);