Format .git/config using LF line endings
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RepositoryConfig.java
blob999b031b9c2f4cc6a90700eb4ec4c62f903954a9
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 @Override
353 public void println() {
354 print('\n');
357 boolean ok = false;
358 try {
359 final Iterator<Entry> i = entries.iterator();
360 while (i.hasNext()) {
361 final Entry e = i.next();
362 if (e.prefix != null) {
363 r.print(e.prefix);
365 if (e.base != null && e.name == null) {
366 r.print('[');
367 r.print(e.base);
368 if (e.extendedBase != null) {
369 r.print(' ');
370 r.print('"');
371 r.print(escapeValue(e.extendedBase));
372 r.print('"');
374 r.print(']');
375 } else if (e.base != null && e.name != null) {
376 if (e.prefix == null || "".equals(e.prefix)) {
377 r.print('\t');
379 r.print(e.name);
380 if (e.value != null) {
381 if (!MAGIC_EMPTY_VALUE.equals(e.value)) {
382 r.print(" = ");
383 r.print(escapeValue(e.value));
386 if (e.suffix != null) {
387 r.print(' ');
390 if (e.suffix != null) {
391 r.print(e.suffix);
393 r.println();
395 ok = true;
396 r.close();
397 if (!tmp.renameTo(configFile)) {
398 configFile.delete();
399 if (!tmp.renameTo(configFile))
400 throw new IOException("Cannot save config file " + configFile + ", rename failed");
402 } finally {
403 r.close();
404 if (tmp.exists() && !tmp.delete()) {
405 System.err.println("(warning) failed to delete tmp config file: " + tmp);
408 readFile = ok;
412 * Read the config file
413 * @throws IOException
415 public void load() throws IOException {
416 clear();
417 readFile = true;
418 final BufferedReader r = new BufferedReader(new InputStreamReader(
419 new FileInputStream(configFile), Constants.CHARACTER_ENCODING));
420 try {
421 Entry last = null;
422 Entry e = new Entry();
423 for (;;) {
424 r.mark(1);
425 int input = r.read();
426 final char in = (char) input;
427 if (-1 == input) {
428 break;
429 } else if ('\n' == in) {
430 // End of this entry.
431 add(e);
432 if (e.base != null) {
433 last = e;
435 e = new Entry();
436 } else if (e.suffix != null) {
437 // Everything up until the end-of-line is in the suffix.
438 e.suffix += in;
439 } else if (';' == in || '#' == in) {
440 // The rest of this line is a comment; put into suffix.
441 e.suffix = String.valueOf(in);
442 } else if (e.base == null && Character.isWhitespace(in)) {
443 // Save the leading whitespace (if any).
444 if (e.prefix == null) {
445 e.prefix = "";
447 e.prefix += in;
448 } else if ('[' == in) {
449 // This is a group header line.
450 e.base = readBase(r);
451 input = r.read();
452 if ('"' == input) {
453 e.extendedBase = readValue(r, true, '"');
454 input = r.read();
456 if (']' != input) {
457 throw new IOException("Bad group header.");
459 e.suffix = "";
460 } else if (last != null) {
461 // Read a value.
462 e.base = last.base;
463 e.extendedBase = last.extendedBase;
464 r.reset();
465 e.name = readName(r);
466 if (e.name.endsWith("\n")) {
467 e.name = e.name.substring(0, e.name.length()-1);
468 e.value = MAGIC_EMPTY_VALUE;
469 } else
470 e.value = readValue(r, false, -1);
471 } else {
472 throw new IOException("Invalid line in config file.");
475 } finally {
476 r.close();
479 core = new CoreConfig(this);
482 private void clear() {
483 entries = new ArrayList<Entry>();
484 byName = new HashMap<String, Object>();
485 lastInEntry = new HashMap<String, Entry>();
486 lastInGroup = new HashMap<String, Entry>();
489 @SuppressWarnings("unchecked")
490 private void add(final Entry e) {
491 entries.add(e);
492 if (e.base != null) {
493 final String b = e.base.toLowerCase();
494 final String group;
495 if (e.extendedBase != null) {
496 group = b + "." + e.extendedBase;
497 } else {
498 group = b;
500 if (e.name != null) {
501 final String n = e.name.toLowerCase();
502 final String key = group + "." + n;
503 final Object o = byName.get(key);
504 if (o == null) {
505 byName.put(key, e);
506 } else if (o instanceof Entry) {
507 final ArrayList<Object> l = new ArrayList<Object>();
508 l.add(o);
509 l.add(e);
510 byName.put(key, l);
511 } else if (o instanceof List) {
512 ((List<Entry>) o).add(e);
514 lastInEntry.put(key, e);
516 lastInGroup.put(group, e);
520 private static String escapeValue(final String x) {
521 boolean inquote = false;
522 int lineStart = 0;
523 final StringBuffer r = new StringBuffer(x.length());
524 for (int k = 0; k < x.length(); k++) {
525 final char c = x.charAt(k);
526 switch (c) {
527 case '\n':
528 if (inquote) {
529 r.append('"');
530 inquote = false;
532 r.append("\\n\\\n");
533 lineStart = r.length();
534 break;
536 case '\t':
537 r.append("\\t");
538 break;
540 case '\b':
541 r.append("\\b");
542 break;
544 case '\\':
545 r.append("\\\\");
546 break;
548 case '"':
549 r.append("\\\"");
550 break;
552 case ';':
553 case '#':
554 if (!inquote) {
555 r.insert(lineStart, '"');
556 inquote = true;
558 r.append(c);
559 break;
561 case ' ':
562 if (!inquote && r.length() > 0
563 && r.charAt(r.length() - 1) == ' ') {
564 r.insert(lineStart, '"');
565 inquote = true;
567 r.append(' ');
568 break;
570 default:
571 r.append(c);
572 break;
575 if (inquote) {
576 r.append('"');
578 return r.toString();
581 private static String readBase(final BufferedReader r) throws IOException {
582 final StringBuffer base = new StringBuffer();
583 for (;;) {
584 r.mark(1);
585 int c = r.read();
586 if (c < 0) {
587 throw new IOException("Unexpected end of config file.");
588 } else if (']' == c) {
589 r.reset();
590 break;
591 } else if (' ' == c || '\t' == c) {
592 for (;;) {
593 r.mark(1);
594 c = r.read();
595 if (c < 0) {
596 throw new IOException("Unexpected end of config file.");
597 } else if ('"' == c) {
598 r.reset();
599 break;
600 } else if (' ' == c || '\t' == c) {
601 // Skipped...
602 } else {
603 throw new IOException("Bad base entry. : " + base + "," + c);
606 break;
607 } else if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c) {
608 base.append((char) c);
609 } else {
610 throw new IOException("Bad base entry. : " + base + ", " + c);
613 return base.toString();
616 private static String readName(final BufferedReader r) throws IOException {
617 final StringBuffer name = new StringBuffer();
618 for (;;) {
619 r.mark(1);
620 int c = r.read();
621 if (c < 0) {
622 throw new IOException("Unexpected end of config file.");
623 } else if ('=' == c) {
624 break;
625 } else if (' ' == c || '\t' == c) {
626 for (;;) {
627 r.mark(1);
628 c = r.read();
629 if (c < 0) {
630 throw new IOException("Unexpected end of config file.");
631 } else if ('=' == c) {
632 break;
633 } else if (';' == c || '#' == c || '\n' == c) {
634 r.reset();
635 break;
636 } else if (' ' == c || '\t' == c) {
637 // Skipped...
638 } else {
639 throw new IOException("Bad entry delimiter.");
642 break;
643 } else if (Character.isLetterOrDigit((char) c) || c == '-') {
644 // From the git-config man page:
645 // The variable names are case-insensitive and only
646 // alphanumeric characters and - are allowed.
647 name.append((char) c);
648 } else if ('\n' == c) {
649 r.reset();
650 name.append((char) c);
651 break;
652 } else {
653 throw new IOException("Bad config entry name: " + name + (char) c);
656 return name.toString();
659 private static String readValue(final BufferedReader r, boolean quote,
660 final int eol) throws IOException {
661 final StringBuffer value = new StringBuffer();
662 boolean space = false;
663 for (;;) {
664 r.mark(1);
665 int c = r.read();
666 if (c < 0) {
667 throw new IOException("Unexpected end of config file.");
669 if ('\n' == c) {
670 if (quote) {
671 throw new IOException("Newline in quotes not allowed.");
673 r.reset();
674 break;
676 if (eol == c) {
677 break;
679 if (!quote) {
680 if (Character.isWhitespace((char) c)) {
681 space = true;
682 continue;
684 if (';' == c || '#' == c) {
685 r.reset();
686 break;
689 if (space) {
690 if (value.length() > 0) {
691 value.append(' ');
693 space = false;
695 if ('\\' == c) {
696 c = r.read();
697 switch (c) {
698 case -1:
699 throw new IOException("End of file in escape.");
700 case '\n':
701 continue;
702 case 't':
703 value.append('\t');
704 continue;
705 case 'b':
706 value.append('\b');
707 continue;
708 case 'n':
709 value.append('\n');
710 continue;
711 case '\\':
712 value.append('\\');
713 continue;
714 case '"':
715 value.append('"');
716 continue;
717 default:
718 throw new IOException("Bad escape: " + ((char) c));
721 if ('"' == c) {
722 quote = !quote;
723 continue;
725 value.append((char) c);
727 return value.length() > 0 ? value.toString() : null;
730 public String toString() {
731 return "RepositoryConfig[" + configFile.getPath() + "]";
734 static class Entry {
735 String prefix;
737 String base;
739 String extendedBase;
741 String name;
743 String value;
745 String suffix;