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>
9 * Redistribution and use in source and binary forms, with or
10 * without modification, are permitted provided that the following
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
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
;
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
;
60 import org
.spearce
.jgit
.util
.FS
;
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
{
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.
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.
111 * path of the file to load (or save).
113 public RepositoryConfig(final RepositoryConfig base
, final File cfgLocation
) {
115 configFile
= cfgLocation
;
120 * @return Core configuration values
122 public CoreConfig
getCore() {
127 * Obtain an integer value from the configuration.
130 * section the key is grouped within.
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.
146 * section the key is grouped within.
148 * subsection name, such a remote or branch 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
);
161 String n
= str
.trim();
166 switch (Character
.toLowerCase(n
.charAt(n
.length() - 1))) {
168 mul
= 1024 * 1024 * 1024;
178 n
= n
.substring(0, n
.length() - 1).trim();
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
194 * section the key is grouped within.
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
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
211 * section the key is grouped within.
213 * subsection name, such a remote or branch 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
221 protected boolean getBoolean(final String section
, String subsection
,
222 final String name
, final boolean defaultValue
) {
223 String n
= getRawString(section
, subsection
, name
);
228 if (MAGIC_EMPTY_VALUE
.equals(n
) || "yes".equals(n
) || "true".equals(n
) || "1".equals(n
)) {
230 } else if ("no".equals(n
) || "false".equals(n
) || "0".equals(n
)) {
233 throw new IllegalArgumentException("Invalid boolean value: "
234 + section
+ "." + name
+ "=" + n
);
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
)) {
256 * @return array of zero or more values from the configuration.
258 public String
[] getStringList(final String section
, String subsection
,
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
;
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
,
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
);
294 private Object
getRawEntry(final String section
, final String subsection
,
299 } catch (FileNotFoundException err
) {
300 // Oh well. No sense in complaining about it.
302 } catch (IOException err
) {
303 err
.printStackTrace();
308 if (subsection
!= null)
309 ss
= "."+subsection
.toLowerCase();
313 o
= byName
.get(section
.toLowerCase() + ss
+ "." + name
.toLowerCase());
318 * Add or modify a configuration value. The parameters will result in a
319 * configuration entry like this.
322 * [section "subsection"]
327 * section name, e.g "branch"
329 * optional subsection value, e.g. a branch name
331 * parameter name, e.g. "filemode"
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.
345 * section name, e.g "branch"
347 * optional subsection value, e.g. a branch name
349 * parameter name, e.g. "filemode"
351 public void unsetString(final String section
, final String subsection
,
353 setStringList(section
, subsection
, name
, Collections
354 .<String
> emptyList());
358 * Set a configuration value.
361 * [section "subsection"]
366 * section name, e.g "branch"
368 * optional subsection value, e.g. a branch name
370 * parameter name, e.g. "filemode"
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)
384 else if (values
.size() == 1) {
385 final Entry e
= new Entry();
387 e
.extendedBase
= subsection
;
389 e
.value
= values
.get(0);
392 final ArrayList
<Entry
> eList
= new ArrayList
<Entry
>(values
.size());
393 for (final String v
: values
) {
394 final Entry e
= new Entry();
396 e
.extendedBase
= subsection
;
401 byName
.put(key
, eList
);
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();
446 e
.extendedBase
= subsection
;
448 insertPosition
= entries
.size();
450 while (valueIndex
< values
.size()) {
451 final Entry e
= new Entry();
455 e
.extendedBase
= subsection
;
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)) {
468 while (i
< entries
.size()) {
470 if (e
.match(section
, subsection
, e
.name
))
482 * Create a new default config
484 public void create() {
496 e
.name
= "repositoryformatversion";
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
518 final PrintWriter r
= new PrintWriter(new BufferedWriter(
519 new OutputStreamWriter(new FileOutputStream(tmp
),
520 Constants
.CHARSET
))) {
522 public void println() {
528 final Iterator
<Entry
> i
= entries
.iterator();
529 while (i
.hasNext()) {
530 final Entry e
= i
.next();
531 if (e
.prefix
!= null) {
534 if (e
.base
!= null && e
.name
== null) {
537 if (e
.extendedBase
!= null) {
540 r
.print(escapeValue(e
.extendedBase
));
544 } else if (e
.base
!= null && e
.name
!= null) {
545 if (e
.prefix
== null || "".equals(e
.prefix
)) {
549 if (e
.value
!= null) {
550 if (!MAGIC_EMPTY_VALUE
.equals(e
.value
)) {
552 r
.print(escapeValue(e
.value
));
555 if (e
.suffix
!= null) {
559 if (e
.suffix
!= null) {
566 if (!tmp
.renameTo(configFile
)) {
568 if (!tmp
.renameTo(configFile
))
569 throw new IOException("Cannot save config file " + configFile
+ ", rename failed");
573 if (tmp
.exists() && !tmp
.delete()) {
574 System
.err
.println("(warning) failed to delete tmp config file: " + tmp
);
581 * Read the config file
582 * @throws IOException
584 public void load() throws IOException
{
587 final BufferedReader r
= new BufferedReader(new InputStreamReader(
588 new FileInputStream(configFile
), Constants
.CHARSET
));
591 Entry e
= new Entry();
594 int input
= r
.read();
595 final char in
= (char) input
;
598 } else if ('\n' == in
) {
599 // End of this entry.
601 if (e
.base
!= null) {
605 } else if (e
.suffix
!= null) {
606 // Everything up until the end-of-line is in the suffix.
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) {
617 } else if ('[' == in
) {
618 // This is a group header line.
619 e
.base
= readBase(r
);
622 e
.extendedBase
= readValue(r
, true, '"');
626 throw new IOException("Bad group header.");
629 } else if (last
!= null) {
632 e
.extendedBase
= last
.extendedBase
;
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
;
639 e
.value
= readValue(r
, false, -1);
641 throw new IOException("Invalid line in config file.");
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
) {
659 if (e
.base
!= null) {
660 final String b
= e
.base
.toLowerCase();
662 if (e
.extendedBase
!= null) {
663 group
= b
+ "." + e
.extendedBase
;
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
);
673 } else if (o
instanceof Entry
) {
674 final ArrayList
<Object
> l
= new ArrayList
<Object
>();
678 } else if (o
instanceof List
) {
679 ((List
<Entry
>) o
).add(e
);
685 private static String
escapeValue(final String x
) {
686 boolean inquote
= false;
688 final StringBuffer r
= new StringBuffer(x
.length());
689 for (int k
= 0; k
< x
.length(); k
++) {
690 final char c
= x
.charAt(k
);
698 lineStart
= r
.length();
720 r
.insert(lineStart
, '"');
727 if (!inquote
&& r
.length() > 0
728 && r
.charAt(r
.length() - 1) == ' ') {
729 r
.insert(lineStart
, '"');
746 private static String
readBase(final BufferedReader r
) throws IOException
{
747 final StringBuffer base
= new StringBuffer();
752 throw new IOException("Unexpected end of config file.");
753 } else if (']' == c
) {
756 } else if (' ' == c
|| '\t' == c
) {
761 throw new IOException("Unexpected end of config file.");
762 } else if ('"' == c
) {
765 } else if (' ' == c
|| '\t' == c
) {
768 throw new IOException("Bad base entry. : " + base
+ "," + c
);
772 } else if (Character
.isLetterOrDigit((char) c
) || '.' == c
|| '-' == c
) {
773 base
.append((char) c
);
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();
787 throw new IOException("Unexpected end of config file.");
788 } else if ('=' == c
) {
790 } else if (' ' == c
|| '\t' == c
) {
795 throw new IOException("Unexpected end of config file.");
796 } else if ('=' == c
) {
798 } else if (';' == c
|| '#' == c
|| '\n' == c
) {
801 } else if (' ' == c
|| '\t' == c
) {
804 throw new IOException("Bad entry delimiter.");
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
) {
815 name
.append((char) c
);
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;
832 if (value
.length() == 0)
833 throw new IOException("Unexpected end of config file.");
838 throw new IOException("Newline in quotes not allowed.");
847 if (Character
.isWhitespace((char) c
)) {
851 if (';' == c
|| '#' == c
) {
857 if (value
.length() > 0) {
866 throw new IOException("End of file in escape.");
885 throw new IOException("Bad escape: " + ((char) c
));
892 value
.append((char) c
);
894 return value
.length() > 0 ? value
.toString() : null;
897 public String
toString() {
898 return "RepositoryConfig[" + configFile
.getPath() + "]";
914 boolean match(final String aBase
, final String aExtendedBase
,
915 final String aName
) {
916 return eq(base
, aBase
) && eq(extendedBase
, aExtendedBase
)
920 private static boolean eq(final String a
, final String b
) {
923 if (a
== null || b
== null)