Merge branch 'master' into rr/fetchmergewithshawn
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RepositoryConfig.java
blobd9aa3a7e40defc5de59fd4e7a405e328b48a50a5
1 /*
2 * Copyright (C) 2006 Shawn Pearce <spearce@spearce.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public
6 * License, version 2, as published by the Free Software Foundation.
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
13 * You should have received a copy of the GNU General Public
14 * License along with this library; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
17 package org.spearce.jgit.lib;
19 import java.io.BufferedReader;
20 import java.io.BufferedWriter;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileNotFoundException;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStreamReader;
27 import java.io.OutputStreamWriter;
28 import java.io.PrintWriter;
29 import java.util.ArrayList;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
35 import org.spearce.jgit.util.FS;
37 /**
38 * An object representing the Git config file.
40 * This can be either the repository specific file or the user global
41 * file depending on how it is instantiated.
43 public class RepositoryConfig {
44 /**
45 * Obtain a new configuration instance for ~/.gitconfig.
47 * @return a new configuration instance to read the user's global
48 * configuration file from their home directory.
50 public static RepositoryConfig openUserConfig() {
51 return new RepositoryConfig(null, new File(System
52 .getProperty("user.home"), ".gitconfig"));
55 private final RepositoryConfig baseConfig;
57 /** Section name for a remote configuration */
58 public static final String REMOTE_SECTION = "remote";
60 /** Section name for a branch configuration. */
61 public static final String BRANCH_SECTION = "branch";
63 private final File configFile;
65 private boolean readFile;
67 private CoreConfig core;
69 private List<Entry> entries;
71 private Map<String, Object> byName;
73 private Map<String, Entry> lastInEntry;
75 private Map<String, Entry> lastInGroup;
77 private static final String MAGIC_EMPTY_VALUE = "%%magic%%empty%%";
79 RepositoryConfig(final Repository repo) {
80 this(openUserConfig(), FS.resolve(repo.getDirectory(), "config"));
83 /**
84 * Create a Git configuration file reader/writer/cache for a specific file.
86 * @param base
87 * configuration that provides default values if this file does
88 * not set/override a particular key. Often this is the user's
89 * global configuration file, or the system level configuration.
90 * @param cfgLocation
91 * path of the file to load (or save).
93 public RepositoryConfig(final RepositoryConfig base, final File cfgLocation) {
94 baseConfig = base;
95 configFile = cfgLocation;
96 clear();
99 /**
100 * @return Core configuration values
102 public CoreConfig getCore() {
103 return core;
107 * Obtain an integer value from the configuration.
109 * @param section
110 * section the key is grouped within.
111 * @param name
112 * name of the key to get.
113 * @param defaultValue
114 * default value to return if no value was present.
115 * @return an integer value from the configuration, or defaultValue.
117 public int getInt(final String section, final String name,
118 final int defaultValue) {
119 return getInt(section, null, name, defaultValue);
123 * Obtain an integer value from the configuration.
125 * @param section
126 * section the key is grouped within.
127 * @param subsection
128 * subsection name, such a remote or branch name.
129 * @param name
130 * name of the key to get.
131 * @param defaultValue
132 * default value to return if no value was present.
133 * @return an integer value from the configuration, or defaultValue.
135 public int getInt(final String section, String subsection,
136 final String name, final int defaultValue) {
137 final String str = getString(section, subsection, name);
138 if (str == null)
139 return defaultValue;
141 String n = str.trim();
142 if (n.length() == 0)
143 return defaultValue;
145 int mul = 1;
146 switch (Character.toLowerCase(n.charAt(n.length() - 1))) {
147 case 'g':
148 mul = 1024 * 1024 * 1024;
149 break;
150 case 'm':
151 mul = 1024 * 1024;
152 break;
153 case 'k':
154 mul = 1024;
155 break;
157 if (mul > 1)
158 n = n.substring(0, n.length() - 1).trim();
159 if (n.length() == 0)
160 return defaultValue;
162 try {
163 return mul * Integer.parseInt(n);
164 } catch (NumberFormatException nfe) {
165 throw new IllegalArgumentException("Invalid integer value: "
166 + section + "." + name + "=" + str);
171 * Get a boolean value from the git config
173 * @param section
174 * section the key is grouped within.
175 * @param name
176 * name of the key to get.
177 * @param defaultValue
178 * default value to return if no value was present.
179 * @return true if any value or defaultValue is true, false for missing or
180 * explicit false
182 protected boolean getBoolean(final String section, final String name,
183 final boolean defaultValue) {
184 return getBoolean(section, null, name, defaultValue);
188 * Get a boolean value from the git config
190 * @param section
191 * section the key is grouped within.
192 * @param subsection
193 * subsection name, such a remote or branch name.
194 * @param name
195 * name of the key to get.
196 * @param defaultValue
197 * default value to return if no value was present.
198 * @return true if any value or defaultValue is true, false for missing or
199 * explicit false
201 protected boolean getBoolean(final String section, String subsection,
202 final String name, final boolean defaultValue) {
203 String n = getRawString(section, subsection, name);
204 if (n == null)
205 return defaultValue;
207 n = n.toLowerCase();
208 if (MAGIC_EMPTY_VALUE.equals(n) || "yes".equals(n) || "true".equals(n) || "1".equals(n)) {
209 return true;
210 } else if ("no".equals(n) || "false".equals(n) || "0".equals(n)) {
211 return false;
212 } else {
213 throw new IllegalArgumentException("Invalid boolean value: "
214 + section + "." + name + "=" + n);
219 * @param section
220 * @param subsection
221 * @param name
222 * @return a String value from git config.
224 public String getString(final String section, String subsection, final String name) {
225 String val = getRawString(section, subsection, name);
226 if (MAGIC_EMPTY_VALUE.equals(val)) {
227 return "";
229 return val;
232 private String getRawString(final String section, final String subsection,
233 final String name) {
234 if (!readFile) {
235 try {
236 load();
237 } catch (FileNotFoundException err) {
238 // Oh well. No sense in complaining about it.
240 } catch (IOException err) {
241 err.printStackTrace();
245 String ss;
246 if (subsection != null)
247 ss = "."+subsection.toLowerCase();
248 else
249 ss = "";
250 final Object o;
251 o = byName.get(section.toLowerCase() + ss + "." + name.toLowerCase());
252 if (o instanceof List) {
253 return ((Entry) ((List) o).get(0)).value;
254 } else if (o instanceof Entry) {
255 return ((Entry) o).value;
256 } else if (baseConfig != null)
257 return baseConfig.getRawString(section, subsection, name);
258 else
259 return null;
263 * Add or modify a configuration value. The parameters will result in a
264 * configuration entry like this.
266 * <pre>
267 * [section &quot;subsection&quot;]
268 * name = value
269 * </pre>
271 * @param section
272 * section name, e.g "branch"
273 * @param subsection
274 * optional subsection value, e.g. a branch name
275 * @param name
276 * parameter name, e.g. "filemode"
277 * @param value
278 * parameter value, e.g. "true"
280 public void putString(final String section, final String subsection,
281 final String name, final String value) {
282 Entry pe = null;
283 for (int i = 0; i < entries.size(); ++i) {
284 pe = entries.get(i);
285 if (pe.base.equals(section)
286 && (pe.extendedBase == null && subsection == null || pe.extendedBase != null
287 && pe.extendedBase.equals(subsection)))
288 break;
289 pe = null;
292 // TODO: This doesn't work in general, so we must revise this code
293 if (pe == null) {
294 pe = new Entry();
295 pe.prefix = null;
296 pe.suffix = null;
297 pe.base = section;
298 pe.extendedBase = subsection;
299 add(pe);
302 Entry e = new Entry();
303 e.prefix = null;
304 e.suffix = null;
305 e.base = section;
306 e.extendedBase = subsection;
307 e.name = name;
308 e.value = value;
309 add(e);
313 * Create a new default config
315 public void create() {
316 Entry e;
318 clear();
319 readFile = true;
321 e = new Entry();
322 e.base = "core";
323 add(e);
325 e = new Entry();
326 e.base = "core";
327 e.name = "repositoryformatversion";
328 e.value = "0";
329 add(e);
331 e = new Entry();
332 e.base = "core";
333 e.name = "filemode";
334 e.value = "true";
335 add(e);
337 core = new CoreConfig(this);
341 * Save config data to the git config file
343 * @throws IOException
345 public void save() throws IOException {
346 final File tmp = new File(configFile.getParentFile(), configFile
347 .getName()
348 + ".lock");
349 final PrintWriter r = new PrintWriter(new BufferedWriter(
350 new OutputStreamWriter(new FileOutputStream(tmp),
351 Constants.CHARACTER_ENCODING)));
352 boolean ok = false;
353 try {
354 final Iterator<Entry> i = entries.iterator();
355 while (i.hasNext()) {
356 final Entry e = i.next();
357 if (e.prefix != null) {
358 r.print(e.prefix);
360 if (e.base != null && e.name == null) {
361 r.print('[');
362 r.print(e.base);
363 if (e.extendedBase != null) {
364 r.print(' ');
365 r.print('"');
366 r.print(escapeValue(e.extendedBase));
367 r.print('"');
369 r.print(']');
370 } else if (e.base != null && e.name != null) {
371 if (e.prefix == null || "".equals(e.prefix)) {
372 r.print('\t');
374 r.print(e.name);
375 if (e.value != null) {
376 if (!MAGIC_EMPTY_VALUE.equals(e.value)) {
377 r.print(" = ");
378 r.print(escapeValue(e.value));
381 if (e.suffix != null) {
382 r.print(' ');
385 if (e.suffix != null) {
386 r.print(e.suffix);
388 r.println();
390 ok = true;
391 } finally {
392 r.close();
393 if (!ok || !tmp.renameTo(configFile)) {
394 tmp.delete();
397 readFile = ok;
401 * Read the config file
402 * @throws IOException
404 public void load() throws IOException {
405 clear();
406 readFile = true;
407 final BufferedReader r = new BufferedReader(new InputStreamReader(
408 new FileInputStream(configFile), Constants.CHARACTER_ENCODING));
409 try {
410 Entry last = null;
411 Entry e = new Entry();
412 for (;;) {
413 r.mark(1);
414 int input = r.read();
415 final char in = (char) input;
416 if (-1 == input) {
417 break;
418 } else if ('\n' == in) {
419 // End of this entry.
420 add(e);
421 if (e.base != null) {
422 last = e;
424 e = new Entry();
425 } else if (e.suffix != null) {
426 // Everything up until the end-of-line is in the suffix.
427 e.suffix += in;
428 } else if (';' == in || '#' == in) {
429 // The rest of this line is a comment; put into suffix.
430 e.suffix = String.valueOf(in);
431 } else if (e.base == null && Character.isWhitespace(in)) {
432 // Save the leading whitespace (if any).
433 if (e.prefix == null) {
434 e.prefix = "";
436 e.prefix += in;
437 } else if ('[' == in) {
438 // This is a group header line.
439 e.base = readBase(r);
440 input = r.read();
441 if ('"' == input) {
442 e.extendedBase = readValue(r, true, '"');
443 input = r.read();
445 if (']' != input) {
446 throw new IOException("Bad group header.");
448 e.suffix = "";
449 } else if (last != null) {
450 // Read a value.
451 e.base = last.base;
452 e.extendedBase = last.extendedBase;
453 r.reset();
454 e.name = readName(r);
455 if (e.name.endsWith("\n")) {
456 e.name = e.name.substring(0, e.name.length()-1);
457 e.value = MAGIC_EMPTY_VALUE;
458 } else
459 e.value = readValue(r, false, -1);
460 } else {
461 throw new IOException("Invalid line in config file.");
464 } finally {
465 r.close();
468 core = new CoreConfig(this);
471 private void clear() {
472 entries = new ArrayList<Entry>();
473 byName = new HashMap<String, Object>();
474 lastInEntry = new HashMap<String, Entry>();
475 lastInGroup = new HashMap<String, Entry>();
478 @SuppressWarnings("unchecked")
479 private void add(final Entry e) {
480 entries.add(e);
481 if (e.base != null) {
482 final String b = e.base.toLowerCase();
483 final String group;
484 if (e.extendedBase != null) {
485 group = b + "." + e.extendedBase;
486 } else {
487 group = b;
489 if (e.name != null) {
490 final String n = e.name.toLowerCase();
491 final String key = group + "." + n;
492 final Object o = byName.get(key);
493 if (o == null) {
494 byName.put(key, e);
495 } else if (o instanceof Entry) {
496 final ArrayList<Object> l = new ArrayList<Object>();
497 l.add(o);
498 l.add(e);
499 byName.put(key, l);
500 } else if (o instanceof List) {
501 ((List<Entry>) o).add(e);
503 lastInEntry.put(key, e);
505 lastInGroup.put(group, e);
509 private static String escapeValue(final String x) {
510 boolean inquote = false;
511 int lineStart = 0;
512 final StringBuffer r = new StringBuffer(x.length());
513 for (int k = 0; k < x.length(); k++) {
514 final char c = x.charAt(k);
515 switch (c) {
516 case '\n':
517 if (inquote) {
518 r.append('"');
519 inquote = false;
521 r.append("\\n\\\n");
522 lineStart = r.length();
523 break;
525 case '\t':
526 r.append("\\t");
527 break;
529 case '\b':
530 r.append("\\b");
531 break;
533 case '\\':
534 r.append("\\\\");
535 break;
537 case '"':
538 r.append("\\\"");
539 break;
541 case ';':
542 case '#':
543 if (!inquote) {
544 r.insert(lineStart, '"');
545 inquote = true;
547 r.append(c);
548 break;
550 case ' ':
551 if (!inquote && r.length() > 0
552 && r.charAt(r.length() - 1) == ' ') {
553 r.insert(lineStart, '"');
554 inquote = true;
556 r.append(' ');
557 break;
559 default:
560 r.append(c);
561 break;
564 if (inquote) {
565 r.append('"');
567 return r.toString();
570 private static String readBase(final BufferedReader r) throws IOException {
571 final StringBuffer base = new StringBuffer();
572 for (;;) {
573 r.mark(1);
574 int c = r.read();
575 if (c < 0) {
576 throw new IOException("Unexpected end of config file.");
577 } else if (']' == c) {
578 r.reset();
579 break;
580 } else if (' ' == c || '\t' == c) {
581 for (;;) {
582 r.mark(1);
583 c = r.read();
584 if (c < 0) {
585 throw new IOException("Unexpected end of config file.");
586 } else if ('"' == c) {
587 r.reset();
588 break;
589 } else if (' ' == c || '\t' == c) {
590 // Skipped...
591 } else {
592 throw new IOException("Bad base entry. : " + base + "," + c);
595 break;
596 } else if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c) {
597 base.append((char) c);
598 } else {
599 throw new IOException("Bad base entry. : " + base + ", " + c);
602 return base.toString();
605 private static String readName(final BufferedReader r) throws IOException {
606 final StringBuffer name = new StringBuffer();
607 for (;;) {
608 r.mark(1);
609 int c = r.read();
610 if (c < 0) {
611 throw new IOException("Unexpected end of config file.");
612 } else if ('=' == c) {
613 break;
614 } else if (' ' == c || '\t' == c) {
615 for (;;) {
616 r.mark(1);
617 c = r.read();
618 if (c < 0) {
619 throw new IOException("Unexpected end of config file.");
620 } else if ('=' == c) {
621 break;
622 } else if (';' == c || '#' == c || '\n' == c) {
623 r.reset();
624 break;
625 } else if (' ' == c || '\t' == c) {
626 // Skipped...
627 } else {
628 throw new IOException("Bad entry delimiter.");
631 break;
632 } else if (Character.isLetterOrDigit((char) c) || c == '-') {
633 // From the git-config man page:
634 // The variable names are case-insensitive and only
635 // alphanumeric characters and - are allowed.
636 name.append((char) c);
637 } else if ('\n' == c) {
638 r.reset();
639 name.append((char) c);
640 break;
641 } else {
642 throw new IOException("Bad config entry name: " + name + (char) c);
645 return name.toString();
648 private static String readValue(final BufferedReader r, boolean quote,
649 final int eol) throws IOException {
650 final StringBuffer value = new StringBuffer();
651 boolean space = false;
652 for (;;) {
653 r.mark(1);
654 int c = r.read();
655 if (c < 0) {
656 throw new IOException("Unexpected end of config file.");
658 if ('\n' == c) {
659 if (quote) {
660 throw new IOException("Newline in quotes not allowed.");
662 r.reset();
663 break;
665 if (eol == c) {
666 break;
668 if (!quote) {
669 if (Character.isWhitespace((char) c)) {
670 space = true;
671 continue;
673 if (';' == c || '#' == c) {
674 r.reset();
675 break;
678 if (space) {
679 if (value.length() > 0) {
680 value.append(' ');
682 space = false;
684 if ('\\' == c) {
685 c = r.read();
686 switch (c) {
687 case -1:
688 throw new IOException("End of file in escape.");
689 case '\n':
690 continue;
691 case 't':
692 value.append('\t');
693 continue;
694 case 'b':
695 value.append('\b');
696 continue;
697 case 'n':
698 value.append('\n');
699 continue;
700 case '\\':
701 value.append('\\');
702 continue;
703 case '"':
704 value.append('"');
705 continue;
706 default:
707 throw new IOException("Bad escape: " + ((char) c));
710 if ('"' == c) {
711 quote = !quote;
712 continue;
714 value.append((char) c);
716 return value.length() > 0 ? value.toString() : null;
719 public String toString() {
720 return "RepositoryConfig[" + configFile.getPath() + "]";
723 static class Entry {
724 String prefix;
726 String base;
728 String extendedBase;
730 String name;
732 String value;
734 String suffix;